Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding the ability to detect drift and send slack notifications #36

Merged
merged 14 commits into from
Jan 3, 2025
Merged
205 changes: 205 additions & 0 deletions .github/workflows/terraform-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
name: Terraform Drift Detection

on:
workflow_call:
secrets:
slack-webhook-url:
description: "The Slack webhook URL"
required: false

inputs:
aws-account-id:
description: "The AWS account ID to deploy to"
required: true
type: string

aws-region:
default: "eu-west-2"
description: "The AWS region to deploy to"
required: false
type: string

aws-role:
default: "${{ github.event.repository.name }}"
description: "The AWS role to assume"
required: false
type: string

aws-read-role-name:
description: "Overrides the default behavior, and uses a custom role name for read-only access"
required: false
type: string

aws-write-role-name:
description: "Overrides the default behavior, and uses a custom role name for read-write access"
required: false
type: string

environment:
default: "production"
description: "The environment to deploy to"
required: false
type: string

use-env-as-suffix:
default: false
description: "Whether to use the environment as a suffix for the state file and iam roles"
required: false
type: boolean

runs-on:
default: "ubuntu-latest"
description: "Single label value for the GitHub runner to use (custom value only applies to Terraform Plan and Apply steps)"
required: false
type: string

terraform-dir:
default: "."
description: "The directory to validate"
required: false
type: string

terraform-lock-timeout:
default: "30s"
description: The time to wait for a lock
required: false
type: string

terraform-state-key:
default: ""
description: "The key of the terraform state (default: <repo-name>.tfstate)"
required: false
type: string

terraform-values-file:
default: ""
description: "The values file to use (default: <environment>.tfvars)"
required: false
type: string

terraform-version:
default: "1.7.1"
description: "The version of terraform to use"
required: false
type: string

working-directory:
default: "."
description: "The working directory to run terraform commands in"
required: false
type: string

env:
AWS_ROLE: ${{ inputs.aws-role }}
AWS_READONLY_OVERRIDE_ROLE: ${{ inputs.aws-read-role-name }}
AWS_READWRITE_OVERRIDE_ROLE: ${{ inputs.aws-write-role-name }}
AWS_WEB_IDENTITY_TOKEN_FILE: /tmp/web_identity_token_file

permissions:
id-token: write
contents: read

jobs:
terraform-plan:
name: "Terraform Plan"
runs-on: ${{ inputs.runs-on }}
defaults:
run:
working-directory: ${{ inputs.working-directory }}
outputs:
result-auth: ${{ steps.auth.outcome }}
result-init: ${{ steps.init.outcome }}
result-validate: ${{ steps.validate.outcome }}
result-s3-backend-check: ${{ steps.s3-backend-check.outcome }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 16
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ inputs.terraform-version }}
- name: Retrieve Web Identity Token for AWS Authentication
run: |
curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r '.value' > $AWS_WEB_IDENTITY_TOKEN_FILE
- name: Determine AWS Role
id: role
run: |
if [ "${{ inputs.use-env-as-suffix }}" == "true" ]; then
role_suffix="-${{ inputs.environment }}"
else
role_suffix=""
fi
if [[ "${GITHUB_REF##*/}" == "main" ]]; then
echo "name=${AWS_READWRITE_OVERRIDE_ROLE:-${AWS_ROLE}${role_suffix}}" >> $GITHUB_OUTPUT
else
echo "name=${AWS_READONLY_OVERRIDE_ROLE:-${AWS_ROLE}${role_suffix}-ro}" >> $GITHUB_OUTPUT
fi
- name: Authenticate with AWS
id: auth
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ inputs.aws-region }}
role-session-name: ${{ github.event.repository.name }}
role-to-assume: arn:aws:iam::${{ inputs.aws-account-id }}:role/${{ steps.role.outputs.name }}
mask-aws-account-id: "no"
- name: Set terraform-state-key variable
id: state-key
run: |
if [ -n "${{ inputs.terraform-state-key }}" ]; then
echo "name=${{ inputs.terraform-state-key }}" >> $GITHUB_OUTPUT
else
if [ "${{ inputs.use-env-as-suffix }}" == "true" ]; then
echo "name=${{ github.event.repository.name }}-${{ inputs.environment }}.tfstate" >> $GITHUB_OUTPUT
else
echo "name=${{ github.event.repository.name }}.tfstate" >> $GITHUB_OUTPUT
fi
fi
- name: Terraform Init
id: init
run: terraform -chdir=${{ inputs.terraform-dir }} init -backend-config="bucket=${{ inputs.aws-account-id }}-${{ inputs.aws-region }}-tfstate" -backend-config="key=${{ steps.state-key.outputs.name }}" -backend-config="encrypt=true" -backend-config="dynamodb_table=${{ inputs.aws-account-id }}-${{ inputs.aws-region }}-tflock" -backend-config="region=${{ inputs.aws-region }}"
- name: Terraform Validate
id: validate
run: terraform -chdir=${{ inputs.terraform-dir }} validate -no-color
- name: Terraform S3 Backend Check
id: s3-backend-check
run: |
if grep -E '^[^#]*backend\s+"s3"' terraform.tf; then
echo "Terraform configuration references an S3 backend."
else
echo "Terraform configuration does not reference an S3 backend."
exit 1
fi
- name: Set terraform-values-file variable
run: |
if [ -n "${{ inputs.terraform-values-file }}" ]; then
echo "TF_VAR_FILE=${{ inputs.terraform-values-file }}" >> $GITHUB_ENV
else
echo "TF_VAR_FILE=values/${{ inputs.environment }}.tfvars" >> $GITHUB_ENV
fi
- name: Check for drift and set status
id: check-drift
run: |
if grep -q 'No changes' <(terraform -chdir=${{ inputs.terraform-dir }} plan -var-file=$TF_VAR_FILE -no-color -input=false -out=tfplan -lock-timeout=${{ inputs.terraform-lock-timeout }}); then
echo "No drift detected."
echo "DRIFT_STATUS=no-drift" >> "$GITHUB_OUTPUT"
else
echo "Drift detected!"
echo "DRIFT_STATUS=drift" >> "$GITHUB_OUTPUT"
fi
- name: Send Slack notification if drift is detected
if: steps.check-drift.outputs.DRIFT_STATUS == 'drift'
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.slack-webhook-url }}
webhook-type: incoming-webhook
payload: |
{
"username": "GitHub Actions",
"text": "🚨 Drift Detected (${{ github.repository }})",
"icon_emoji": ":warning:"
}
4 changes: 3 additions & 1 deletion .github/workflows/terraform-plan-and-apply-aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: tfplan-${{ inputs.environment }}
compression-level: 9
path: "tfplan*"
retention-days: 14
- name: Optional Additional Directory Upload
Expand All @@ -357,9 +358,10 @@ jobs:
if: inputs.additional-dir
uses: actions/upload-artifact@v4
with:
name: additional-dir-${{ inputs.environment }}
compression-level: 9
if-no-files-found: error
include-hidden-files: true
name: additional-dir-${{ inputs.environment }}
path: ${{ inputs.additional-dir }}
retention-days: 14

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
**/._.DS_Store

*.orig

.idea
61 changes: 61 additions & 0 deletions docs/terraform-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Terraform Draft Workflow for AWS Infrastructure

This workflow is used to run an scheduled or manually triggered drift detection on AWS infrastructure and alert in Slack if a change is detected, using GitHub Actions workflow template ([terraform-drift.yml](../.github/workflows/terraform-drift.yml))
In order to trigger the workflow, firstly the workflow must be referenced from the calling workflow flow, see below.

## Workflow Steps

1. **Setup Terraform:** Terraform is fetched at the specified version (overridable via inputs).
2. **AWS Authentication:** The workflow uses Web Identity Federation to authenticate with AWS. The required AWS Role ARN must be provided as an input for successful authentication.
- A Web Identity Token File is also generated and stored in `/tmp/web_identity_token_file`, which can be referenced in Terraform Provider configuration blocks if required.
3. **Terraform Init:** The Terraform backend is initialised and any necessary provider plugins are downloaded. The required inputs for AWS S3 bucket name and DynamoDB table name must be provided for storing the Terraform state.
4. **Terraform Plan:** A Terraform plan is generated with a specified values file (overridable via inputs) using the terraform plan command.
5. **Change Detection Status:** Plan is checked for any changes and status is set as to drift status, "No drift detected." or "Drift detected!"
6. **Alerting:** If drift is detected from the Terraform Plan, an alert is sent to a configured Slack Channel alerting users. Otherwise where there is no change, this step is skipped.

## Usage

Create a workflow file in your Terraform repository (e.g. `.github/workflows/terraform-drift.yml`) with the below contents:

```yml
---
name: Terraform Drift
on:
workflow_dispatch:
schedule:
- cron: "56 10 * * *"

permissions:
contents: read
id-token: write

jobs:
terraform-drift:
uses: appvia/appvia-cicd-workflows/.github/workflows/terraform-drift.yml@main
name: Drift Detection
secrets:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
with:
aws-account-id: <aws-account-id-value>
```

REQUIRED INPUTS:
- `slack-webhook-url` - Slack Webhook to a channel/app, stored as a secret in your Github Actions Secrets
- `aws-account-id` - AWS account number where the infrastructure is deployed, and consequently planned against

OPTIONAL INPUTS:
- `aws-region` - Default: "eu-west-2"
- `aws-role` - Default: Repository Name
- `aws-read-role-name` - Extra role for read only access
- `aws-write-role-name` - Extra role for read-write access
- `environment` - Default: "production"
- `use-env-as-suffix` - Default: false, Whether to use the environment as a suffix for the state file and iam roles
- `runs-on` - Default: "ubuntu-latest"
- `terraform-dir` - Default: ".",
- `terraform-lock-timeout` - Default: "30s"
- `terraform-state-key` - Default: <repo-name>.tfstate
- `terraform-values-file` - Default: <environment>.tfvars
- `terraform-version` - Default: "1.7.1"
- `working-directory` - Default: "."

**Note:** This template may change over time, so it is recommended that you point to a tagged version rather than the main branch.
Loading