Having infrastructure as code (IaC) means we can better manage software infrastructure components throughout their lifecycles. Terraform provides a way to responsibly manage infrastructure state at scale and has achieved mass adoption in industry as an open-source project. You can tell terraform what exactly you would like to create, and terraform gives you a plan showing the configuration changes that will happen upon approval. Here’s an example of some terraform code that could be used to create a storage bucket in Google Cloud Platform.
provider "google" {
credentials = "google_credentials.json"
project = "example-project"
}
resource "google_storage_bucket" "example" {
name = "example-storage-bucket-1234"
location = "US"
force_destroy = true
public_access_prevention = "enforced"
}
Plan and Apply
If we make a file bucket.tf
with the above code and place it in the same directory as our keyfile, then we can run terraform init
and then terraform plan
to get the following output.
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# google_storage_bucket.example will be created
+ resource "google_storage_bucket" "example" {
+ effective_labels = (known after apply)
+ force_destroy = true
+ id = (known after apply)
+ location = "US"
+ name = "example-storage-bucket-1234"
+ project = (known after apply)
+ public_access_prevention = "enforced"
+ rpo = (known after apply)
+ self_link = (known after apply)
+ storage_class = "STANDARD"
+ terraform_labels = (known after apply)
+ uniform_bucket_level_access = (known after apply)
+ url = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Running terraform apply
at this point would apply the changes in the plan and create a single private storage bucket in Google Cloud. Running the terraform destroy
command from the same directory will delete the bucket.
Pipeline
In an organization having multiple developers and lots more infrastructure in terraform, things are organized roughly as follows. Terraform code will be stored in a git repository like GitHub where developers can publish new code, re-use existing code, and review code changes. Any proposed change automatically runs the terraform plan
command and attaches the output to the proposal–making it easy for reviewers to see what will be added, changed, or destroyed according to the plan.
Here is an example using GitHub Actions. The folder structure looks something like this:
.
├── .github/
│ └── workflows/
│ ├── tfplan.yaml
│ └── tfapply.yaml
└── terraform/
├── bucket.tf
├── variables.tf
├── backend.tf
├── provider.tf
└── terraform.tfvars
Workflow #1: Generate the plan and post it as a comment to the pull request. Our credentials are now stored in GitHub environment variables for the repository.
name: Terraform Plan
on:
pull_request:
branches:
- main
jobs:
terraform:
runs-on: ubuntu-latest
env:
GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}
steps:
- name: Checkout code // Get the latest changes
uses: actions/checkout@v2
- name: Install Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.0.0
- name: Terraform Init
run: |
cd terraform
terraform init -reconfigure
- name: Terraform Plan
id: plan
run: |
cd terraform
terraform plan -no-color -out=tfstate // Generate plan
terraform show tfstate // Output plan
- name: Post Plan Output to PR
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const plan_output = `${{ steps.plan.outputs.stdout }}`
.replace(/\u001b\[.*?m/g, ''); // Remove ANSI escape codes
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '```' + plan_output + '```'
});
Workflow #2: Regenerate a plan (in case other PRs have merged in the meantime) and execute.
name: Terraform Apply
on:
pull_request:
types:
- closed // PR is closed
branches:
- main
jobs:
terraform:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true // PR is merged
env:
GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.0.0
- name: Terraform Init
run: |
cd terraform
terraform init
- name: Terraform Plan
id: plan
run: |
cd terraform
terraform plan
- name: Terraform Apply
run: terraform apply -auto-approve
GitHub Configuration
Running these plans without configuring proper permissions and integrations in GitHub will result in an error like “resource not available for integration.” To set up the app, go to https://github.com/settings/apps and create a new app with the repository link as the Homepage URL and read/write permissions on issues, discussions, and pull requests.