Create GitHub Repo and Terraform Cloud Project

Copy and paste the following into a new file, tfcloud_pipeline_project.tf in the tfcloud-mgmt repo.

locals {
  tfcloud_pipeline_project_name = "tfcloud-pipeline"

  tfcloud_pipeline_environment_configuration = {

    "pre-dev" = {
      aws_account_id = "320797911953"
      regions = [
        "eu-west-2"
      ]
    },

    "dev" = {
      aws_account_id = "320797911953"
      regions = [
        "eu-west-2",
      ]
    },

    "test" = {
      aws_account_id = "320797911953"
      regions = [
        "eu-west-2",
      ]
    },

    "prod" = {
      aws_account_id = "320797911953"
      regions = [
        "eu-west-2",
      ]
    },
  }

  tfcloud_pipeline_workspace_names = flatten([for k, v in local.tfcloud_pipeline_environment_configuration : module.tfcloud_pipeline_workspaces[k].workspace_names])
}

resource "github_repository" "tfcloud_pipeline" {
  name               = local.tfcloud_pipeline_project_name
  auto_init          = true
  gitignore_template = "Terraform"
  license_template   = "mit"

  lifecycle {
    prevent_destroy = true
  }
}

resource "github_branch_default" "tfcloud_pipeline_main" {
  repository = github_repository.tfcloud_pipeline.name
  branch     = "main"
}

resource "github_branch_protection" "tfcloud_pipeline_main" {
  repository_id  = github_repository.tfcloud_pipeline.name
  pattern        = github_branch_default.tfcloud_pipeline_main.branch
  enforce_admins = true

  required_status_checks {
    strict   = false
    contexts = formatlist("Terraform Cloud/%s/%s", tfe_organization.example.name, local.tfcloud_pipeline_workspace_names)
  }
}

resource "tfe_project" "tfcloud_pipeline" {
  organization = tfe_organization.example.id
  name         = local.tfcloud_pipeline_project_name
}

resource "tfe_project_variable_set" "tfcloud_pipeline_aws_common" {
  project_id      = tfe_project.tfcloud_pipeline.id
  variable_set_id = tfe_variable_set.aws_common.id
}

resource "tfe_variable_set" "tfcloud_pipeline_common" {
  name         = "${local.tfcloud_pipeline_project_name}-common"
  description  = "Variables common to all workspaces within the ${local.tfcloud_pipeline_project_name} project"
  organization = tfe_organization.example.id
}

resource "tfe_variable" "tfcloud_pipeline_common_tfcloud_project" {
  category        = "terraform"
  key             = "tfcloud_project"
  value           = local.tfcloud_pipeline_project_name
  description     = "Name of the Terraform Cloud Project"
  variable_set_id = tfe_variable_set.tfcloud_pipeline_common.id
}

resource "tfe_project_variable_set" "tfcloud_pipeline_common" {
  project_id      = tfe_project.tfcloud_pipeline.id
  variable_set_id = tfe_variable_set.tfcloud_pipeline_common.id
}

module "tfcloud_pipeline_workspaces" {
  for_each = local.tfcloud_pipeline_environment_configuration
  source   = "./modules/tfcloud_aws_workspace"

  tfe_project = {
    tfe_organization = tfe_project.tfcloud_pipeline.organization
    project_name     = tfe_project.tfcloud_pipeline.name
    project_id       = tfe_project.tfcloud_pipeline.id
  }

  pipeline_environment_name          = each.key
  pipeline_environment_configuration = each.value
  vcs_repo_name                      = github_repository.tfcloud_pipeline.full_name
  vcs_repo_oauth_client_token_id     = tfe_oauth_client.example_github.oauth_token_id
}

resource "tfe_run_trigger" "tfcloud_pipeline_dev_eu_west_2" {
  workspace_id  = module.tfcloud_pipeline_workspaces["dev"].workspace_ids["tfcloud-pipeline-dev-eu-west-2"]
  sourceable_id = module.tfcloud_pipeline_workspaces["pre-dev"].workspace_ids["tfcloud-pipeline-pre-dev-eu-west-2"]
}

resource "tfe_run_trigger" "tfcloud_pipeline_test_eu_west_2" {
  workspace_id  = module.tfcloud_pipeline_workspaces["test"].workspace_ids["tfcloud-pipeline-test-eu-west-2"]
  sourceable_id = module.tfcloud_pipeline_workspaces["dev"].workspace_ids["tfcloud-pipeline-dev-eu-west-2"]
}

resource "tfe_run_trigger" "tfcloud_pipeline_prod_eu_west_2" {
  workspace_id  = module.tfcloud_pipeline_workspaces["prod"].workspace_ids["tfcloud-pipeline-prod-eu-west-2"]
  sourceable_id = module.tfcloud_pipeline_workspaces["test"].workspace_ids["tfcloud-pipeline-test-eu-west-2"]
}

Adjust the aws_account_id values to match your AWS account setup. Whilst the pre-requisites only strictly need us to have a single AWS account, it's strongly recommended to maintain separate accounts for your different path to production environments. AWS has some guidance on this topic if you wish to explore it further but the main reasons for account separation are:

  • Minimises the "blast radius" of changes; if your production account is the only one that contains production resources, then a change to your dev account can't possibly affect your production service.
  • Some AWS services, most notably IAM and Route53 are global in nature. For example, a change to an IAM role in an account shared between environments will affect all environments at the same time.

One option to avoid some of the IAM-related problems that can arise from having a shared account is to prefix or suffix the role name such that unique roles are created in each workspace. This is alluded to above, and more clearly shown in the next section when we create the roles necessary for OIDC authentication.

The code above creates 4 workspaces, each one representing a separate stage in the path to production.

Commit and push your changes to a branch, and raise a PR. The resulting Terraform Cloud plan should show 35 resources will be created. Go ahead and merge the PR, then apply the changes.

If you take a look at the new tfcloud-pipeline project in the Terraform Cloud UI, you'll see that it has the expected 4 workspaces configured and that each of them had a plan triggered by the initial commit to the new tfcloud-pipeline GitHub repo. Again, as expected at this stage, all of those runs failed due to a lack of Terraform code in the repository. The next section will bootstrap the OIDC authentication between Terraform Cloud and your AWS account so that Terraform Cloud can plan and apply changes successfully.