├── .gitignore ├── 0_remote_state ├── .terraform-docs.yml ├── README.md ├── data.tf ├── dynamodb.tf ├── img │ └── Remote-state.png ├── main.tf ├── outputs.tf ├── provider.tf ├── s3.tf └── usage.md ├── 1_pipeline ├── .terraform-docs.yml ├── README.md ├── artifacts_s3.tf ├── buildspecs │ ├── buildspec.yml │ ├── checkov.yml │ ├── infracost.yml │ ├── opa.yml │ ├── terrascan.yml │ ├── terratest.yml │ └── tflint.yml ├── codebuild.tf ├── codepipeline.tf ├── data.tf ├── img │ └── CICD-pipeline-architecture.png ├── main.tf ├── provider.tf ├── ssm_parameters.tf ├── usage.md └── variables.tf └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .terraform/ 3 | .terraform.lock.hcl 4 | *.tfstate* -------------------------------------------------------------------------------- /0_remote_state/.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | formatter: "markdown" 2 | 3 | version: "" 4 | 5 | header-from: main.tf 6 | footer-from: "" 7 | 8 | recursive: 9 | enabled: false 10 | path: modules 11 | 12 | sections: 13 | hide: [] 14 | show: [] 15 | 16 | content: |- 17 | {{ include "./usage.md" }} 18 | {{ .Providers }} 19 | {{ .Resources }} 20 | {{ .Outputs }} 21 | 22 | output: 23 | file: README.md 24 | mode: inject 25 | template: |- 26 | 27 | 28 | {{ .Content }} 29 | 30 | 31 | 32 | output-values: 33 | enabled: false 34 | from: "" 35 | 36 | sort: 37 | enabled: true 38 | by: name 39 | 40 | settings: 41 | anchor: true 42 | color: true 43 | default: true 44 | description: false 45 | escape: true 46 | hide-empty: false 47 | html: true 48 | indent: 2 49 | lockfile: true 50 | read-comments: true 51 | required: true 52 | sensitive: true 53 | type: true 54 | -------------------------------------------------------------------------------- /0_remote_state/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Terraform remote state 4 | 5 | This module deploys AWS infrastructure to store Terraform remote state in S3 bucket and lock Terraform execution in DynamoDB table. 6 | 7 | ![Terraform remote state](img/Remote-state.png) 8 | 9 | ## Deployment 10 | 11 | ```sh 12 | terraform init 13 | terraform plan 14 | terraform apply -auto-approve 15 | ``` 16 | 17 | ## Tier down 18 | 19 | ```sh 20 | terraform destroy -auto-approve 21 | ``` 22 | ## Providers 23 | 24 | | Name | Version | 25 | |------|---------| 26 | | [aws](#provider\_aws) | n/a | 27 | ## Resources 28 | 29 | | Name | Type | 30 | |------|------| 31 | | [aws_dynamodb_table.lock_table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | 32 | | [aws_s3_bucket.remote_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | 33 | | [aws_s3_bucket_policy.remote_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | 34 | | [aws_s3_bucket_public_access_block.s3Public_remote_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | 35 | | [aws_ssm_parameter.locks_table_arn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 36 | | [aws_ssm_parameter.remote_state_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 37 | ## Outputs 38 | 39 | | Name | Description | 40 | |------|-------------| 41 | | [dynamodb-lock-table](#output\_dynamodb-lock-table) | DynamoDB table for Terraform execution locks | 42 | | [dynamodb-lock-table-ssm-parameter](#output\_dynamodb-lock-table-ssm-parameter) | SSM parameter containing DynamoDB table for Terraform execution locks | 43 | | [s3-state-bucket](#output\_s3-state-bucket) | S3 bucket for storing Terraform state | 44 | | [s3-state-bucket-ssm-parameter](#output\_s3-state-bucket-ssm-parameter) | SSM parameter containing S3 bucket for storing Terraform state | 45 | 46 | -------------------------------------------------------------------------------- /0_remote_state/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current_account" {} 2 | -------------------------------------------------------------------------------- /0_remote_state/dynamodb.tf: -------------------------------------------------------------------------------- 1 | resource "aws_dynamodb_table" "lock_table" { 2 | name = "${local.prefix}-dynamodb" 3 | billing_mode = "PROVISIONED" 4 | read_capacity = 5 5 | write_capacity = 5 6 | hash_key = "LockID" 7 | tags = local.common_tags 8 | 9 | attribute { 10 | name = "LockID" 11 | type = "S" 12 | } 13 | } 14 | 15 | resource "aws_ssm_parameter" "locks_table_arn" { 16 | name = "${local.ssm_prefix}/tf-locks-table-arn" 17 | type = "String" 18 | value = aws_dynamodb_table.lock_table.arn 19 | } 20 | -------------------------------------------------------------------------------- /0_remote_state/img/Remote-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hands-on-cloud/aws-codepipeline-terraform-cicd-pipeline/7a44c6733a51da39eda9356e9e60a61334559db6/0_remote_state/img/Remote-state.png -------------------------------------------------------------------------------- /0_remote_state/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | aws_region = "us-west-2" 3 | prefix = "hands-on-cloud-terraform-remote-state" 4 | ssm_prefix = "/org/hands-on-cloud/terraform" 5 | common_tags = { 6 | Project = "hands-on-cloud" 7 | ManagedBy = "Terraform" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /0_remote_state/outputs.tf: -------------------------------------------------------------------------------- 1 | output "dynamodb-lock-table" { 2 | value = aws_dynamodb_table.lock_table.name 3 | description = "DynamoDB table for Terraform execution locks" 4 | } 5 | 6 | output "dynamodb-lock-table-ssm-parameter" { 7 | value = "${local.ssm_prefix}/tf-locks-table-arn" 8 | description = "SSM parameter containing DynamoDB table for Terraform execution locks" 9 | } 10 | 11 | output "s3-state-bucket" { 12 | value = aws_s3_bucket.remote_state.id 13 | description = "S3 bucket for storing Terraform state" 14 | } 15 | 16 | output "s3-state-bucket-ssm-parameter" { 17 | value = "${local.ssm_prefix}/tf-remote-state-bucket" 18 | description = "SSM parameter containing S3 bucket for storing Terraform state" 19 | } 20 | -------------------------------------------------------------------------------- /0_remote_state/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | } 4 | -------------------------------------------------------------------------------- /0_remote_state/s3.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "remote_state" { 2 | #checkov:skip=CKV_AWS_144: "Cross Region Unneccessary" 3 | #checkov:skip=CKV_AWS_145: "Bucket Encryption IS enabled separately" 4 | 5 | bucket = "${local.prefix}-${data.aws_caller_identity.current_account.id}" 6 | force_destroy = true 7 | 8 | lifecycle { 9 | prevent_destroy = false 10 | } 11 | 12 | tags = local.common_tags 13 | } 14 | 15 | resource "aws_s3_bucket_server_side_encryption_configuration" "remote_state" { 16 | bucket = aws_s3_bucket.remote_state.id 17 | rule { 18 | apply_server_side_encryption_by_default { 19 | sse_algorithm = "AES256" 20 | } 21 | bucket_key_enabled = true 22 | } 23 | } 24 | 25 | resource "aws_s3_bucket_versioning" "remote_state_versioning" { 26 | bucket = aws_s3_bucket.remote_state.id 27 | versioning_configuration { 28 | status = "Enabled" 29 | } 30 | } 31 | 32 | 33 | resource "aws_s3_bucket_acl" "remote_state_acl" { 34 | bucket = aws_s3_bucket.remote_state.id 35 | acl = "private" 36 | } 37 | 38 | 39 | resource "aws_s3_bucket_public_access_block" "s3Public_remote_state" { 40 | depends_on = [aws_s3_bucket_policy.remote_state] 41 | bucket = aws_s3_bucket.remote_state.id 42 | block_public_acls = true 43 | block_public_policy = true 44 | ignore_public_acls = true 45 | restrict_public_buckets = true 46 | } 47 | 48 | resource "aws_s3_bucket_policy" "remote_state" { 49 | bucket = aws_s3_bucket.remote_state.id 50 | 51 | policy = < 27 | 28 | {{ .Content }} 29 | 30 | 31 | 32 | output-values: 33 | enabled: false 34 | from: "" 35 | 36 | sort: 37 | enabled: true 38 | by: name 39 | 40 | settings: 41 | anchor: true 42 | color: true 43 | default: true 44 | description: false 45 | escape: true 46 | hide-empty: false 47 | html: true 48 | indent: 2 49 | lockfile: true 50 | read-comments: true 51 | required: true 52 | sensitive: true 53 | type: true 54 | -------------------------------------------------------------------------------- /1_pipeline/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # AWS CodePipeline demo CICD pipeline for testing Terraform projects 4 | 5 | This module deploys AWS CodePipeline, which uses tflint, Checkov, OPA, Terrascan, and Terratest to test Terraform modules. 6 | 7 | Check out [How to use CodePipeline CICD pipeline to test Terraform](https://hands-on.cloud/how-to-use-codepipeline-cicd-pipeline-to-test-terraform/) article for more information. 8 | 9 | ![CICD pipeline architecture](img/CICD-pipeline-architecture.png) 10 | 11 | ## Deployment 12 | 13 | ```sh 14 | terraform init 15 | terraform plan 16 | terraform apply -auto-approve 17 | ``` 18 | 19 | ## Tier down 20 | 21 | ```sh 22 | terraform destroy -auto-approve 23 | ``` 24 | ## Providers 25 | 26 | | Name | Version | 27 | |------|---------| 28 | | [aws](#provider\_aws) | 3.63.0 | 29 | ## Resources 30 | 31 | | Name | Type | 32 | |------|------| 33 | | [aws_codebuild_project.checkov](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | 34 | | [aws_codebuild_project.opa](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | 35 | | [aws_codebuild_project.terrascan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | 36 | | [aws_codebuild_project.terratest](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | 37 | | [aws_codebuild_project.tf_apply](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | 38 | | [aws_codebuild_project.tflint](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) | resource | 39 | | [aws_codepipeline.demo](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codepipeline) | resource | 40 | | [aws_iam_role.codebuild](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 41 | | [aws_iam_role.codepipeline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 42 | | [aws_iam_role_policy.codebuild](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 43 | | [aws_iam_role_policy.codepipeline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 44 | | [aws_s3_bucket.artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | 45 | | [aws_s3_bucket_public_access_block.s3Public_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | 46 | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 47 | | [aws_ssm_parameter.locks_table_arn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | 48 | | [aws_ssm_parameter.remote_state_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | 49 | ## Outputs 50 | 51 | No outputs. 52 | 53 | -------------------------------------------------------------------------------- /1_pipeline/artifacts_s3.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "artifacts" { 2 | #checkov:skip=CKV_AWS_144: "Cross Region Unneccessary" 3 | #checkov:skip=CKV_AWS_145: "Bucket Encryption IS enabled separately" 4 | 5 | bucket = "${var.project_name}-s3" 6 | force_destroy = true 7 | lifecycle { 8 | prevent_destroy = false 9 | } 10 | 11 | tags = local.common_tags 12 | } 13 | 14 | resource "aws_s3_bucket_public_access_block" "s3Public_artifacts" { 15 | bucket = aws_s3_bucket.artifacts.id 16 | block_public_acls = true 17 | block_public_policy = true 18 | ignore_public_acls = true 19 | restrict_public_buckets = true 20 | } 21 | 22 | 23 | 24 | resource "aws_s3_bucket_logging" "example" { 25 | bucket = aws_s3_bucket.artifacts.id 26 | target_bucket = var.logging_bucket 27 | target_prefix = "s3/${aws_s3_bucket.artifacts.id}/" 28 | } 29 | 30 | 31 | resource "aws_s3_bucket_server_side_encryption_configuration" "example" { 32 | bucket = aws_s3_bucket.artifacts.id 33 | rule { 34 | apply_server_side_encryption_by_default { 35 | sse_algorithm = "AES256" 36 | } 37 | bucket_key_enabled = true 38 | } 39 | } 40 | 41 | resource "aws_s3_bucket_versioning" "versioning" { 42 | bucket = aws_s3_bucket.artifacts.id 43 | versioning_configuration { 44 | status = "Enabled" 45 | } 46 | } 47 | 48 | 49 | resource "aws_s3_bucket_acl" "bucket_acl" { 50 | bucket = aws_s3_bucket.artifacts.id 51 | acl = "private" 52 | } 53 | 54 | 55 | resource "aws_s3_bucket_logging" "artifacts" { 56 | bucket = aws_s3_bucket.artifacts.id 57 | target_bucket = var.logging_bucket 58 | target_prefix = "s3/${aws_s3_bucket.artifacts.id}/" 59 | } 60 | 61 | resource "aws_s3_bucket_policy" "artifacts" { 62 | bucket = aws_s3_bucket.artifacts.id 63 | 64 | policy = < tf.json 19 | - opa eval --format pretty --data ./test/opa/terraform.rego --input tf.json "data.terraform" 20 | post_build: 21 | commands: 22 | - echo "OPA Test completed on `date`" 23 | -------------------------------------------------------------------------------- /1_pipeline/buildspecs/terrascan.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | env: 3 | variables: 4 | TF_VERSION: "1.0.6" 5 | TERRASCAN_VERSION: "1.9.0" 6 | phases: 7 | install: 8 | runtime-versions: 9 | python: latest 10 | commands: 11 | - cd /usr/bin 12 | - "curl -s -qL -o terraform.zip https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" 13 | - unzip -o terraform.zip 14 | - "curl -L -o terrascan_${TERRASCAN_VERSION}_Linux_x86_64.tar.gz https://github.com/accurics/terrascan/releases/download/v${TERRASCAN_VERSION}/terrascan_${TERRASCAN_VERSION}_Linux_x86_64.tar.gz" 15 | - "tar -xf terrascan_${TERRASCAN_VERSION}_Linux_x86_64.tar.gz terrascan" 16 | build: 17 | commands: 18 | - cd "$CODEBUILD_SRC_DIR" 19 | - terrascan init 20 | - terrascan scan -i terraform 21 | post_build: 22 | commands: 23 | - echo "Terrascan test is completed on `date`" 24 | -------------------------------------------------------------------------------- /1_pipeline/buildspecs/terratest.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | env: 3 | variables: 4 | TF_VERSION: "1.0.6" 5 | phases: 6 | install: 7 | commands: 8 | - cd /usr/bin 9 | - "curl -s -qL -o terraform.zip https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" 10 | - unzip -o terraform.zip 11 | build: 12 | commands: 13 | - cd "$CODEBUILD_SRC_DIR" 14 | - cd test/terratest 15 | - go mod init "tftest" 16 | - go get github.com/gruntwork-io/terratest/modules/aws 17 | - go get github.com/gruntwork-io/terratest/modules/terraform@v0.38.2 18 | - go test -v 19 | post_build: 20 | commands: 21 | - echo "terratest completed on `date`" 22 | -------------------------------------------------------------------------------- /1_pipeline/buildspecs/tflint.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | env: 3 | variables: 4 | TF_VERSION: "1.0.6" 5 | phases: 6 | install: 7 | commands: 8 | - cd /usr/bin 9 | - "curl -s -qL -o terraform.zip https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" 10 | - unzip -o terraform.zip 11 | - "curl --location https://github.com/terraform-linters/tflint/releases/download/v0.33.0/tflint_linux_amd64.zip --output tflint_linux_amd64.zip" 12 | - unzip -o tflint_linux_amd64.zip 13 | build: 14 | commands: 15 | - cd "$CODEBUILD_SRC_DIR" 16 | - terraform init 17 | - terraform validate 18 | - tflint --init 19 | - tflint 20 | post_build: 21 | commands: 22 | - echo "terraform validate completed on `date`" 23 | - echo "tflint completed on `date`" 24 | -------------------------------------------------------------------------------- /1_pipeline/codebuild.tf: -------------------------------------------------------------------------------- 1 | # IAM 2 | 3 | resource "aws_iam_role" "codebuild" { 4 | description = "CodeBuild Service Role - Managed by Terraform" 5 | tags = local.common_tags 6 | 7 | assume_role_policy = jsonencode( 8 | { 9 | "Version" : "2012-10-17", 10 | "Statement" : [ 11 | { 12 | "Effect" : "Allow", 13 | "Principal" : { 14 | "Service" : "codebuild.amazonaws.com" 15 | }, 16 | "Action" : "sts:AssumeRole" 17 | } 18 | ] 19 | } 20 | ) 21 | } 22 | 23 | resource "aws_iam_role_policy" "codebuild" { 24 | role = aws_iam_role.codebuild.id 25 | 26 | policy = jsonencode( 27 | { 28 | "Version" : "2012-10-17", 29 | "Statement" : [ 30 | { 31 | "Effect" : "Allow", 32 | "Action" : [ 33 | "s3:*" 34 | ], 35 | "Resource" : "*" 36 | }, 37 | { 38 | "Effect" : "Allow", 39 | "Action" : [ 40 | "dynamodb:GetItem", 41 | "dynamodb:PutItem", 42 | "dynamodb:DeleteItem" 43 | ], 44 | "Resource" : data.aws_ssm_parameter.locks_table_arn.value 45 | }, 46 | { 47 | "Effect" : "Allow", 48 | "Action" : [ 49 | "kms:*" 50 | ], 51 | "Resource" : "*" 52 | }, 53 | { 54 | "Effect" : "Allow", 55 | "Action" : [ 56 | "ssm:*" 57 | ], 58 | "Resource" : "*" 59 | }, 60 | { 61 | "Effect" : "Allow", 62 | "Action" : [ 63 | "logs:CreateLogGroup", 64 | "logs:CreateLogStream", 65 | "logs:PutLogEvents" 66 | ], 67 | "Resource" : "*" 68 | } 69 | ] 70 | } 71 | ) 72 | } 73 | 74 | # CodeBuild 75 | resource "aws_codebuild_project" "tflint" { 76 | #checkov:skip=CKV_AWS_147: "temp" 77 | 78 | name = "${var.project_name}-tflint" 79 | description = "Managed using Terraform" 80 | service_role = aws_iam_role.codebuild.arn 81 | tags = local.common_tags 82 | 83 | artifacts { 84 | type = "CODEPIPELINE" 85 | } 86 | 87 | environment { 88 | compute_type = "BUILD_GENERAL1_SMALL" 89 | image = "aws/codebuild/standard:6.0" 90 | type = "LINUX_CONTAINER" 91 | } 92 | 93 | source { 94 | type = "CODEPIPELINE" 95 | buildspec = data.local_file.tflint.content 96 | } 97 | } 98 | 99 | resource "aws_codebuild_project" "checkov" { 100 | #checkov:skip=CKV_AWS_147: "temp" 101 | 102 | name = "${var.project_name}-checkov" 103 | description = "Managed using Terraform" 104 | service_role = aws_iam_role.codebuild.arn 105 | tags = local.common_tags 106 | 107 | artifacts { 108 | type = "CODEPIPELINE" 109 | } 110 | 111 | environment { 112 | compute_type = "BUILD_GENERAL1_MEDIUM" 113 | image = "aws/codebuild/standard:6.0" 114 | type = "LINUX_CONTAINER" 115 | } 116 | 117 | source { 118 | type = "CODEPIPELINE" 119 | buildspec = data.local_file.checkov.content 120 | } 121 | } 122 | 123 | resource "aws_codebuild_project" "opa" { 124 | #checkov:skip=CKV_AWS_147: "temp" 125 | 126 | name = "${var.project_name}-opa" 127 | description = "Managed using Terraform" 128 | service_role = aws_iam_role.codebuild.arn 129 | tags = local.common_tags 130 | 131 | artifacts { 132 | type = "CODEPIPELINE" 133 | } 134 | 135 | environment { 136 | compute_type = "BUILD_GENERAL1_SMALL" 137 | image = "aws/codebuild/standard:6.0" 138 | type = "LINUX_CONTAINER" 139 | } 140 | 141 | source { 142 | type = "CODEPIPELINE" 143 | buildspec = data.local_file.opa.content 144 | } 145 | } 146 | 147 | resource "aws_codebuild_project" "terrascan" { 148 | #checkov:skip=CKV_AWS_147: "temp" 149 | 150 | name = "${var.project_name}-terrascan" 151 | description = "Managed using Terraform" 152 | service_role = aws_iam_role.codebuild.arn 153 | tags = local.common_tags 154 | 155 | artifacts { 156 | type = "CODEPIPELINE" 157 | } 158 | 159 | environment { 160 | compute_type = "BUILD_GENERAL1_SMALL" 161 | image = "aws/codebuild/standard:6.0" 162 | type = "LINUX_CONTAINER" 163 | } 164 | 165 | source { 166 | type = "CODEPIPELINE" 167 | buildspec = data.local_file.terrascan.content 168 | } 169 | } 170 | 171 | resource "aws_codebuild_project" "terratest" { 172 | #checkov:skip=CKV_AWS_147: "temp" 173 | 174 | name = "${var.project_name}-terratest" 175 | description = "Managed using Terraform" 176 | service_role = aws_iam_role.codebuild.arn 177 | tags = local.common_tags 178 | 179 | artifacts { 180 | type = "CODEPIPELINE" 181 | } 182 | 183 | environment { 184 | compute_type = "BUILD_GENERAL1_SMALL" 185 | image = "aws/codebuild/standard:6.0" 186 | type = "LINUX_CONTAINER" 187 | } 188 | 189 | source { 190 | type = "CODEPIPELINE" 191 | buildspec = data.local_file.terratest.content 192 | } 193 | } 194 | 195 | resource "aws_codebuild_project" "infracost" { 196 | #checkov:skip=CKV_AWS_147: "temp" 197 | 198 | name = "${var.project_name}-infracost" 199 | description = "Managed using Terraform" 200 | service_role = aws_iam_role.codebuild.arn 201 | tags = local.common_tags 202 | 203 | artifacts { 204 | type = "CODEPIPELINE" 205 | } 206 | 207 | environment { 208 | compute_type = "BUILD_GENERAL1_SMALL" 209 | image = "aws/codebuild/standard:6.0" 210 | type = "LINUX_CONTAINER" 211 | 212 | environment_variable { 213 | name = "INFRACOST_API_KEY_SSM_PARAM_NAME" 214 | value = "${local.ssm_prefix}/infracost_api_key" 215 | } 216 | } 217 | 218 | source { 219 | type = "CODEPIPELINE" 220 | buildspec = data.local_file.infracost.content 221 | } 222 | } 223 | 224 | resource "aws_codebuild_project" "tf_apply" { 225 | #checkov:skip=CKV_AWS_147: "temp" 226 | 227 | name = "${var.project_name}-tf-apply" 228 | description = "Managed using Terraform" 229 | service_role = aws_iam_role.codebuild.arn 230 | tags = local.common_tags 231 | 232 | artifacts { 233 | type = "CODEPIPELINE" 234 | } 235 | 236 | environment { 237 | compute_type = "BUILD_GENERAL1_SMALL" 238 | image = "aws/codebuild/standard:6.0" 239 | type = "LINUX_CONTAINER" 240 | } 241 | 242 | source { 243 | type = "CODEPIPELINE" 244 | buildspec = data.local_file.buildspec.content 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /1_pipeline/codepipeline.tf: -------------------------------------------------------------------------------- 1 | # IAM 2 | 3 | resource "aws_iam_role" "codepipeline" { 4 | description = "CodePipeline Service Role - Managed by Terraform" 5 | tags = local.common_tags 6 | 7 | assume_role_policy = jsonencode( 8 | { 9 | "Version" : "2012-10-17", 10 | "Statement" : [ 11 | { 12 | "Effect" : "Allow", 13 | "Principal" : { 14 | "Service" : "codepipeline.amazonaws.com" 15 | }, 16 | "Action" : "sts:AssumeRole" 17 | } 18 | ] 19 | } 20 | ) 21 | } 22 | 23 | resource "aws_iam_role_policy" "codepipeline" { 24 | role = aws_iam_role.codepipeline.id 25 | 26 | policy = jsonencode( 27 | { 28 | "Version" : "2012-10-17", 29 | "Statement" : [ 30 | { 31 | "Effect":"Allow", 32 | "Action": [ 33 | "s3:*" 34 | ], 35 | "Resource": "*" 36 | }, 37 | { 38 | "Effect" : "Allow", 39 | "Action" : "iam:PassRole", 40 | "Resource" : aws_iam_role.codebuild.arn 41 | }, 42 | { 43 | "Effect" : "Allow", 44 | "Action" : [ 45 | "codecommit:BatchGet*", 46 | "codecommit:BatchDescribe*", 47 | "codecommit:Describe*", 48 | "codecommit:Get*", 49 | "codecommit:List*", 50 | "codecommit:GitPull", 51 | "codecommit:UploadArchive", 52 | "codecommit:GetBranch", 53 | ], 54 | "Resource" : "*" 55 | }, 56 | { 57 | "Effect" : "Allow", 58 | "Action" : [ 59 | "codebuild:StartBuild", 60 | "codebuild:StopBuild", 61 | "codebuild:BatchGetBuilds" 62 | ], 63 | "Resource" : [ 64 | aws_codebuild_project.tflint.arn, 65 | aws_codebuild_project.checkov.arn, 66 | aws_codebuild_project.opa.arn, 67 | aws_codebuild_project.terrascan.arn, 68 | aws_codebuild_project.terratest.arn, 69 | aws_codebuild_project.infracost.arn, 70 | aws_codebuild_project.tf_apply.arn 71 | ] 72 | } 73 | ] 74 | } 75 | ) 76 | } 77 | 78 | # CodePipeline 79 | 80 | resource "aws_codepipeline" "demo" { 81 | name = "${local.prefix}-demo" 82 | role_arn = aws_iam_role.codepipeline.arn 83 | tags = local.common_tags 84 | 85 | artifact_store { 86 | location = aws_s3_bucket.artifacts.id 87 | type = "S3" 88 | } 89 | 90 | stage { 91 | name = "Clone" 92 | 93 | action { 94 | name = "Source" 95 | category = "Source" 96 | owner = "AWS" 97 | provider = "CodeCommit" 98 | version = "1" 99 | output_artifacts = ["CodeWorkspace"] 100 | 101 | configuration = { 102 | RepositoryName = var.repository_name 103 | BranchName = var.listen_branch_name 104 | PollForSourceChanges = true 105 | } 106 | } 107 | } 108 | 109 | stage { 110 | name = "Terraform-Project-Testing" 111 | 112 | action { 113 | run_order = 1 114 | name = "tflint" 115 | category = "Build" 116 | owner = "AWS" 117 | provider = "CodeBuild" 118 | input_artifacts = ["CodeWorkspace"] 119 | output_artifacts = [] 120 | version = "1" 121 | 122 | configuration = { 123 | ProjectName = aws_codebuild_project.tflint.name 124 | } 125 | } 126 | 127 | action { 128 | run_order = 1 129 | name = "checkov" 130 | category = "Build" 131 | owner = "AWS" 132 | provider = "CodeBuild" 133 | input_artifacts = ["CodeWorkspace"] 134 | output_artifacts = [] 135 | version = "1" 136 | 137 | configuration = { 138 | ProjectName = aws_codebuild_project.checkov.name 139 | } 140 | } 141 | 142 | action { 143 | run_order = 1 144 | name = "opa" 145 | category = "Build" 146 | owner = "AWS" 147 | provider = "CodeBuild" 148 | input_artifacts = ["CodeWorkspace"] 149 | output_artifacts = [] 150 | version = "1" 151 | 152 | configuration = { 153 | ProjectName = aws_codebuild_project.opa.name 154 | } 155 | } 156 | 157 | action { 158 | run_order = 1 159 | name = "terrascan" 160 | category = "Build" 161 | owner = "AWS" 162 | provider = "CodeBuild" 163 | input_artifacts = ["CodeWorkspace"] 164 | output_artifacts = [] 165 | version = "1" 166 | 167 | configuration = { 168 | ProjectName = aws_codebuild_project.terrascan.name 169 | } 170 | } 171 | 172 | action { 173 | run_order = 2 174 | name = "terratest" 175 | category = "Build" 176 | owner = "AWS" 177 | provider = "CodeBuild" 178 | input_artifacts = ["CodeWorkspace"] 179 | output_artifacts = [] 180 | version = "1" 181 | 182 | configuration = { 183 | ProjectName = aws_codebuild_project.terratest.name 184 | } 185 | } 186 | 187 | action { 188 | run_order = 1 189 | name = "infracost" 190 | category = "Build" 191 | owner = "AWS" 192 | provider = "CodeBuild" 193 | input_artifacts = ["CodeWorkspace"] 194 | output_artifacts = [] 195 | version = "1" 196 | 197 | configuration = { 198 | ProjectName = aws_codebuild_project.infracost.name 199 | } 200 | } 201 | } 202 | 203 | stage { 204 | name = "Manual-Approval" 205 | 206 | action { 207 | run_order = 1 208 | name = "DevOps-Lead-Approval" 209 | category = "Approval" 210 | owner = "AWS" 211 | provider = "Manual" 212 | version = "1" 213 | } 214 | } 215 | 216 | stage { 217 | name = "Deploy" 218 | 219 | action { 220 | run_order = 1 221 | name = "terraform-apply" 222 | category = "Build" 223 | owner = "AWS" 224 | provider = "CodeBuild" 225 | input_artifacts = ["CodeWorkspace"] 226 | output_artifacts = [] 227 | version = "1" 228 | 229 | configuration = { 230 | ProjectName = aws_codebuild_project.tf_apply.name 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /1_pipeline/data.tf: -------------------------------------------------------------------------------- 1 | 2 | data "aws_caller_identity" "current_account" { 3 | # To retrieve the account ID -- needed for KMS key policy 4 | } 5 | 6 | 7 | data "aws_region" "current_region" { 8 | # To retrieve the current AWS region 9 | } 10 | 11 | ##### Buildspecs ##### 12 | data "local_file" "buildspec" { 13 | filename = "${path.module}/buildspecs/buildspec.yml" 14 | } 15 | 16 | data "local_file" "checkov" { 17 | filename = "${path.module}/buildspecs/checkov.yml" 18 | } 19 | 20 | 21 | data "local_file" "infracost" { 22 | filename = "${path.module}/buildspecs/infracost.yml" 23 | } 24 | 25 | data "local_file" "opa" { 26 | filename = "${path.module}/buildspecs/opa.yml" 27 | } 28 | 29 | data "local_file" "terrascan" { 30 | filename = "${path.module}/buildspecs/terrascan.yml" 31 | } 32 | 33 | data "local_file" "terratest" { 34 | filename = "${path.module}/buildspecs/terratest.yml" 35 | } 36 | 37 | data "local_file" "tflint" { 38 | filename = "${path.module}/buildspecs/tflint.yml" 39 | } 40 | -------------------------------------------------------------------------------- /1_pipeline/img/CICD-pipeline-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hands-on-cloud/aws-codepipeline-terraform-cicd-pipeline/7a44c6733a51da39eda9356e9e60a61334559db6/1_pipeline/img/CICD-pipeline-architecture.png -------------------------------------------------------------------------------- /1_pipeline/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | bucket = "hands-on-cloud-terraform-remote-state-s3" 4 | key = "hands-on-cloud-terraform-demo-pipeline.tfstate" 5 | region = "us-west-2" 6 | encrypt = "true" 7 | } 8 | } 9 | 10 | locals { 11 | aws_region = "us-west-2" 12 | prefix = "${var.repository_name}-${var.listen_branch_name}-pipeline" 13 | ssm_prefix = "/org/hands-on-cloud/terraform" 14 | common_tags = { 15 | Project = local.prefix 16 | ManagedBy = "Terraform" 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /1_pipeline/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.aws_region 3 | } 4 | -------------------------------------------------------------------------------- /1_pipeline/ssm_parameters.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_ssm_parameter" "remote_state_bucket" { 4 | name = "${local.ssm_prefix}/tf-remote-state-bucket" 5 | } 6 | 7 | data "aws_ssm_parameter" "locks_table_arn" { 8 | name = "${local.ssm_prefix}/tf-locks-table-arn" 9 | } 10 | -------------------------------------------------------------------------------- /1_pipeline/usage.md: -------------------------------------------------------------------------------- 1 | # AWS CodePipeline demo CICD pipeline for testing Terraform projects 2 | 3 | This module deploys AWS CodePipeline, which uses tflint, Checkov, OPA, Terrascan, and Terratest to test Terraform modules. 4 | 5 | Check out [How to use CodePipeline CICD pipeline to test Terraform](https://hands-on.cloud/how-to-use-codepipeline-cicd-pipeline-to-test-terraform/) article for more information. 6 | 7 | ![CICD pipeline architecture](img/CICD-pipeline-architecture.png) 8 | 9 | ## Deployment 10 | 11 | Manually create SSM Parameter store parameter to store Infracost API key. For example: 12 | 13 | * Key name: `/org/hands-on-cloud/terraform/infracost_api_key` 14 | * Type: `SecureString` 15 | * Description: `Infracost API key` 16 | * Value: `YOUR_INFRACOST_API_KEY` (Use `infracost register` to get one) 17 | 18 | By default, we're using the following prefix for SSM Parameter Store keys `/org/hands-on-cloud/terraform` (defined in [ssm_parameters](ssm_parameters.tf) file). 19 | 20 | ```sh 21 | terraform init 22 | terraform plan 23 | terraform apply -auto-approve 24 | ``` 25 | 26 | ## Tier down 27 | 28 | ```sh 29 | terraform destroy -auto-approve 30 | ``` 31 | -------------------------------------------------------------------------------- /1_pipeline/variables.tf: -------------------------------------------------------------------------------- 1 | variable "repository_name" { 2 | default = "tf-demo-project" 3 | description = "CodeCommit repository name for CodePipeline builds" 4 | } 5 | 6 | variable "listen_branch_name" { 7 | default = "master" 8 | description = "CodeCommit branch name for CodePipeline builds" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to use CodePipeline CICD pipeline to test Terraform modules 2 | 3 | This is a demo Terraform repository to set up AWS CodePipeline to test Terraform projects using tflint, Checkov, OPA, Terrascan, and Terratest. 4 | 5 | Check out [How to use CodePipeline CICD pipeline to test Terraform](https://hands-on.cloud/how-to-use-codepipeline-cicd-pipeline-to-test-terraform/) article for more information. 6 | 7 | ![CICD pipeline architecture](1_pipeline/img/CICD-pipeline-architecture.png) 8 | 9 | ## Set up Terraform remote state infrastructure 10 | 11 | This step is required to set up an infrastructure to store Terraform remote state files 12 | 13 | ```sh 14 | cd 0_remote_state 15 | terraform init 16 | terraform plan 17 | terraform apply -auto-approve 18 | ``` 19 | 20 | ## Set up AWS CodePipeline 21 | 22 | This step is required to set up an AWS CodePipeline to test Terraform projects using tflint, Checkov, OPA, Terrascan, and Terratest. 23 | 24 | ```sh 25 | cd 1_pipeline 26 | terraform init 27 | terraform plan 28 | terraform apply -auto-approve 29 | ``` 30 | --------------------------------------------------------------------------------