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
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.
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
- Use the
auto-apply
label on Pull Request to apply all plans on merge - Use the
no-apply
label on a Pull Request to skip all workflows on merge - 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.
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
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.
- If there are changes, the workflow will then create or update a GitHub Issue for the given component and stack.
- 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.
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
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.
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:
- uses: unfor19/install-aws-cli-action@v1
- 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":
- 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 - 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. - If both are selected, the workflow will run both actions. This means it will create a new planfile and then immediately apply it.
- If neither are selected, the workflow does nothing.
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.
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.
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 }}