blog dots

devops

 

How To: Bootstrap a Terraform Cloud Governance Workspace

While working with Terraform, you’ll eventually find advice to keep your configurations small. There are excellent reasons to follow this advice if you understand that “smallness” is not the end goal. Instead, the goal is to reduce the blast radius if anything goes wrong, make the code easier to reason about, and keep independent infrastructure independent. The goals provide a good rule of thumb for creating appropriately sized Terraform projects.

Once you get the knack for creating small Terraform configurations, you’re going to need a lot of workspaces. It would be a shame to completely automate your cloud infrastructure only to find yourself using the Terraform Cloud portal to manage an extensive collection of workspaces. Fortunately, Terraform provides a way to manage Terraform Cloud workspaces using Terraform.

Using Terraform to manage Terraform Cloud creates a chicken-and-egg problem. To solve this problem, run your initial configuration locally to establish the workspace, then migrate to use that workspace. Read on to learn the steps needed to bootstrap your first workspace.

Bootstrap Components

Terraform Cloud architecture diagram

The diagram shows the components you’ll build to bootstrap your initial workspace. You will use this workspace to create additional workspaces in the future. Naturally, you will need to sign-up for Terraform Cloud. Once you have an account, generate a user token with the necessary rights to create organizations and workspaces. The example populates the organization’s private registry with custom modules hosted on GitHub. You need a GitHub account and a personal access token with appropriate permissions to make that GitHub connection.

The code sample below will configure the following (you can view a starter version here):

  • A new Terraform Cloud organization with a VCS connection to GitHub
  • Import a few pre-made modules from my public GitHub organization
  • Creates a root governance workspace with an environment variable to hold the Terraform Cloud token.

main.tf:

resource "random_pet" "instance" {}

module "organization" {
  source = "github.com/jamesrcounts/terraform-tfe-organization.git"

  email      = "nobody@example.com"
  github_pat = var.github_pat
  name       = random_pet.instance.id
}

module "registry" {
  source = "github.com/jamesrcounts/terraform-tfe-registry.git"

  oauth_token_id = module.organization.github_token_id


  modules = {
    organization = "jamesrcounts/terraform-tfe-organization"
    workspace    = "jamesrcounts/terraform-tfe-workspace"
    registry     = "jamesrcounts/terraform-tfe-registry"
  }
}

module "workspace" {
  source = "github.com/jamesrcounts/terraform-tfe-workspace.git"

  name              = "governance-root"
  organization_name = module.organization.name

  environment = {
    TFE_TOKEN = {
      description = "An admin token for Terraform Cloud"
      sensitive   = true
      value       = var.tfe_token
    }
  }
}

Bootstrapping

Before kicking off the bootstrapping process, ensure you have your two tokens ready: one user API token for Terraform Cloud and a personal access token for GitHub.

  1. Start by adding your Terraform Cloud token to your local environment. Terraform will need this to configure the Terraform Enterprise (TFE) provider. In a Linux or Mac, or WSL shell, run a command like this:

    $ export TFE_TOKEN=YOUR_TFE_TOKEN

    Of course, you should add your token where it says YOUR_TFE_TOKEN before running the command.

  2. Next, create a file named “terraform.auto.secrets.tfvars” and add the following contents:

    github_pat = "YOUR_GITHUB_PAT"
    tfe_token   = "YOUR_TFE_TOKEN"

    As before, replace YOUR_GITHUB_PAT and YOUR_TFE_TOKEN with your secrets. If you cloned the example repository, the “.gitignore” file filters “*.auto.secrets.tfvars” to prevent git from adding your secrets to source control.

    You may notice that we add the Terraform Cloud token to the environment and the terraform variables file. This duplication is not a mistake. Terraform uses the environment variable to configure the TFE provider but Terraform also needs access to the token as a variable so it may inject that value into the workspace it is building.

  3. In the same shell used for step 1, initialize the terraform configuration.

    $ terraform init

    This command will download the providers from the public Terraform registry, the modules from my GitHub repository, and generate the Terraform version lock file (.terraform.lock.hcl), which you should add to source control.

  4. Continue in the same shell. It’s time to create our resources!

    $ terraform apply

    Terraform’s plan shows eight new resources. The three modules generate seven resources, and the final resource is the random_pet resource that the configuration uses to create the organization name. Type ‘yes’ at the prompt to confirm and Terraform will create and configure the organization and workspace.

  5. When Terraform finishes, visit Terraform Cloud and navigate to your new workspace. Terraform generated “welcome-cow” as my random ID, so I now have an organization with that name and a “governance-root” workspace inside. The overview provides me with the code I need to add to my local config to configure this workspace as the new backend:

    Terraform configuration
  6. Copy the “cloud” block from the overview page and add it to the terraform block in “versions.tf”. The code below shows my terraform block after making these changes. Remember to update your organization name with the random ID Terraform generated for you.

    versions.tf:

    terraform {
      required_version = ">= 1"
    
      cloud {
        organization = "welcome-cow"
    
        workspaces {
          name = "governance-root"
        }
      }
    
      required_providers {
        random = {
          source  = "hashicorp/random"
          version = "~>3.1.0"
        }
        tfe = {
          source  = "hashicorp/tfe"
          version = "~> 0.28.1"
        }
      }
    }
  7. Now re-run the initialization to update the backend.

    $ terraform init

    Terraform will detect the existing state file on your machine and prompt you to copy it into the new backend. Confirm by typing “yes” at the prompt!

  8. Before testing the new backend, clean up the old “terraform.tfstate” file. Open the file in your editor, and you will see that it is empty. Delete it to avoid an error when running in Terraform Cloud.

  9. Now apply again. You will notice output indicating that this run executes in Terraform Cloud. Terraform should find no other changes—which is a good thing. You have bootstrapped your state into Terraform Cloud without any side effects!

  10. The final bootstrapping step is to bootstrap your modules. The code currently loads modules from my GitHub repository. To get full advantage of your Terraform Cloud account, you will want to pull these modules from the private registry associated with your new organization. Navigate to the registry and view modules. Each module will provide an example of the code you need to reference the module.

    Examle code
  11. The code below shows my module call sites after migrating to the private registry. Your code should look similar, but the source argument contains a different organization name.

    main.tf:

    resource "random_pet" "instance" {}
    
    module "organization" {
      source  = "app.terraform.io/welcome-cow/organization/tfe"
      version = "0.0.3"
    
      email      = "nobody@example.com"
      github_pat = var.github_pat
      name       = random_pet.instance.id
    }
    
    module "registry" {
      source  = "app.terraform.io/welcome-cow/registry/tfe"
      version = "0.0.1"
    
      oauth_token_id = module.organization.github_token_id
    
    
      modules = {
        organization = "jamesrcounts/terraform-tfe-organization"
        workspace    = "jamesrcounts/terraform-tfe-workspace"
        registry     = "jamesrcounts/terraform-tfe-registry"
      }
    }
    
    module "workspace" {
      source  = "app.terraform.io/welcome-cow/workspace/tfe"
      version = "0.0.4"
    
      name              = "governance-root"
      organization_name = module.organization.name
    
      environment = {
        TFE_TOKEN = {
          description = "An admin token for Terraform Cloud"
          sensitive   = true
          value       = var.tfe_token
        }
      }
    }
  12. After changing all module source addresses, re-run the initialization, then apply to check for changes.

    $ terraform init
    $ terraform apply

    Even though the modules now come from the private registry, the plan should indicate no changes needed. There are no unwanted side effects because the modules are the same.

Conclusion

You’re now ready to use Terraform Cloud to manage your workspaces. You can create more workspaces and add additional custom modules by updating this project. Future projects will not need these bootstrapping steps because this configuration will create those workspaces ahead of time. The new Terraform solutions can adopt Terraform Cloud immediately.


The first step starts with you

Get in touch!

The bigger the challenge, the more excited we get. Don’t be shy, throw it at us. We live to build solutions that work for you now and into the future. Get to know more about who we are, what we do, and the resources on our site. Drop a line and let’s connect.