Photo by Michael Dziedzic on Unsplash

This was originally posted on May 30th, 2018 on Medium

GitLab Review Apps are a convenient way of managing dynamic environments for the purpose of reviewing changes before merging into your main branch. GitLab has great Kubernetes support and can easily deploy to your clusters, but if your application is not nicely containerized or your team is not ready to take on Kubernetes, what do you do? You Terraform!


Terraform

Terraform is an excellent cloud-agnostic tool for developing your infrastructure as code. By combining a couple features of Terraform, we can pretty easily build a system for deploying Review Apps.

1. Workspaces

Workspaces provide a way of provisioning and managing multiple sets of identical infrastructure without copying Terraform configuration files. If you are familiar with Terraform and its .tfstate files, you can think of Workspaces as creating and managing new arbitrarily named .tfstate files.

For example, terraform workspace new $BRANCH creates a new Workspace which is named with the value of the environment variable $BRANCH. This creates a new blank .tfstate, meaning a terraform apply will provision a new set of resources that are managed independently of any resources managed by any other workspace.

2. Remote State

Remote State is a way to persist Terraform state across multiple machines by storing .tfstate files in one of several supported remote storage mechanisms, such as Amazon S3. While this is most commonly used to allow people to collaboratively work on infrastructure, it can easily be used within GitLab jobs to provision and keep track of infrastructure managed by Terraform.

The key here is that Remote State persists Workspaces, allowing GitLab CI/CD to reference the Workspaces created for each branch across different jobs that will be ran at different times from different machines.


GitLab CI/CD

The .gitlab-ci.yml configuration for this is pretty straightforward, but there are a few key points.

variables:
  TERRAFORM_TAG: 0.0.7

StartReview:
  stage: Review
  when: manual
  image: $CI_REGISTRY/yourcompany/docker/deployment:latest
  dependencies: []
  only:
    - branches
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.yourcompany.com
    on_stop: StopReview
  script:
    - git clone -b $TERRAFORM_TAG --depth 1 https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/yourcompany/terraform.git
    - cd terraform/review/
    - terraform init
    - terraform workspace select $CI_COMMIT_REF_SLUG || terraform workspace new $CI_COMMIT_REF_SLUG
    - export TF_VAR_subdomain=$CI_COMMIT_REF_SLUG
    - terraform apply -auto-approve
    - export DEPLOYMENT_GROUP=$(terraform output codedeploy_deployment_group)
    - cd $CI_PROJECT_DIR
    - pipelines_scripts/deploy_application $DEPLOYMENT_GROUP

There are three important pieces of configuration here, all within the environment block. 1. name is the dynamically built name of the environment itself. In this case it is based on the the CI_COMMIT_REF_SLUG variable which is the URL-friendly representation of the branch name. 2. url is the URL that GitLab associates to the environment. Note that GitLab does not create the DNS record for this, it is up to you to create it. 3. on_stop tells GitLab which job should be triggered when the branch is closed or the environment is manually stopped, thus finishing off what makes this a Review App and not just a dynamically named environment.


The script portion of the configuration is where Terraform comes in.

We need to store the Terraform configuration outside of this repository because we need the configuration files to be available after the branch is closed.

Referencing a particular Git tag when cloning this repository is necessary to ensure that the infrastructure used to provision these applications does not change without you knowing.

As mentioned earlier, Terraform Workspaces is one of the big features that makes Review Apps with Terraform possible. We can define the infrastructure required to run the application and then use Workspaces to manage isolated copies of this infrastructure.

terraform workspace select $CI_COMMIT_REF_SLUG || terraform workspace new $CI_COMMIT_REF_SLUG

Here we are selecting the Workspace named with the value of $CI_COMMIT_REF_SLUG or we are creating it if it does not exist. This ensures that we can run the the StartReview job multiple times from a single branch without having issues.

The rest of the script is a standard terraform apply followed by some CodeDeploy specific stuff. I am not going into deployment specifics in this article since there are many tools for doing it, but if you are interested in setting up CodeDeploy I have written about it in the past.


The StopReview job is very similar to StartReview.

StopReview:
  stage: Review
  when: manual
  image: $CI_REGISTRY/yourcompany/docker/deployment:latest
  dependencies: []
  only:
    - branches
  variables:
      GIT_STRATEGY: none
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  script:
    - git clone -b $TERRAFORM_TAG --depth 1 https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/yourcompany/terraform.git
    - cd terraform/review/
    - terraform init
    - terraform workspace select $CI_COMMIT_REF_SLUG
    - export TF_VAR_subdomain=$CI_COMMIT_REF_SLUG
    - terraform destroy -auto-approve
    - terraform workspace select default #can't delete current workspace
    - terraform workspace delete $CI_COMMIT_REF_SLUG

The GIT_STRATEGY environment variable set to none makes sure that GitLab does not try to clone the branch when running this job. This is necessary because this job may be ran after the branch is deleted, and thus with nothing to clone.

The combination of action: stop and the name property is what tells GitLab which environment this job is meant to stop. The name here needs to match the name set in StartReview.

Getting to the script, we again clone the Terraform repository with the same tag used earlier.

We select the Terraform Workspace corresponding to this branch (it will already be created if we are running this job) and run terraform destroy -auto-approve to de-provision the infrastructure that was created for this branch’s Review App.

To clean everything up, we then switch the default Workspace and delete the Workspace we created for this branch. Terraform does not let you delete the currently selected Workspace, which is why we need to switch back to default.


At this point you should have infrastructure that can be provisioned at will dynamically based on branch name and then destroyed either at will or automatically when the branch is closed. Integrating your deployment tool of choice on top of this will give you fully functional Review Apps with their own infrastructure even if your application is not ready for Kubernetes.