Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 62 additions & 3 deletions .github/workflows/terraform-apply.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: 'Terraform Apply'

# Applies a saved plan produced by terraform-plan. Downloads the plan artifact,
# re-inits, and applies it. Pair with a GitHub `environment` for required
# reviewers on production applies.
# Applies a saved plan produced by terraform-plan. Retrieves the plan from
# GitHub artifacts or an S3-compatible object store, re-inits, and applies it.
# Pair with a GitHub `environment` for required reviewers on production applies.

on:
workflow_call:
Expand All @@ -15,6 +15,21 @@ on:
description: 'Plan artifact name produced by terraform-plan'
required: true
type: string
plan-storage-endpoint:
description: 'S3-compatible endpoint for the plan archive; empty uses GitHub artifacts'
required: false
type: string
default: ''
plan-storage-bucket:
description: 'Bucket containing the plan archive'
required: false
type: string
default: ''
plan-storage-key:
description: 'Attempt-scoped object key containing the plan archive'
required: false
type: string
default: ''
terraform-version:
description: 'Terraform version (must match the version that produced the plan)'
required: false
Expand Down Expand Up @@ -52,6 +67,12 @@ on:
cloudflare-api-token:
description: 'Scoped Cloudflare API token (optional; required only when Terraform uses Cloudflare)'
required: false
plan-storage-access-key-id:
description: 'Access key for the S3-compatible plan store'
required: false
plan-storage-secret-access-key:
description: 'Secret key for the S3-compatible plan store'
required: false
outputs:
applied:
description: 'true when the apply completed successfully'
Expand Down Expand Up @@ -93,11 +114,34 @@ jobs:
terraform_wrapper: false

- name: Download plan artifact
if: inputs.plan-storage-endpoint == ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.plan-artifact-name }}
path: ${{ inputs.terraform-path }}

- name: Download plan from object storage
if: inputs.plan-storage-endpoint != ''
env:
AWS_ACCESS_KEY_ID: ${{ secrets.plan-storage-access-key-id }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.plan-storage-secret-access-key }}
PLAN_ENDPOINT: ${{ inputs.plan-storage-endpoint }}
PLAN_BUCKET: ${{ inputs.plan-storage-bucket }}
PLAN_KEY: ${{ inputs.plan-storage-key }}
run: |
set -euo pipefail
: "${AWS_ACCESS_KEY_ID:?missing plan storage access key}"
: "${AWS_SECRET_ACCESS_KEY:?missing plan storage secret key}"
: "${PLAN_BUCKET:?missing plan storage bucket}"
: "${PLAN_KEY:?missing plan storage key}"
archive="$RUNNER_TEMP/terraform-plan.tar.gz"
env -u AWS_SESSION_TOKEN aws --endpoint-url "$PLAN_ENDPOINT" --region garage \
s3 cp "s3://$PLAN_BUCKET/$PLAN_KEY" "$archive" --no-progress
env -u AWS_SESSION_TOKEN aws --endpoint-url "$PLAN_ENDPOINT" --region garage \
s3 cp "s3://$PLAN_BUCKET/$PLAN_KEY.sha256" "$archive.sha256" --no-progress
(cd "$RUNNER_TEMP" && sha256sum --check terraform-plan.tar.gz.sha256)
tar -xzf "$archive"

- name: Terraform init
env:
BACKEND_CONFIG: ${{ inputs.backend-config }}
Expand All @@ -119,6 +163,21 @@ jobs:
terraform apply -input=false -no-color tfplan
echo "applied=true" >> "$GITHUB_OUTPUT"

- name: Delete plan from object storage
if: always() && inputs.plan-storage-endpoint != ''
env:
AWS_ACCESS_KEY_ID: ${{ secrets.plan-storage-access-key-id }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.plan-storage-secret-access-key }}
PLAN_ENDPOINT: ${{ inputs.plan-storage-endpoint }}
PLAN_BUCKET: ${{ inputs.plan-storage-bucket }}
PLAN_KEY: ${{ inputs.plan-storage-key }}
run: |
set -euo pipefail
env -u AWS_SESSION_TOKEN aws --endpoint-url "$PLAN_ENDPOINT" --region garage \
s3 rm "s3://$PLAN_BUCKET/$PLAN_KEY" --no-progress
env -u AWS_SESSION_TOKEN aws --endpoint-url "$PLAN_ENDPOINT" --region garage \
s3 rm "s3://$PLAN_BUCKET/$PLAN_KEY.sha256" --no-progress

- name: Summary
if: always()
run: |
Expand Down
50 changes: 47 additions & 3 deletions .github/workflows/terraform-plan.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: 'Terraform Plan'

# Runs `terraform plan` against real cloud state via GitHub OIDC, uploads the
# binary plan as an artifact (consumed by terraform-apply), and optionally posts
# the plan to the PR and estimates cost with Infracost.
# Runs `terraform plan` against real cloud state, saves the binary plan for
# terraform-apply, and optionally posts the plan to the PR and estimates cost.
# Plans use GitHub artifacts unless an S3-compatible object store is configured.

on:
workflow_call:
Expand Down Expand Up @@ -41,6 +41,21 @@ on:
required: false
type: string
default: 'tfplan'
plan-storage-endpoint:
description: 'S3-compatible endpoint for the plan archive; empty uses GitHub artifacts'
required: false
type: string
default: ''
plan-storage-bucket:
description: 'Bucket for the plan archive when plan-storage-endpoint is set'
required: false
type: string
default: ''
plan-storage-key:
description: 'Attempt-scoped object key for the plan archive when plan-storage-endpoint is set'
required: false
type: string
default: ''
post-pr-comment:
description: 'Post the plan to the PR as a comment (PR events only)'
required: false
Expand Down Expand Up @@ -71,6 +86,12 @@ on:
cloudflare-api-token:
description: 'Scoped Cloudflare API token (optional; required only when Terraform uses Cloudflare)'
required: false
plan-storage-access-key-id:
description: 'Access key for the S3-compatible plan store'
required: false
plan-storage-secret-access-key:
description: 'Secret key for the S3-compatible plan store'
required: false
outputs:
has-changes:
description: 'true when the plan contains changes'
Expand Down Expand Up @@ -175,6 +196,7 @@ jobs:
PY

- name: Upload plan artifact
if: inputs.plan-storage-endpoint == ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.plan-artifact-name }}
Expand All @@ -185,6 +207,28 @@ jobs:
retention-days: 5
if-no-files-found: error

- name: Upload plan to object storage
if: inputs.plan-storage-endpoint != ''
env:
AWS_ACCESS_KEY_ID: ${{ secrets.plan-storage-access-key-id }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.plan-storage-secret-access-key }}
PLAN_ENDPOINT: ${{ inputs.plan-storage-endpoint }}
PLAN_BUCKET: ${{ inputs.plan-storage-bucket }}
PLAN_KEY: ${{ inputs.plan-storage-key }}
run: |
set -euo pipefail
: "${AWS_ACCESS_KEY_ID:?missing plan storage access key}"
: "${AWS_SECRET_ACCESS_KEY:?missing plan storage secret key}"
: "${PLAN_BUCKET:?missing plan storage bucket}"
: "${PLAN_KEY:?missing plan storage key}"
archive="$RUNNER_TEMP/terraform-plan.tar.gz"
tar -czf "$archive" tfplan plan.txt plan.json
(cd "$RUNNER_TEMP" && sha256sum terraform-plan.tar.gz >terraform-plan.tar.gz.sha256)
env -u AWS_SESSION_TOKEN aws --endpoint-url "$PLAN_ENDPOINT" --region garage \
s3 cp "$archive" "s3://$PLAN_BUCKET/$PLAN_KEY" --no-progress
env -u AWS_SESSION_TOKEN aws --endpoint-url "$PLAN_ENDPOINT" --region garage \
s3 cp "$archive.sha256" "s3://$PLAN_BUCKET/$PLAN_KEY.sha256" --no-progress

- name: Set up Infracost
if: inputs.cost-estimation
uses: infracost/actions/setup@fb736a8f219195d6efdca58682069b16fe1bc280 # v4.2.0
Expand Down
Loading