Skip to content

Commit

Permalink
Merge pull request #9 from appvia/monorepo
Browse files Browse the repository at this point in the history
Adding plan only workflow
  • Loading branch information
lenasappvia authored Oct 20, 2023
2 parents 5b77ac6 + 9e8e1c3 commit 50fad18
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 0 deletions.
289 changes: 289 additions & 0 deletions .github/workflows/terraform-plan-aws.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
## Plan only workflow for testing purposes. Allows working directory
# distinction to test with different modules.

name: Terraform Validate and Plan
on:
workflow_call:
secrets:
infracost-api-key:
description: 'The API key for infracost'
required: false
inputs:
aws-account-id:
description: 'The AWS account ID to deploy to'
required: true
type: number
aws-role-name-readonly:
default: '${{ github.event.repository.name }}-ro'
description: 'The Read Only AWS role to assume for PR branch executions'
required: false
type: string
aws-role-name-readwrite:
default: '${{ github.event.repository.name }}-rw'
description: 'The Read/Write AWS role to assume for main branch executions'
required: false
type: string
aws-region:
default: 'eu-west-2'
description: 'The AWS region to deploy to'
required: false
type: string
enable-infracost:
default: false
description: 'Whether to run infracost on the Terraform Plan (secrets.infracost-api-key must be set if enabled)'
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-log-level:
default: ''
description: 'The log level of terraform'
required: false
type: string
terraform-state-key:
default: '${{ github.event.repository.name }}.tfstate'
description: 'The key of the terraform state'
required: false
type: string
terraform-values-file:
default: 'values/production.tfvars'
description: 'The values file to use'
required: false
type: string
terraform-version:
default: '1.5.7'
description: 'The version of terraform to use'
required: false
type: string
working-directory:
default: '.'
description: 'Working directory'
required: false
type: string


env:
TF_LOG: ${{ inputs.terraform-log-level }}
AWS_WEB_IDENTITY_TOKEN_FILE: /tmp/web_identity_token_file

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

jobs:
terraform-format:
name: "Terraform Format"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
outputs:
result: ${{ steps.format.outcome }}
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ inputs.terraform-version }}
- name: Terraform Format
id: format
uses: dflook/terraform-fmt-check@v1
terraform-lint:
name: "Terraform Lint"
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
outputs:
result: ${{ steps.lint.outcome }}
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Setup Linter
uses: terraform-linters/setup-tflint@v3
- name: Linter Initialize
run: tflint --init
- name: Linting Code
id: lint
run: tflint -f compact
terraform-plan:
name: "Terraform Plan"
if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
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 }}
result-plan: ${{ steps.plan.outcome }}
plan-stdout: ${{ steps.plan.outputs.stdout }}
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
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 [[ "${GITHUB_REF##*/}" == "main" ]]; then
echo "name=${{ inputs.aws-role-name-readwrite }}" >> $GITHUB_OUTPUT
else
echo "name=${{ inputs.aws-role-name-readonly }}" >> $GITHUB_OUTPUT
fi
- name: Authenticate with AWS
id: auth
uses: aws-actions/configure-aws-credentials@v2
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: Terraform Init
id: init
run: terraform init -backend-config="bucket=${{ inputs.aws-account-id }}-${{ inputs.aws-region }}-tfstate" -backend-config="key=${{ inputs.terraform-state-key }}" -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 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: Terraform Plan
id: plan
run: |
terraform plan -var-file=${{ inputs.terraform-values-file }} -no-color -input=false -out=tfplan
- name: Terraform Plan JSON Output
run: |
terraform show -json tfplan > tfplan.json
- name: Upload tfplan
uses: actions/upload-artifact@v3
with:
name: tfplan
path: "tfplan*"
retention-days: 1
get-cost-estimate:
name: "Get Cost Estimate"
if: github.event_name == 'pull_request' && inputs.enable-infracost
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
needs:
- terraform-plan
steps:
- name: Setup Infracost
uses: infracost/actions/setup@v2
with:
api-key: ${{ secrets.infracost-api-key }}
currency: GBP
- name: Download tfplan
uses: actions/download-artifact@v3
with:
name: tfplan
- name: Generate Infracost Cost Estimate
run: |
infracost breakdown --path=tfplan.json \
--format=json \
--out-file=/tmp/infracost.json
- name: Post Infracost comment
run: |
infracost comment github --path=/tmp/infracost.json \
--repo=$GITHUB_REPOSITORY \
--github-token=${{github.token}} \
--pull-request=${{github.event.pull_request.number}} \
--behavior=update
update-pr:
name: "Update PR"
if: github.event_name == 'pull_request' && (success() || failure())
runs-on: ubuntu-latest
needs:
- terraform-format
- terraform-lint
- terraform-plan
steps:
- name: Add PR Comment
uses: actions/github-script@v6
env:
PLAN: "${{ needs.terraform-plan.outputs.plan-stdout }}"
with:
script: |
// 1. Retrieve existing bot comments for the PR
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
})
const botComment = comments.find(comment => {
return comment.user.type === 'Bot' && comment.body.includes('Pull Request Review Status')
})
// 2. Check output length
const PLAN = process.env.PLAN || '';
const excludedStrings = ["Refreshing state...", "Reading...", "Read complete after"];
const filteredLines = PLAN.split('\n').filter(line =>
!excludedStrings.some(excludedStr => line.includes(excludedStr))
);
var planOutput = filteredLines.join('\n').trim();
if (planOutput.length < 1 || planOutput.length > 65000) {
planOutput = "Terraform Plan output is too large, please view the workflow run logs directly."
}
// 3. Prepare format of the comment
const output = `### Pull Request Review Status
* 🖌 <b>Terraform Format and Style:</b> \`${{ needs.terraform-format.outputs.result }}\`
* 🔍 <b>Terraform Linting:</b> \`${{ needs.terraform-lint.outputs.result }}\`
* 🔑 <b>AWS Authentication:</b> \`${{ needs.terraform-plan.outputs.result-auth }}\`
* 🔧 <b>Terraform Initialisation:</b> \`${{ needs.terraform-plan.outputs.result-init }}\`
* 🤖 <b>Terraform Validation:</b> \`${{ needs.terraform-plan.outputs.result-validate }}\`
* 📁 <b>Terraform S3 Backend:</b> \`${{ needs.terraform-plan.outputs.result-s3-backend-check }}\`
* 📖 <b>Terraform Plan:</b> \`${{ needs.terraform-plan.outputs.result-plan }}\`
<details><summary><b>Output: 📖 Terraform Plan</b></summary>
\`\`\`
${planOutput}
\`\`\`
</details>
*<b>Pusher:</b> @${{ github.actor }}, <b>Action:</b> \`${{ github.event_name }}\`*
*<b>Workflow Run Link:</b> ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}*`;
// 4. If we have a comment, update it, otherwise create a new one
if (botComment) {
github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: output
})
} else {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
._.DS_Store
**/.DS_Store
**/._.DS_Store
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This repository contains a collection of GitHub Actions workflow templates that
## Workflows

- [Terraform Plan & Apply (AWS)](./docs/terraform-plan-and-apply-aws.md)
- [Terraform Plan Only (AWS)](./docs/terraform-plan.md)
- [Terraform Module Validation](./docs/terraform-module-validation.md)
- [Terraform Module Release](./docs/terraform-module-release.md)

Expand Down
59 changes: 59 additions & 0 deletions docs/terraform-plan-aws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Terraform Workflow for AWS Infrastructure

This GitHub Actions workflow template ([terraform-plan-aws.yml](../.github/workflows/terraform-plan-aws.yml)) can be used with Terraform repositories to automate the testing and planning of AWS infrastructure. The workflow performs various steps such as authentication with AWS, Terraform formatting, initialization, validatio and planning. It also adds the Terraform plan output as a comment to the associated pull request and triggers an apply action for pushes to the main branch.

## Workflow Steps

1. **Setup Terraform:** Terraform is fetched at the specified version (overridable via inputs).
2. **Terraform Format:** This step runs the terraform fmt command to check that all Terraform files are formatted correctly.
3. **Terraform Lint:** This step runs terraform lint to check for deprecated syntax, unused declarations, invalid types, and enforcing best practices.
4. **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.
5. **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.
6. **Terraform Validate:** The workflow validates the Terraform configuration files using the terraform validate command to check for syntax errors and other issues.
7. **Terraform Plan:** A Terraform plan is generated with a specified values file (overridable via inputs) using the terraform plan command.
8. **Get Cost Estimate:** The infracost utility is run to get a cost estimate on the Terraform Plan output. A comment will be added to the pull request with the cost estimate.
9. **Add PR Comment:** If the workflow is triggered via a Pull Request, a comment will be added to the ticket containing the results of the previous steps.

## Inputs

| Input | Required? | Default Value | Description |
|-------|-------------|-----------|---------------|
| aws-role-arn | Yes | | The ARN of the AWS role to assume for authentication |
| aws-s3-bucket-name | Yes | | The name of the AWS S3 bucket to store the Terraform state |
| aws-dynamodb-table-name | Yes | | The name of the AWS DynamoDB table to use for locking |
| aws-region | No | eu-west-2 | The AWS region to deploy the infrastructure to |
| terraform-log-level | No | INFO | The log level of Terraform |
| terraform-state-key | No | ${{ github.event.repository.name }}.tfstate | The name of the Terraform state file to store in S3 |
| terraform-values-file | No | values/production.tfvars | The path to the values file to use |
| terraform-version | No | 1.5.2 | The version of Terraform to use |
| working-directory | No | . | Which working directory to navigate to |

## Usage

Create a new workflow file in your Terraform repository (e.g. `.github/workflows/terraform.yml`) with the below contents:
```yml
name: Terraform Plan Generic Modules
on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
terraform:
uses: appvia/appvia-cicd-workflows/.github/workflows/terraform-plan-aws.yml@main
name: Terraform Run
secrets:
infracost-api-key: ${{ secrets.ORG_INFRACOST_API_KEY }}
with:
aws-account-id: 536471746696
enable-infracost: true
terraform-version: 1.5.7
```
The `aws-role-name` inputs are optional and will default to the repository name (with the respective `-ro` or `-rw` suffixes) if not provided.

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

0 comments on commit 50fad18

Please sign in to comment.