Continuous Integration and Deployment (CI/CD)

The GitHub Control is a Terraform live repository [1]. The Continuous Integration part of the workflow ensures that a proposed change is syntactically correct and it prepares the terraform plan output for the code review. The Continuous Deployment part of the workflow applies the suggested change i.e. runs terraform apply.

The document describes how CI/CD is implemented in GitHub Control.

GitHub Actions

GitHub Actions acts as execution engine for the GitHub Control’s CI/CD. GitHub Actions workers run both terraform plan, terraform apply, linters, and other commands. Even though GitHub Actions is tightly integrated with GitHub itself, the worker still needs a GitHub token so the GitHub provider can access the GitHub organization and make appropriate changes. Besides the GitHub token the workers need AWS credentials to work with the Terraform state file.

Repository settings

In the context of the CI/CD the most important part of the repository configuration is branch protection. The default branch (that happens to have the name “main”) needs to be “protected”.

Code review rules

Normally you want to require a code review for a pull request. There is only one user in GitHub Control, so only pull requests are required. It means direct pushes to the main branch are not allowed.

Code review settings

Fig. 3 Code review settings.

Status check rules

A Terraform CI workflow runs terraform plan, so we want to enable it.

Require branches to be up to date before merging is especially important when it comes to Terraform repositories. It requires a pull request to include all known commits from the default branch. If this option is not set, a user may unintentionally destroy resources created in recent commits.

Code review settings

Fig. 4 Status checks settings.

Continuous Integration

A Terraform CD workflow defined in .github/workflows/terraform-CI.yml triggers on a new pull request or any updates in it. Among other trivial steps like running a linter and checking code style there are two important steps.

Listing 1 Step 1. terraform plan.
# Generates an execution plan for Terraform
- name: Terraform Plan
run: |
    make plan

Not only the step runs terraform plan but it also saves the plan in a file. The plan file will be used by Continuous Deployment.

Listing 2 Step 2. Save the plan [2].
# Upload Terraform Plan
- name: Upload Terraform Plan
run: |
    ih-plan upload \
        --key-name=plans/${{ github.event.pull_request.number }}.plan \
        tf.plan

This step uploads the plan file to the same S3 bucket as the Terraform state. ih-plan [2] parses terraform.tf to get the bucket. The plan is identified by a pull request number.

It is important to note, when the pull request is reviewed, not only the code is a subject for a review but so is the plan. We want to execute the approved plan, not regenerated one later on.

Continuous Deployment

The deployment workflow is defined in .github/workflows/terraform-CD.yml. It is triggered when a pull request is closed.

Listing 3 Trigger condition.
on:  # yamllint disable-line rule:truthy
  pull_request:
    types:
      - closed

Why not on a push to the default branch? The matter is we need to know what pull request is being merged into the main branch. In a context of the push event there is no a pull request number and we need it to download the plan.

Listing 4 Download the plan [2].
# Download a plan from the approved pull request
- name: Download plan
run: |
    ih-plan download \
        plans/${{ github.event.pull_request.number }}.plan \
        tf.plan

When the plan is downloaded, the worker can execute it:

Listing 5 Execute the plan.
# Execute the plan
- name: Terraform Apply
run: make apply

Thus terraform apply applies only approved plan exactly as it was shown in the pull request.