├── .github └── workflows │ └── ci.yaml ├── README.md ├── profile └── example │ ├── attributes-example.yml │ ├── controls │ └── s3.rb │ └── inspec.yml └── terraform ├── modules └── acme_bucket │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── providers.tf ├── s3_buckets.tf └── variables.tf /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "GitHub Actions Example" 3 | 4 | "on": 5 | pull_request: 6 | push: { branches: [main] } 7 | 8 | jobs: 9 | terraform: 10 | runs-on: ubuntu-latest 11 | env: 12 | working-directory: terraform 13 | TF_WORKSPACE: my-workspace 14 | steps: 15 | - name: Configure AWS credentials 16 | uses: aws-actions/configure-aws-credentials@v1 17 | with: 18 | aws-region: eu-west-1 19 | 20 | - name: Check out code 21 | uses: actions/checkout@v2 22 | 23 | - name: Run Terrascan 24 | id: terrascan 25 | uses: accurics/terrascan-action@main 26 | with: 27 | iac_type: 'terraform' 28 | iac_version: 'v14' 29 | policy_type: 'aws' 30 | only_warn: true 31 | iac_dir: infrastructure 32 | #sarif_upload: true 33 | #non_recursive: 34 | #iac_dir: 35 | #policy_path: 36 | #skip_rules: 37 | #config_path: 38 | 39 | - uses: hashicorp/setup-terraform@v1 40 | with: 41 | terraform_version: 0.14.4 42 | 43 | - name: Terraform Fmt 44 | id: fmt 45 | run: terraform fmt -check 46 | continue-on-error: true 47 | 48 | - name: Terraform init 49 | id: init 50 | run: terraform init 51 | working-directory: ${{ env.working-directory }} 52 | env: 53 | TF_CLI_ARGS_init: "-backend-config=role_arn=arn:aws:iam::99999999:role/my-github-actions-role -upgrade -reconfigure" 54 | TF_VAR_assume_role: "my-github-actions-role" 55 | 56 | - name: Terraform validate 57 | id: validate 58 | run: terraform validate 59 | 60 | - name: Terraform plan 61 | id: plan 62 | run: terraform plan -no-color 63 | working-directory: ${{ env.working-directory }} 64 | env: 65 | TF_VAR_assume_role: "my-github-actions-role" 66 | 67 | - name: Plan output 68 | id: output 69 | uses: actions/github-script@v3 70 | if: github.event_name == 'pull_request' 71 | env: 72 | PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" 73 | with: 74 | github-token: ${{ secrets.GITHUB_TOKEN }} 75 | script: | 76 | const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` 77 | ### Workspace 78 | 79 | \`${process.env.TF_WORKSPACE}\` 80 | 81 | #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` 82 | #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` 83 |
Show Plan 84 | 85 | \`\`\`hcl 86 | ${process.env.PLAN} 87 | \`\`\` 88 | 89 |
90 | 91 | **Pusher**: @${{ github.actor }} 92 | **Action**: ${{ github.event_name }} 93 | `; 94 | github.issues.createComment({ 95 | issue_number: context.issue.number, 96 | owner: context.repo.owner, 97 | repo: context.repo.repo, 98 | body: output 99 | }) 100 | 101 | - name: Terraform apply 102 | id: apply 103 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 104 | run: terraform apply -auto-approve -input=false 105 | working-directory: ${{ env.working-directory }} 106 | env: 107 | TF_VAR_assume_role: "my-github-actions-role" 108 | 109 | - name: Install InSpec 110 | uses: actionshub/chef-install@main 111 | with: 112 | channel: current 113 | project: inspec 114 | 115 | - name: Assume Role 116 | uses: aws-actions/configure-aws-credentials@v1 117 | with: 118 | aws-region: eu-west-1 119 | role-to-assume: arn:aws:iam::88888888888888:role/my-github-actions-role 120 | role-duration-seconds: 600 121 | role-skip-session-tagging: true 122 | 123 | - name: Infrastructure Tests 124 | id: inspec 125 | run: inspec exec profile/example --reporter=cli:- progress:inspec.log --input-file=profile/example/attributes-example.yml -t aws:// 126 | env: 127 | CHEF_LICENSE: accept 128 | 129 | - name: Infrastructure Test Results 130 | id: inspec_results 131 | if: always() 132 | run: | 133 | REPORT="$(cat inspec.log)" 134 | REPORT="${REPORT//'%'/'%25'}" 135 | REPORT="${REPORT//$'\n'/'%0A'}" 136 | REPORT="${REPORT//$'\r'/'%0D'}" 137 | echo "::set-output name=loginspec::$REPORT" 138 | 139 | - name: Infrastructure tests Output 140 | if: always() 141 | uses: actions/github-script@v3 142 | env: 143 | INSPEC: "Inspec Test Results \n${{ steps.inspec_results.outputs.loginspec }}" 144 | with: 145 | github-token: ${{secrets.GITHUB_TOKEN}} 146 | script: | 147 | const output = `#### Inspec Tests 🖌\`${{ steps.inspec.outcome }}\` 148 | 149 |
Show Test Results 150 | 151 | \`\`\` 152 | 153 | ${process.env.INSPEC} 154 | \`\`\` 155 | 156 |
157 | 158 | `; 159 | github.issues.createComment({ 160 | issue_number: context.issue.number, 161 | owner: context.repo.owner, 162 | repo: context.repo.repo, 163 | body: output 164 | }) 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-github-actions 2 | terraform aws github action workflow example with tests 3 | 4 | Contains an example workflow for terraform and github actions incorporating: 5 | - terrascan for security scanning before running terraform 6 | - inspec for testing after running terraform 7 | 8 | This provides a simple terraform workflow that includes tests. 9 | Example uses terraform workspaces and AWS assume roles. 10 | 11 | The approach is on a pull request the workflow is run without the terraform apply and when it is merged with main branch the terraform apply is also run. 12 | 13 | NOTE: It is recommended to run this on your hosted github runners rather than github's runners so you keep your AWS credentials out of github. 14 | 15 | 16 | ## Repository Contents 17 | 18 | ### profile directory 19 | 20 | Contains an example inspec test against AWS. you need to be authenticated against a valid AWS account to run the tests 21 | 22 | ### terraform directory 23 | 24 | Contains a test project with known bad terraform to run tfsec against. 25 | 26 | Uses S3 buckets for testing example 27 | 28 | This project has a number of issues that are known to fail tfsec checks. 29 | 30 | ## Usage 31 | 32 | - git clone repo 33 | - create a new branch 34 | - push changes to github 35 | - Create Pull Request and see github action CI workflow run 36 | 37 | 38 | ## References 39 | 40 | - [Continuous Integration with GitHub Actions and Terraform](https://wahlnetwork.com/2020/05/12/continuous-integration-with-github-actions-and-terraform/) 41 | - [WahlNetwork/github-action-terraform](https://github.com/WahlNetwork/github-action-terraform) 42 | - [Automate Terraform with GitHub Actions](https://learn.hashicorp.com/tutorials/terraform/github-actions) 43 | - [Automated Terraform Deployments to AWS with Github Actions](https://medium.com/@dnorth98/automated-terraform-deployments-to-aws-with-github-actions-c590c065c179) 44 | 45 | - [Terrascan Documentation](https://docs.accurics.com/projects/accurics-terrascan/en/latest/) 46 | - [terrascan-action](https://github.com/accurics/terrascan-action) 47 | - [Terrascan GitHub Action: Easy Policy as Code for IaC Pipelines](https://www.accurics.com/blog/devops-blog/terrascan-github-action-policy-as-code-for-iac-pipelines/) 48 | 49 | - [inspec-aws](https://github.com/inspec/inspec-aws) 50 | - [InSpec Resources Reference](https://docs.chef.io/inspec/resources/) 51 | -------------------------------------------------------------------------------- /profile/example/attributes-example.yml: -------------------------------------------------------------------------------- 1 | bucket_name: bucket-with-encryption-and-logging -------------------------------------------------------------------------------- /profile/example/controls/s3.rb: -------------------------------------------------------------------------------- 1 | control 's3 buckets' do 2 | title 's3 buckets tests' 3 | impact 1.0 4 | 5 | bucket_name = attribute('bucket_name') 6 | 7 | describe aws_s3_bucket(bucket_name: bucket_name.to_s) do 8 | it { should exist } 9 | it { should_not be_public } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /profile/example/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Example 3 | title: Example 4 | maintainer: Analytical Platform 5 | copyright: None 6 | copyright_email: None 7 | license: Apache 2 8 | summary: An InSpec Compliance Profile Example 9 | version: 0.1.0 10 | inspec_version: '> 4.18.97' 11 | supports: 12 | - platform: aws 13 | depends: 14 | - name: inspec-aws 15 | url: https://github.com/inspec/inspec-aws/archive/v1.33.0.zip -------------------------------------------------------------------------------- /terraform/modules/acme_bucket/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_s3_bucket" "logging_bucket" { 2 | bucket = var.s3_logging_bucket 3 | } 4 | 5 | resource "aws_s3_bucket" "acme_bucket" { 6 | bucket = var.bucket_name 7 | 8 | logging { 9 | target_prefix = format("%s/logs/", var.bucket_name) 10 | target_bucket = data.aws_s3_bucket.logging_bucket.id 11 | } 12 | 13 | server_side_encryption_configuration { 14 | rule { 15 | apply_server_side_encryption_by_default { 16 | sse_algorithm = "AES256" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /terraform/modules/acme_bucket/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = aws_s3_bucket.acme_bucket.id 3 | } -------------------------------------------------------------------------------- /terraform/modules/acme_bucket/variables.tf: -------------------------------------------------------------------------------- 1 | variable bucket_name { 2 | description = "Name of the bucket that is going to be created" 3 | } 4 | 5 | variable "s3_logging_bucket" { 6 | description = "The name of the acme corp logging bucket" 7 | default = "acme-s3-logging-bucket" 8 | } 9 | 10 | variable "cost_centre" { 11 | description = "The cost centre code for the bucket" 12 | } -------------------------------------------------------------------------------- /terraform/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "eu-west-1" 3 | assume_role { 4 | role_arn = "arn:aws:iam::${var.account_id[terraform.workspace]}:role/${var.assume_role}" 5 | session_name = "example" 6 | } 7 | } -------------------------------------------------------------------------------- /terraform/s3_buckets.tf: -------------------------------------------------------------------------------- 1 | 2 | data "aws_s3_bucket" "acme-s3-access-logging" { 3 | bucket = var.acme_s3_logging_bucket 4 | } 5 | 6 | module "acme_finance_bucket" { 7 | source = "./modules/acme_bucket" 8 | bucket_name = "finance-reports" 9 | cost_centre = "CC001" 10 | 11 | s3_logging_bucket = var.acme_s3_logging_bucket 12 | } 13 | 14 | resource "aws_s3_bucket" "bucket-with-encryption-and-logging" { 15 | bucket = "my-passing-bucket" 16 | 17 | logging { 18 | target_bucket = data.aws_s3_bucket.acme-s3-access-logging.id 19 | target_prefix = "my-passing-bucket/logs/" 20 | } 21 | 22 | server_side_encryption_configuration { 23 | rule { 24 | apply_server_side_encryption_by_default { 25 | sse_algorithm = "AES256" 26 | } 27 | } 28 | } 29 | } 30 | 31 | resource "aws_s3_bucket" "bucket-with-encryption" { 32 | bucket = "my-failing-bucket-no-logging" 33 | 34 | server_side_encryption_configuration { 35 | rule { 36 | apply_server_side_encryption_by_default { 37 | sse_algorithm = "AES256" 38 | } 39 | } 40 | } 41 | } 42 | 43 | resource "aws_s3_bucket" "bucket-with-logging" { 44 | bucket = "my-failing-bucket-no-encryption" 45 | 46 | logging { 47 | target_prefix = "my-failing-bucket-not-encryption/logs/" 48 | target_bucket = data.aws_s3_bucket.acme-s3-access-logging.id 49 | } 50 | } 51 | 52 | resource "aws_s3_bucket" "bucket-with-encryption-and-logging-but-public" { 53 | bucket = "my-public-bucket" 54 | acl = "public-read" 55 | 56 | logging { 57 | target_bucket = data.aws_s3_bucket.acme-s3-access-logging.id 58 | target_prefix = "my-passing-bucket/logs/" 59 | } 60 | 61 | server_side_encryption_configuration { 62 | rule { 63 | apply_server_side_encryption_by_default { 64 | sse_algorithm = "AES256" 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "acme_s3_logging_bucket" { 2 | description = "The s3 logging bucket" 3 | default = "acme-s3-access-bucket" 4 | } --------------------------------------------------------------------------------