diff --git a/.github/workflows/terraform-drift.yml b/.github/workflows/terraform-drift.yml new file mode 100644 index 0000000..bd6bd4d --- /dev/null +++ b/.github/workflows/terraform-drift.yml @@ -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: .tfstate)" + required: false + type: string + + terraform-values-file: + default: "" + description: "The values file to use (default: .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/slack-github-action@v2.0.0 + with: + webhook: ${{ secrets.slack-webhook-url }} + webhook-type: incoming-webhook + payload: | + { + "username": "GitHub Actions", + "text": "🚨 Drift Detected (${{ github.repository }})", + "icon_emoji": ":warning:" + } diff --git a/.github/workflows/terraform-plan-and-apply-aws.yml b/.github/workflows/terraform-plan-and-apply-aws.yml index c52106a..93dab7f 100644 --- a/.github/workflows/terraform-plan-and-apply-aws.yml +++ b/.github/workflows/terraform-plan-and-apply-aws.yml @@ -432,6 +432,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 @@ -443,9 +444,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 diff --git a/.gitignore b/.gitignore index dca4c80..c79dfca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ .idea *.orig + +.idea \ No newline at end of file diff --git a/docs/terraform-drift.md b/docs/terraform-drift.md new file mode 100644 index 0000000..40c820f --- /dev/null +++ b/docs/terraform-drift.md @@ -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: +``` + +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: .tfstate +- `terraform-values-file` - Default: .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.