Atlantis is an infrastructure as code (IaC) tool to automate Terraform interactions within your GitLab/GitHub MR/PR workflow. This post uses the setup explained in an older post, where we used GitLab as Terraform backend.

Instead of running terraform init/apply/plan on your local dev machine, you can now run it automagically when opening a merge request. This works by interacting via comments with Atlantis.

How to interact with atlantis inside an MR.

Setup

1. Preparations

First generate a personal access token for the gitlab user with scope api. As this user will comment on MRs you should create a new atlantisbot user or reuse an already existing bot user.

Then generate a secret which will be shared between GitLab and Atlantis. For example with pwgen -s 64 1 . This is not necessary but helps against random request against your Atlantis instance, as it will only accept requests with this secret in the header.

2. Setup Atlantis deployment.

Most of my services are running as containers and as Atlantis provides an upstream image there is no good reason to not use it. All the TF_* variables must match the configured Terraform backend. HCLOUD_TOKEN and HETZNER_DNS_API_TOKEN are specific token needed by the Hetzner Cloud and Hetzner Cloud DNS Terraform provider.

version: "3"
services:
  atlantis:
    image: runatlantis/atlantis
    container_name: atlantis
    restart: always
    ports:
      - 127.0.0.1:4141:4141
    environment:
      ATLANTIS_GITLAB_TOKEN: "wasd"
      ATLANTIS_GITLAB_WEBHOOK_SECRET: "wasd"
      HCLOUD_TOKEN: "wasd"
      HETZNER_DNS_API_TOKEN: "wasd"
      TF_HTTP_ADDRESS: https://gitlab.mydomain.com/api/v4/projects/<ID>/terraform/state/project
      TF_HTTP_LOCK_ADDRESS: https://gitlab.mydomain.com/api/v4/projects/<ID>/terraform/state/project/lock
      TF_HTTP_UNLOCK_ADDRESS: https://gitlab.mydomain.com/api/v4/projects/<ID>/terraform/state/project/lock
      TF_HTTP_USERNAME: me
      TF_HTTP_PASSWORD: password
      TF_HTTP_LOCK_METHOD: POST
      TF_HTTP_UNLOCK_METHOD: DELETE
    command:
      - server
      - --atlantis-url=https://atlantis.mydomain.com
      - --gitlab-user=my-bot
      - --repo-allowlist=gitlab.mydomain.com/my-project/iac
      - --gitlab-hostname=gitlab.mydomain.com

Add repositories in the repo-allowlist parameters to enable Atlantis for these specific repositories.

3. Register a GitLab webhook in your project.

Add a webhook to all projects previously added in the repo-allowlist option. Select the trigger push events, comments and merge request events. Also check that the URL ends with /events.

Atlantis webhook

4. Setup Caddy as reverse proxy to expose Atlantis.

Setup caddy as a revsere proxy to handle TLS certificates and disable all log outputs.

atlantis.mydomain.com {
  reverse_proxy http://localhost:4141 {
    header_up Host {http.reverse_proxy.upstream.hostport}
  }
  log {
    output discard
  }
}

Secure access to the dashboard

Atlantis provides a dashboard that displays current locks on running deployments. The lock locks the Terraform state to mitigate multiple apply operations. This is a neat feature as it prevents an inconsistent Terraform state. But it also leaks the name of your repos/projects if the dashboard is available on the public internet (and anyone with access to it can remove any state lock). Also the /events endpoint must be reachable from the public internet as it will be called by the earlier registered webhook(s).

Atlantis dashboard

This is an task for the basicauth Caddy module. The password must be hashed as Caddy will not accept plain text passwords. You can generate a password with caddy hash-password.

root@server:~# caddy hash-password --plaintext my-secure-password
JDJhJDE0JGVsMXhGeDI0UTZ4SkhhSi9yWlZ5Uk93Q08uU0twa05vOXNQVDY0MFpkVGdZMjZUT09Ha2hH
atlantis.mydomain.com {
  reverse_proxy http://localhost:4141 {
    header_up Host {http.reverse_proxy.upstream.hostport}
  }
  basicauth / {
    my-user JDJhJDE0JGVsMXhGeDI0UTZ4SkhhSi9yWlZ5Uk93Q08uU0twa05vOXNQVDY0MFpkVGdZMjZUT09Ha2hH
  }
  log {
    output discard
  }
}

Conclusion

Atlantis provides an easy way to let teams work together without problems like locking the state because two members are running Terraform on their local machine at the same time. The interaction within MRs/PRs is simple and it also makes changes visible within an MR/PR. With that a reviewer can directly see what Terraform will do and doesn’t have to check out the branch locally and run a terraform plan.