├── .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 | }
--------------------------------------------------------------------------------