Skip to main content

Example Workflows

Using GitHub Actions with Atmos and Terraform is fantastic because it gives you full control over the workflow. While we offer some opinionated implementations below, you are free to customize them entirely to suit your needs.

The following GitHub Workflows should be used as examples. These are created in a given Infrastructure repository and can be modified however best suites your needs. For example, the labels we've chosen for triggering or skipping workflows are noted here as "Conventions" but can be changed however you would prefer.

Atmos Terraform Plan

Conventions

Use the no-plan label on a Pull Request to skip this workflow.

The Atmos Terraform Plan workflow is triggered for every affected component from the Atmos Describe Affected workflow. This workflow takes a matrix of components and stacks and creates a plan for each, using the Atmos Terraform Plan composite action. For more on the Atmos Terraform Plan composite action, see the official atmos.tools documentation.

If an affected component is disabled with terraform.settings.github.actions_enabled, the component will show up as affected but all Terraform steps will be skipped. See Enabling or disabling components.

.github/workflows/atmos-terraform-plan.yaml
name: 👽 Atmos Terraform Plan
run-name: 👽 Atmos Terraform Plan

on:
pull_request_target:
types:
- opened
- synchronize
- reopened
branches:
- main

permissions:
id-token: write
contents: read

jobs:
atmos-affected:
if: ${{ !contains( github.event.pull_request.labels.*.name, 'no-plan') }}
name: Determine Affected Stacks
runs-on: ["self-hosted", "terraform"]
steps:
- id: affected
uses: cloudposse/github-action-atmos-affected-stacks@v4
with:
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
base-ref: ${{ github.event.pull_request.base.sha }}
head-ref: ${{ github.event.pull_request.head.sha }}
outputs:
stacks: ${{ steps.affected.outputs.matrix }}
has-affected-stacks: ${{ steps.affected.outputs.has-affected-stacks }}

atmos-plan:
needs: ["atmos-affected"]
if: ${{ needs.atmos-affected.outputs.has-affected-stacks == 'true' }}
name: Plan (${{ matrix.name }})
uses: ./.github/workflows/atmos-terraform-plan-matrix.yaml
strategy:
matrix: ${{ fromJson(needs.atmos-affected.outputs.stacks) }}
max-parallel: 1 # This is important to avoid ddos GHA API
fail-fast: false # Don't fail fast to avoid locking TF State
with:
stacks: ${{ matrix.items }}
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
sha: ${{ github.event.pull_request.head.sha }}
secrets: inherit

Atmos Terraform Apply

Conventions
  1. Use the auto-apply label on Pull Request to apply all plans on merge
  2. Use the no-apply label on a Pull Request to skip all workflows on merge
  3. If a Pull Request has neither label, run drift detection for only the affected components and stacks.

The Atmos Terraform Apply workflow runs on merges into main. There are two different workflows that can be triggered based on the given labels.

If you attach the Apply label (typically auto-apply), this workflow will trigger the Atmos Terraform Apply composite action for every affected component in this Pull Request. For more on the Atmos Terraform Apply composite action, see the official atmos.tools documentation.

Alternatively, you can choose to merge the Pull Request without labels. If the "apply" label and the "skip" label are not added, this workflow will trigger the Atmos Drift Detection composite action for only the affected components in this Pull Request. That action will create a GitHub Issue for every affected component that has unapplied changes.

.github/workflows/atmos-terraform-apply.yaml
name: 👽 Atmos Terraform Apply
run-name: 👽 Atmos Terraform Apply


on:
push:
branches:
- main

permissions:
id-token: write
contents: read
issues: write
pull-requests: write

jobs:
pr:
name: PR Context
runs-on:
- "self-hosted"
- "amd64"
- "common"
steps:
- uses: cloudposse-github-actions/get-pr@v2
id: pr

outputs:
base: ${{ fromJSON(steps.pr.outputs.json).base.sha }}
head: ${{ fromJSON(steps.pr.outputs.json).head.sha }}
auto-apply: ${{ contains( fromJSON(steps.pr.outputs.json).labels.*.name, 'auto-apply') }}
no-apply: ${{ contains( fromJSON(steps.pr.outputs.json).labels.*.name, 'no-apply') }}

atmos-affected:
name: Determine Affected Stacks
if: needs.pr.outputs.no-apply == 'false'
needs: ["pr"]
runs-on: ["self-hosted", "terraform"]
steps:
- id: affected
uses: cloudposse/github-action-atmos-affected-stacks@v4
with:
base-ref: ${{ github.event.pull_request.base.sha }}
head-ref: ${{ github.event.pull_request.head.sha }}
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
outputs:
stacks: ${{ steps.affected.outputs.matrix }}
has-affected-stacks: ${{ steps.affected.outputs.has-affected-stacks }}

plan-atmos-components:
needs: ["atmos-affected", "pr"]
if: |
needs.atmos-affected.outputs.has-affected-stacks == 'true' && needs.pr.outputs.auto-apply != 'true'
name: Validate plan (${{ matrix.name }})
uses: ./.github/workflows/atmos-terraform-plan-matrix.yaml
strategy:
matrix: ${{ fromJson(needs.atmos-affected.outputs.stacks) }}
max-parallel: 1 # This is important to avoid ddos GHA API
fail-fast: false # Don't fail fast to avoid locking TF State
with:
stacks: ${{ matrix.items }}
drift-detection-mode-enabled: "true"
continue-on-error: 'true'
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
sha: ${{ needs.pr.outputs.head }}
secrets: inherit

drift-detection:
needs: ["atmos-affected", "plan-atmos-components", "pr"]
if: |
always() && needs.atmos-affected.outputs.has-affected-stacks == 'true' && needs.pr.outputs.auto-apply != 'true'
name: Reconcile issues
runs-on:
- "self-hosted"
- "amd64"
- "common"
steps:
- name: Drift Detection
uses: cloudposse/github-action-atmos-terraform-drift-detection@v2
with:
max-opened-issues: '-1'
process-all: 'false'

auto-apply:
needs: ["atmos-affected", "pr"]
if: |
needs.atmos-affected.outputs.has-affected-stacks == 'true' && needs.pr.outputs.auto-apply == 'true'
name: Apply (${{ matrix.name }})
uses: ./.github/workflows/atmos-terraform-apply-matrix.yaml
strategy:
max-parallel: 1
fail-fast: false # Don't fail fast to avoid locking TF State
matrix: ${{ fromJson(needs.atmos-affected.outputs.stacks) }}
with:
stacks: ${{ matrix.items }}
sha: ${{ needs.pr.outputs.head }}
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
secrets: inherit

Atmos Terraform Drift Detection

Max Opened Issues

Drift detection is configured to open a set number of Issues at a time. See max-opened-issues for the cloudposse/github-action-atmos-terraform-drift-detection composite action.

The Atmos Terraform Drift Detection workflow runs on a schedule. This workflow will gather every component in every stack and run the Atmos Drift Detection composite action for each.

For every stack and component included with drift detection, the workflow first triggers an Atmos Terraform Plan.

  1. If there are changes, the workflow will then create or update a GitHub Issue for the given component and stack.
  2. If there are no changes, the workflow will check if there's an existing Issue. If there's an existing issue, the workflow will then mark that Issue as resolved.
.github/workflows/atmos-terraform-drift-detection.yaml
name: 👽 Atmos Terraform Drift Detection
run-name: 👽 Atmos Terraform Drift Detection

on:
# push:
# branches:
# - main # this is for debugging only
workflow_dispatch: {}
schedule:
- cron: "0 */12 * * *"

permissions:
id-token: write
contents: write
issues: write

jobs:
select-components:
name: Select Components
runs-on: ["self-hosted", "terraform"]
steps:
- name: Selected Components
id: components
uses: cloudposse/github-action-atmos-terraform-select-components@v2
with:
select-filter: '.settings.github.actions_enabled and .metadata.type != "abstract"'
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
outputs:
stacks: ${{ steps.components.outputs.matrix }}
has-selected-components: ${{ steps.components.outputs.has-selected-components }}

plan-atmos-components:
needs: ["select-components"]
if: ${{ needs.select-components.outputs.has-selected-components == 'true' }}
name: Detect Drift (${{ matrix.name }})
uses: ./.github/workflows/atmos-terraform-plan-matrix.yaml
strategy:
max-parallel: 1 # This is important to avoid ddos GHA API
fail-fast: false # Don't fail fast to avoid locking TF State
matrix: ${{ fromJson(needs.select-components.outputs.stacks) }}
with:
stacks: ${{ matrix.items }}
sha: ${{ github.sha }}
drift-detection-mode-enabled: "true"
continue-on-error: true
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
secrets: inherit

drift-detection:
needs: ["plan-atmos-components"]
if: always()
name: Reconcile issues
runs-on: ["self-hosted", "terraform"]
steps:
- name: Drift Detection
uses: cloudposse/github-action-atmos-terraform-drift-detection@v2
with:
max-opened-issues: '25'
process-all: 'true'

Atmos Terraform Drift Remediation

Conventions

Use the apply label to apply the plan for the given stack and component

The Atmos Terraform Drift Remediation workflow is triggered from an open Github Issue when the remediation label is added to the Issue. This workflow will run the Atmos Terraform Drift Remediation composite action for the given component and stack in the Issue. This composite action will apply Terraform using the Atmos Terraform Apply composite action and close out the Issue if the changes are applied successfully.

The drift and remediated labels are added to Issues by the composite action directly. The drift is added to all Issues created by Atmos Terraform Drift Detection. Remediation will only run on Issues that have this label. Whereas the remediated label is added to any Issue that has been resolved by Atmos Terraform Drift Remediation.

.github/workflows/atmos-terraform-drift-remediation.yaml
name: 👽 Atmos Terraform Drift Remediation
run-name: 👽 Atmos Terraform Drift Remediation

on:
issues:
types:
- labeled
- closed

permissions:
id-token: write
contents: read
issues: write

jobs:
remediate-drift:
if: github.event.action == 'labeled' && contains(github.event.issue.labels.*.name, 'apply')
name: Remediate Drift
runs-on: ["self-hosted", "terraform"]
steps:
- name: Remediate Drift
uses: cloudposse/github-action-atmos-terraform-drift-remediation@v2
with:
issue-number: ${{ github.event.issue.number }}
action: remediate
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}

discard-drift:
if: >
github.event.action == 'closed' && (
contains(github.event.issue.labels.*.name, 'drift') ||
contains(github.event.issue.labels.*.name, 'error')
) &&
!contains(github.event.issue.labels.*.name, 'remediated')
name: Discard Drift
runs-on: ["self-hosted", "terraform"]
steps:
- name: Discard Drift
uses: cloudposse/github-action-atmos-terraform-drift-remediation@v2
with:
issue-number: ${{ github.event.issue.number }}
action: discard
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}

Atmos Terraform Dispatch

The Atmos Terraform Dispatch workflow is optionally included and is not required for any other workflow. This workflow can be triggered by workflow dispatch, will take a single stack and single component as arguments, and will run Atmos Terraform workflows for planning and applying for only the given target.

This workflow includes a boolean option for both "Terraform Plan" and "Terraform Apply":

  1. If only "Terraform Plan" is selected, the workflow will call the Atmos Terraform Plan Worker (./.github/workflows/atmos-terraform-plan-matrix.yaml) workflow to create a new planfile
  2. If only "Terraform Apply" is selected, the workflow will call the Atmos Terraform Apply Worker (./.github/workflows/atmos-terraform-apply-matrix.yaml) for the given branch. This action will take the latest planfile for the given stack, component, and branch and apply it.
  3. If both are selected, the workflow will run both actions. This means it will create a new planfile and then immediately apply it.
  4. If neither are selected, the workflow does nothing.
.github/workflows/atmos-terraform-dispatch.yaml
name: 👽 Atmos Terraform Dispatch
run-name: 👽 Atmos Terraform Dispatch

on:
workflow_dispatch:
inputs:
component:
description: "Atmos Component"
type: string
stack:
description: "Atmos Stack"
type: string
plan:
description: "Terraform Plan"
type: boolean
default: true
apply:
description: "Terraform Apply"
type: boolean
default: false
distinct_id:
description: "Distinct ID"


permissions:
id-token: write
contents: read

jobs:
dispatch-id:
runs-on: ["self-hosted", "terraform"]
steps:
- name: echo Distinct ID ${{ github.event.inputs.distinct_id }}
run: echo ${{ github.event.inputs.distinct_id }}

atmos-plan:
needs: [ "dispatch-id" ]
if: ${{ inputs.plan }}
name: Plan (${{ inputs.stack }})
uses: ./.github/workflows/atmos-terraform-plan-matrix.yaml
with:
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
stacks: |
{"include": [
{"component": "${{ inputs.component }}", "stack": "${{ inputs.stack }}", "stack_slug": "${{ inputs.stack }}-${{ inputs.component }}"}
]}
secrets: inherit

atmos-apply:
needs: ["atmos-plan"]
if: ${{ inputs.apply }}
name: Apply (${{ inputs.stack }})
uses: ./.github/workflows/atmos-terraform-apply-matrix.yaml
with:
atmos-version: ${{ vars.ATMOS_VERSION }}
atmos-config-path: ${{ vars.ATMOS_CONFIG_PATH }}
stacks: |
{"include": [
{"component": "${{ inputs.component }}", "stack": "${{ inputs.stack }}", "stack_slug": "${{ inputs.stack }}-${{ inputs.component }}"}
]}
secrets: inherit

Atmos Terraform Plan Matrix (Reusable)

The Atmos Terraform Plan Matrix is reusable workflow that called by another workflows to create a terraform plan.

.github/workflows/atmos-terraform-plan-matrix.yaml
name: 👽 Atmos Terraform Plan Matrix (Reusable)
run-name: 👽 Atmos Terraform Plan Matrix (Reusable)

on:
workflow_call:
inputs:
stacks:
description: "Stacks"
required: true
type: string
drift-detection-mode-enabled:
description: "Indicate whether this action is used in drift detection workflow."
type: string
required: false
default: 'false'
sha:
description: "SHA to use"
required: false
default: "${{ github.event.pull_request.head.sha }}"
type: string
atmos-version:
description: The version of atmos to install
required: false
default: ">= 1.63.0"
type: string
atmos-config-path:
description: The path to the atmos.yaml file
required: true
type: string
continue-on-error:
description: "Prevents a workflow run from failing when a job fails. Set to true to allow a workflow run to pass when this job fails."
required: false
default: "false"
type: string

permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout

jobs:
atmos-plan:
if: ${{ inputs.stacks != '{include:[]}' }}
name: ${{ matrix.stack_slug }}
runs-on: ["self-hosted", "terraform"]
continue-on-error: ${{ inputs.continue-on-error == 'true' }}
strategy:
max-parallel: 10
fail-fast: false # Don't fail fast to avoid locking TF State
matrix: ${{ fromJson(inputs.stacks) }}
## Avoid running the same stack in parallel mode (from different workflows)
concurrency:
group: ${{ matrix.stack_slug }}
cancel-in-progress: false
steps:
- uses: unfor19/install-aws-cli-action@v1

- name: Plan Atmos Component
uses: cloudposse/github-action-atmos-terraform-plan@v3
with:
component: ${{ matrix.component }}
stack: ${{ matrix.stack }}
drift-detection-mode-enabled: ${{ inputs.drift-detection-mode-enabled }}
infracost-api-key: ${{ secrets.INFRACOST_API_KEY }}
sha: ${{ inputs.sha }}
atmos-version: ${{ inputs.atmos-version }}
atmos-config-path: ${{ inputs.atmos-config-path }}

Atmos Terraform Apply Matrix (Reusable)

The Atmos Terraform Apply Matrix is reusable workflow called by another workflow to apply an existing plan file.

.github/workflows/atmos-terraform-apply-matrix.yaml
name: 👽 Atmos Terraform Apply Matrix (Reusable)
run-name: 👽 Atmos Terraform Apply Matrix (Reusable)

on:
workflow_call:
inputs:
stacks:
description: "Stacks"
required: true
type: string
sha:
description: "Commit SHA to apply. Default: github.sha"
type: string
required: false
default: "${{ github.event.pull_request.head.sha }}"
atmos-version:
description: The version of atmos to install
required: false
default: ">= 1.63.0"
type: string
atmos-config-path:
description: The path to the atmos.yaml file
required: true
type: string

permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout

jobs:
atmos-apply:
if: ${{ inputs.stacks != '{include:[]}' }}
name: ${{ matrix.stack_slug }}
runs-on: ["self-hosted", "terraform"]
strategy:
max-parallel: 10
fail-fast: false # Don't fail fast to avoid locking TF State
matrix: ${{ fromJson(inputs.stacks) }}
## Avoid running the same stack in parallel mode (from different workflows)
concurrency:
group: ${{ matrix.stack_slug }}
cancel-in-progress: false
steps:
- uses: unfor19/install-aws-cli-action@v1

- name: Apply Atmos Component
uses: cloudposse/github-action-atmos-terraform-apply@v2
with:
component: ${{ matrix.component }}
stack: ${{ matrix.stack }}
sha: ${{ inputs.sha }}
atmos-version: ${{ inputs.atmos-version }}
atmos-config-path: ${{ inputs.atmos-config-path }}