Environments

Resourcely Uses Terraform variable values to manage multiple environments with the same blueprint.

You must define a CI/CD configuration to deploy Terraform to different environments based on directory structure.

Configuring Resourcely.yaml

This guide will walk you through configuring .resourcely.yaml in your Terraform repository.

What is .resourcely.yaml?

.resourcely.yaml is a config file in repos to which Resourcely Create can emit Terraform. It provides metadata about the structure of the Terraform config within the repo:

  • paths to the config roots in the Repo. These are where we can emit Terraform.

  • for config roots that are instantiated in multiple environments, what these environments are and where the environment-specific variable values (*.tfvars) are stored.

Resourcely Create will use this metadata to help developers

  • place their Terraform config in the correct paths

  • provide values for all environments

  • without them needing to be aware of the repository or Terraform structure themselves.

Where to place .resourcely.yaml?

The .resourcely.yaml files may be placed anywhere within a repo. Multiple such files are allowed, primarily to support large mono-repos where subdirectories are owned by different teams.

The only restriction is that .resourcely.yaml files cannot be nested. No subdirectories below a .resourcely.yaml may contain a .resourcely.yaml.

.resourcely.yaml Structure

There are a variety of ways that Terraform config can be structured, including:

  • no-envs: one config root, no environment, one state file

  • multiple-envs: one config root, *.tfvars file per environment, state file per environment

    • There’s an alternative flavor of this where the .tfvars file is not checked in. Instead the runner injects it somehow - by writing a .tfvars file, by writing a .tf file, or by adding -var 'key=val' to the command line.

  • common-module: a config root covering each shared module, and a config root per environment that instantiates the modules (and any env-specific resources) (coming soon)

    • Each shared module contains references to the module input (as Terraform Variables)

Rather than trying to define one config format that can describe all of these variations, we’ll support various types.

version: "2"

# Each .resourcely.yaml file manages all config roots in or below its directory.
# 
# - A subdirectory of a .resourcely.yaml directory may not contain its own
#   .resourcely.yaml file.
#
# - All the paths and files referenced in a .resourcely.yaml must be in or below
#   its directory. References cannot "escape" that directory. 

# List of directories that contain Terraform configuration.
#
# In the Resourcely Pull Request UI, a developer will pick one of these
# locations to put their new resources in.
terraform_config_roots:
  
  - # Name that will be shown in the Resourcely Pull Request UI
    # dropdown menu.
    name: <string>

    # (optional)
    #
    # Longer description for the config root. If developers may have trouble
    # selecting the correct config root, use this text to help guide them.
    description: <string>

    # (optional)
    #
    # Instructs Resourcely to ignore this config root. It will not be
    # shown in the Resourcely Pull Request UI dropdown menu.
    skip: true | false

    # Path to the config root, the directory containing the *.tf
    # files.
    #
    # Relative to the location of this .resourcely.yaml file
    # 
    # If the config root is the same directory, specify
    #     path: .
    #
    # Example: projectA
    path: <string>

    # Name of the file in the `path` directory in which to place
    # new resources by default.
    #
    # Developers may pick a different file, but will be defaulted to this one.
    #
    # Example: main.tf
    default_file: <string>

    # Name of the file in the `path` directory in which to place new 
    # variable declarations for new environment-specific values.
    # 
    # For `common-module` configs, these variables are the module inputs.
    #
    # Example: variables.tf
    var_file: <string>
    
    # (optional)
    # 
    # Name of the file in which to place the variables definitions
    # for this config root.
    #
    # Mutually exclusive with the `environments` option below. Use this if
    # your config root does not have multiple environments, but does use
    # tfvars as way of defining "constant" values.
    #
    # The config root name will be used for `context.environment` in
    # guardrails and blueprints.
    #
    # Relative to the config root `path` directory.
    #
    # Example: constants.tfvars
    tfvars_file: <string>

    # (optional)
    #
    # List of the environments that this config root supports.
    #
    # Mutually exclusive with the `tfvars_file` option above. Use this if
    # you run `terraform apply` in this config root multiple times with
    # different values for the vars.
    #
    # If this list is missing or empty and `tfvars_file` above is not set,
    # developers will not be allowed to use Terraform variables.
    #
    # If non-empty, a developers will be allowed to use Terraform variables.
    # For any new variable, they will have to supply a value for each
    # environment in this list.
    environments:

      - # Name of the environment.
        #
        # This name is used for two purposes.
        # 1. It is shown in the UI to the developers.
        # 2. It is used as the value for `context.environment` in guardrails
        #    and blueprints.
        name: <string>

        # (`simple` config roots only)
        #
        # Name of the file in which to place the variables definitions
        # for this environment.
        #
        # Relative to the config root `path` directory.
        #
        # Example: envs/dev.tfvars
        tfvars_file: <string>

Examples

# Repo with one Terraform root module.
#
# Assume the repo is stuctured like
#
# network.tf
# main.tf
# providers.tf
# terraform.tf 

version: "2"

terraform_config_roots:
  - name: "Project A"
    path: .
    default_file: main.tf
# Repo with one Terraform root module that use tfvars
# to hold constant values, but not for multiple environments.
#
# Assume the repo is stuctured like
#
# network.tf
# main.tf
# providers.tf
# terraform.tf 

version: "2"

terraform_config_roots:
  - name: "Project B"
    path: .
    default_file: main.tf
    var_files: variables.tf
    tfvars_file: constants.tfvars
# Repo with one Terraform root module that uses tfvars 
# to instantiante in multiple environments.
#
# Assume the repo is stuctured like
#
# network.tf
# main.tf
# providers.tf
# terraform.tf 
# variables.tf
# envs/
#   | dev.tfvars
#   | stage.tfvars
#   | envs/prod.tfvars

version: "2"

terraform_config_roots:
  - name: "Project C"
    path: .
    default_file: main.tf
    var_file: variables.tf
    environments:
      - name: dev
        tfvars_file: envs/dev.tfvars
      - name: stage
        tfvars_file: envs/stage.tfvars
      - name: prod
        tfvars_file: envs/prod.tfvars

# Repo with multiple Terraform root modules.
#
# Assume the repo is stuctured like
#
# projectA/
#   | network.tf
#   | main.tf
#   | providers.tf
#   | terraform.tf 
# projectB/
#   | network.tf
#   | main.tf
#   | providers.tf
#   | terraform.tf 
#   | variables.tf
#   | envs/
#   |   | dev.tfvars
#   |   | stage.tfvars
#   |   | prod.tfvars

version: "2"

terraform_config_roots:
  - name: "Project A"
    path: projectA
    default_file: main.tf

  - name: "Project B"
    path: projectB
    default_file: main.tf
    var_file: variables.tf
    environments:
      - name: dev
        tfvars_file: envs/dev.tfvars
      - name: stage
        tfvars_file: envs/stage.tfvars
      - name: prod
        tfvars_file: envs/prod.tfvars

How to create resourcely.yaml

You can use the scaffolding we created to get started with the basic resourcely.yaml file.

Advanced resourcely-cli usage

If you use resourcely.yaml to define multiple config roots (and optionally, environments), you probably need to use resourcely-cli to evaluate multiple plans, one per config root (and environment).

Below, we'll use the following resourcely.yaml in examples:

terraform_config_roots:
  - name: "Global resources"
    path: global
    # no environments for the sake of example
  - name: "Regional resources"
    path: regional
    environments:
      - name: dev
        tfvars_file: regional/dev.tfvars
      - name: prod
        tfvars_file: regional/prod.tfvars

Running resourcely-cli with many plans

You can run resourcely-cli with several plans at once. For each plan, you must specify the config root (and environment, if applicable). Resourcely uses these values to merge findings from each plan and determine when it can approve the pull request.

Note: The values of --config_root_path and --environment must match the corresponding path and name fields in resourcely.yaml.

Examples:

$ resourcely-cli <other args> \
  --config_root_path global --plan global.tfplan.json

$ resourcely-cli <other args> \
  --config_root_path regional --environment dev --plan regional-dev.tfplan.json \
  --config_root_path regional --environment prod --plan regional-prod.tfplan.json

Multiple runs of resourcely-cli

Some CI/CD setups can't access every plan in one place, so they must run resourcely-cli multiple times. After each run of resourcely-cli, Resourcely will merge findings from that run over any prior findings, using the config root and (optionally) environment as the key.

An example for the sake of demonstrating the merge logic:

# Assume we ran the commands from the previous section. There were 9 findings:
# 1 in global, 3 in regional-dev, and 5 in regional-prod.

# The developer fixes 3/3 regional-dev findings, and 2/5 regional-prod findings.
# Your CI job then runs resourcely-cli for dev and prod separately.

$ resourcely-cli <other args> \
  --config_root_path regional --environment dev --plan regional-dev.tfplan.json
  
# This run replaced the 3 regional-dev findings with zero findings. There are now
# 6 findings total - 1 in global and 5 in regional-prod.

$ resourcely-cli <other args> \
  --config_root_path regional --environment prod --plan regional-prod.tfplan.json

# This run replaced the 5 regional-prod findings with the 3 un-fixed findings. There
# are now 4 findings total - 1 in global and 3 in regional-prod.

Pull request approval

When a pull request is created, Resourcely computes the set of plans that it expects to receive findings from. Resourcely will only approve the pull request once it has received findings from every plan for the PR's latest commit.

Resourcely computes the set of expected plans based on which files the pull request has changed:

  • If the PR changes a .tf file in a config root...

    • ...if that config root does not have environments, Resourcely expects one plan from it.

    • ...otherwise, Resourcely expects one plan from each of its environments.

  • Additionally, if the PR changes a .tfvars file listed in resourcely.yaml, Resourcely expects one plan from that config root and environment.

Examples:

Files changed: global/main.tf, regional/main.tf
3 expected plans: global (no env), regional dev, regional prod

# Note the change to main.tf. Resourcely will expect a plan from both envs, even
# though only one env's tfvars changed.
Files changed: regional/main.tf, regional/dev.tfvars
2 expected plans: regional dev, regional prod

Files changed: regional/prod.tfvars
1 expected plans: regional prod

Creating resourcely.yaml via the API

The following endpoint sets the Terraform configuration for a repo used with Resourcely. It can be used in conjunction with .resourcely.yaml files. If a config root is present in both an API config and a .resourcely.yaml config, the API config will take precedence.

Each POST request must include the configurations for ALL config roots that are to be set via the API. For example, if configuration roots foo, bar, and baz are set via the API in one call, and a subsequent call specifies only foo and bar, the configuration for baz will no longer be managed through the API.

Endpoint

POST /api/v1/terraform/config

Request

repo_url (string, required): Git repository URL. Resourcely will scan this repo asynchronously to determine existing resources within config roots

version (string, required): Version of config structure

Supported values: ["1", "2"]

provider (string, required): Git provider

Supported values: ["REMOTE_GIT_GITHUB", "REMOTE_GIT_GITLAB"]

terraform_configs(list, required): List of Terraform configs. See .resourcely.yaml documentation for field descriptions

An empty list will delete all existing Terraform configs set by the API. Configuration will fall back to .resourcely.yaml files (if they exist).

Example

Assume a repo is structured as follows:

# <https://github.com/ResourcelyTest/terraform>

# projectA/
#   | network.tf
#   | main.tf
#   | providers.tf
#   | terraform.tf 
# projectB/
#   | network.tf
#   | main.tf
#   | providers.tf
#   | terraform.tf 
#   | variables.tf
#   | envs/
#   |   | dev.tfvars
#   |   | stage.tfvars
#   |   | prod.tfvars
{
    "repo_url": "<https://github.com/ResourcelyTest/terraform>",
    "version": "2",
    "provider": "REMOTE_GIT_GITHUB",
    "terraform_configs": [
        {
            "name": "Project A",
            "path": "projectA",
            "default_file": "main.tf",
            "skip": false,
        },
        {
            "name": "Project B",
            "default_file": "main.tf",
            "path": "projectB",
            "skip": false,
            "var_file": "variables.tf",
            "environments": [
                {
                    "name": "dev",
                    "tfvars_file": "dev.tfvars"
                },
                {
		    "name": "stage",
		    "tfvars_file": "stage.tfvars"
                },
                {
                    "name": "prod",
                    "tfvars_file": "prod.tfvars"
                }
            ]
        }
    ]
}

Response

On success the following is expected

204 No Content

Last updated