diff --git a/codebase-pipelines/artifactstore.tf b/codebase-pipelines/artifactstore.tf new file mode 100644 index 000000000..377fe0732 --- /dev/null +++ b/codebase-pipelines/artifactstore.tf @@ -0,0 +1,107 @@ +resource "aws_s3_bucket" "artifact_store" { + # checkov:skip=CKV_AWS_144: It's just a pipeline artifacts bucket, cross-region replication is not needed. + # checkov:skip=CKV2_AWS_62: It's just a pipeline artifacts bucket, event notifications are not needed. + # checkov:skip=CKV_AWS_21: It's just a pipeline artifacts bucket, versioning is not needed. + # checkov:skip=CKV_AWS_18: It's just a pipeline artifacts bucket, access logging is not needed. + bucket = "${var.application}-${var.codebase}-codebase-pipeline-artifact-store" + + tags = local.tags +} + +resource "aws_s3_bucket_lifecycle_configuration" "lifecycle_rule" { + bucket = aws_s3_bucket.artifact_store.id + + rule { + id = "delete-after-7-days" + status = "Enabled" + + abort_incomplete_multipart_upload { + days_after_initiation = 1 + } + + expiration { + days = 7 + } + } +} + +data "aws_iam_policy_document" "artifact_store_bucket_policy" { + statement { + principals { + type = "*" + identifiers = ["*"] + } + + actions = [ + "s3:*", + ] + + effect = "Deny" + + condition { + test = "Bool" + variable = "aws:SecureTransport" + + values = [ + "false", + ] + } + + resources = [ + aws_s3_bucket.artifact_store.arn, + "${aws_s3_bucket.artifact_store.arn}/*", + ] + } +} + +resource "aws_s3_bucket_policy" "artifact_store_bucket_policy" { + bucket = aws_s3_bucket.artifact_store.id + policy = data.aws_iam_policy_document.artifact_store_bucket_policy.json +} + +resource "aws_kms_key" "artifact_store_kms_key" { + # checkov:skip=CKV_AWS_7:We are not currently rotating the keys + description = "KMS Key for S3 encryption" + tags = local.tags + + policy = jsonencode({ + Statement = [ + { + "Sid" : "Enable IAM User Permissions", + "Effect" : "Allow", + "Principal" : { + "AWS" : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + }, + "Action" : "kms:*", + "Resource" : "*" + } + ] + Version = "2012-10-17" + }) +} + +resource "aws_kms_alias" "artifact_store_kms_alias" { + depends_on = [aws_kms_key.artifact_store_kms_key] + name = "alias/${var.application}-${var.codebase}-codebase-pipeline-artifact-store-key" + target_key_id = aws_kms_key.artifact_store_kms_key.id +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "encryption-config" { + # checkov:skip=CKV2_AWS_67:We are not currently rotating the keys + bucket = aws_s3_bucket.artifact_store.id + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.artifact_store_kms_key.arn + sse_algorithm = "aws:kms" + } + } +} + +resource "aws_s3_bucket_public_access_block" "public_access_block" { + bucket = aws_s3_bucket.artifact_store.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} diff --git a/codebase-pipelines/buildspec.yml b/codebase-pipelines/buildspec-images.yml similarity index 100% rename from codebase-pipelines/buildspec.yml rename to codebase-pipelines/buildspec-images.yml diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml new file mode 100644 index 000000000..20adbdce4 --- /dev/null +++ b/codebase-pipelines/buildspec-manifests.yml @@ -0,0 +1,33 @@ +version: 0.2 + +env: + ${jsonencode({ + exported-variables: flatten([ + for env in environments : [ + "CLUSTER_NAME_${env}", + [ for svc in services: "SERVICE_NAME_${env}_${svc}" ] + ] + ]) + })} + +phases: + build: + commands: + - set -e + - for env in $(echo $ENVIRONMENTS | jq -c -r '.[]'); + do + EXPORT_ENV=$(echo $env | tr '[:lower:]' '[:upper:]'); + export CLUSTER_NAME_$EXPORT_ENV="$APPLICATION-$env"; + for svc in $(echo $SERVICES | jq -c -r '.[]'); + do + echo '[{"name":"'$svc'","imageUri":"'$REPOSITORY_URL':'$IMAGE_TAG'"}]' > image-definitions-$svc.json; + SERVICE_NAME=$(aws ecs list-services --cluster $APPLICATION-$env | jq -r '.serviceArns[] | select(contains("'$APPLICATION-$env'-'$svc'-Service"))'); + EXPORT_SVC="$(echo $EXPORT_ENV'_'$svc | tr - _ | tr '[:lower:]' '[:upper:]')"; + export SERVICE_NAME_$EXPORT_SVC=$(echo $SERVICE_NAME | cut -d '/' -f3); + cat image-definitions-$svc.json; + done + done + +artifacts: + files: + - "**/*" diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index bd6600d66..0c5f5e0a1 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -56,7 +56,7 @@ resource "aws_codebuild_project" "codebase_image_build" { source { type = "GITHUB" - buildspec = file("${path.module}/buildspec.yml") + buildspec = file("${path.module}/buildspec-images.yml") location = "https://github.com/${var.repository}.git" git_clone_depth = 0 git_submodules_config { @@ -113,3 +113,83 @@ resource "aws_codebuild_webhook" "codebuild_webhook" { } } } + + +resource "aws_codebuild_project" "codebase_deploy_manifests" { + for_each = local.pipeline_map + name = "${var.application}-${var.codebase}-${each.value.name}-codebase-deploy-manifests" + description = "Create image deploy manifests to deploy services" + build_timeout = 5 + service_role = aws_iam_role.codebuild_manifests.arn + encryption_key = aws_kms_key.artifact_store_kms_key.arn + + artifacts { + type = "CODEPIPELINE" + } + + cache { + type = "S3" + location = aws_s3_bucket.artifact_store.bucket + } + + environment { + compute_type = "BUILD_GENERAL1_SMALL" + image = "aws/codebuild/amazonlinux2-x86_64-standard:5.0" + type = "LINUX_CONTAINER" + image_pull_credentials_type = "CODEBUILD" + } + + logs_config { + cloudwatch_logs { + group_name = aws_cloudwatch_log_group.codebase_deploy_manifests.name + stream_name = aws_cloudwatch_log_stream.codebase_deploy_manifests.name + } + } + + source { + type = "CODEPIPELINE" + buildspec = templatefile("${path.module}/buildspec-manifests.yml", { + application = var.application, + environments = [ + for env in each.value.environments : upper(env.name) + ], + services = local.service_export_names + }) + } + + tags = local.tags +} + +resource "aws_kms_key" "codebuild_kms_key" { + description = "KMS Key for ${var.application} ${var.codebase} CodeBuild encryption" + enable_key_rotation = true + + policy = jsonencode({ + Statement = [ + { + "Sid" : "Enable IAM User Permissions", + "Effect" : "Allow", + "Principal" : { + "AWS" : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + }, + "Action" : "kms:*", + "Resource" : "*" + } + ] + Version = "2012-10-17" + }) + + tags = local.tags +} + +resource "aws_cloudwatch_log_group" "codebase_deploy_manifests" { + # checkov:skip=CKV_AWS_338:Retains logs for 3 months instead of 1 year + # checkov:skip=CKV_AWS_158:Log groups encrypted using default encryption key instead of KMS CMK + name = "codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-group" + retention_in_days = 90 +} + +resource "aws_cloudwatch_log_stream" "codebase_deploy_manifests" { + name = "codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-stream" + log_group_name = aws_cloudwatch_log_group.codebase_deploy_manifests.name +} diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf new file mode 100644 index 000000000..e1a728039 --- /dev/null +++ b/codebase-pipelines/codepipeline.tf @@ -0,0 +1,108 @@ +resource "aws_codepipeline" "codebase_pipeline" { + for_each = local.pipeline_map + name = "${var.application}-${var.codebase}-${each.value.name}-codebase-pipeline" + role_arn = aws_iam_role.codebase_deploy_pipeline.arn + depends_on = [aws_iam_role_policy.artifact_store_access_for_codebase_pipeline] + pipeline_type = "V2" + execution_mode = "QUEUED" + + variable { + name = "IMAGE_TAG" + default_value = coalesce(each.value.tag, false) ? "tag-latest" : "branch-${each.value.branch}" + description = "Tagged image in ECR to deploy" + } + + artifact_store { + location = aws_s3_bucket.artifact_store.bucket + type = "S3" + + encryption_key { + id = aws_kms_key.artifact_store_kms_key.arn + type = "KMS" + } + } + + stage { + name = "Source" + + action { + name = "Source" + category = "Source" + owner = "AWS" + provider = "ECR" + version = "1" + namespace = "source_ecr" + output_artifacts = ["source_output"] + + configuration = { + RepositoryName = local.ecr_name + ImageTag = coalesce(each.value.tag, false) ? "tag-latest" : "branch-${each.value.branch}" + } + } + } + + stage { + name = "Create-Deploy-Manifests" + + action { + name = "CreateManifests" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + input_artifacts = ["source_output"] + output_artifacts = ["manifest_output"] + version = "1" + namespace = "build_manifest" + + configuration = { + ProjectName = "${var.application}-${var.codebase}-${each.value.name}-codebase-deploy-manifests" + EnvironmentVariables : jsonencode([ + { name : "APPLICATION", value : var.application }, + { name : "ENVIRONMENTS", value : jsonencode([for env in each.value.environments : env.name]) }, + { name : "SERVICES", value : jsonencode(local.services) }, + { name : "REPOSITORY_URL", value : "${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/${local.ecr_name}" }, + { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } + ]) + } + } + } + + dynamic "stage" { + for_each = each.value.environments + content { + name = "Deploy-${stage.value.name}" + + dynamic "action" { + for_each = coalesce(stage.value.requires_approval, false) ? [1] : [] + content { + name = "Approve-${stage.value.name}" + category = "Approval" + owner = "AWS" + provider = "Manual" + version = "1" + run_order = 1 + } + } + + dynamic "action" { + for_each = local.service_order_list + content { + name = action.value.name + category = "Deploy" + owner = "AWS" + provider = "ECS" + version = "1" + input_artifacts = ["manifest_output"] + run_order = action.value.order + 1 + configuration = { + ClusterName = "#{build_manifest.CLUSTER_NAME_${upper(stage.value.name)}}" + ServiceName = "#{build_manifest.SERVICE_NAME_${upper(stage.value.name)}_${upper(replace(action.value.name, "-", "_"))}}" + FileName = "image-definitions-${action.value.name}.json" + } + } + } + } + } + + tags = local.tags +} diff --git a/codebase-pipelines/eventbridge.tf b/codebase-pipelines/eventbridge.tf new file mode 100644 index 000000000..404c0e803 --- /dev/null +++ b/codebase-pipelines/eventbridge.tf @@ -0,0 +1,63 @@ +resource "aws_cloudwatch_event_rule" "ecr_image_publish" { + for_each = local.pipeline_map + name = "${var.application}-${var.codebase}-ecr-image-publish-${each.value.name}" + description = "Trigger ${each.value.name} deploy pipeline when an ECR image is published" + + event_pattern = jsonencode({ + source : ["aws.ecr"], + detail-type : ["ECR Image Action"], + detail : { + action-type : ["PUSH"], + image-tag : [coalesce(each.value.tag, false) ? "tag-latest" : "branch-${each.value.branch}"], + repository-name : [local.ecr_name], + result : ["SUCCESS"], + } + }) +} + +resource "aws_cloudwatch_event_target" "codepipeline" { + for_each = local.pipeline_map + rule = aws_cloudwatch_event_rule.ecr_image_publish[each.key].name + arn = aws_codepipeline.codebase_pipeline[each.key].arn + role_arn = aws_iam_role.event_bridge_pipeline_trigger.arn +} + +resource "aws_iam_role" "event_bridge_pipeline_trigger" { + name = "${var.application}-${var.codebase}-event-bridge-pipeline-trigger" + assume_role_policy = data.aws_iam_policy_document.assume_event_bridge_policy.json + tags = local.tags +} + +resource "aws_iam_role_policy" "event_bridge_pipeline_trigger" { + name = "${var.application}-${var.codebase}-pipeline-trigger-access-for-event-bridge" + role = aws_iam_role.event_bridge_pipeline_trigger.name + policy = data.aws_iam_policy_document.event_bridge_pipeline_trigger.json +} + +data "aws_iam_policy_document" "assume_event_bridge_policy" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["events.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +data "aws_iam_policy_document" "event_bridge_pipeline_trigger" { + dynamic "statement" { + for_each = local.pipeline_map + content { + effect = "Allow" + actions = [ + "codepipeline:StartPipelineExecution" + ] + resources = [ + "arn:aws:codepipeline:${local.account_region}:${var.application}-${var.codebase}-${statement.value.name}-codebase-pipeline" + ] + } + } +} diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 4f2cd722d..1a4eae210 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -25,13 +25,13 @@ resource "aws_iam_role_policy_attachment" "ssm_access" { policy_arn = "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess" } -resource "aws_iam_role_policy" "codebuild_logs" { - name = "log-policy" +resource "aws_iam_role_policy" "log_access_for_codebuild_images" { + name = "${var.application}-${var.codebase}-log-access-for-codebuild-images" role = aws_iam_role.codebase_image_build.name - policy = data.aws_iam_policy_document.codebuild_logs.json + policy = data.aws_iam_policy_document.log_access_for_codebuild.json } -data "aws_iam_policy_document" "codebuild_logs" { +data "aws_iam_policy_document" "log_access_for_codebuild" { statement { effect = "Allow" actions = [ @@ -43,7 +43,9 @@ data "aws_iam_policy_document" "codebuild_logs" { resources = [ aws_cloudwatch_log_group.codebase_image_build.arn, "${aws_cloudwatch_log_group.codebase_image_build.arn}:*", - "arn:aws:logs:${local.account_region}:log-group:*" + "arn:aws:logs:${local.account_region}:log-group:*", + "arn:aws:codebuild:${local.account_region}:build/${var.application}-${var.codebase}-*-codebase-deploy-manifests", + "arn:aws:codebuild:${local.account_region}:build/${var.application}-${var.codebase}-*-codebase-deploy-manifests:*" ] } @@ -58,18 +60,19 @@ data "aws_iam_policy_document" "codebuild_logs" { ] resources = [ "arn:aws:codebuild:${local.account_region}:report-group/${aws_codebuild_project.codebase_image_build.name}-*", - "arn:aws:codebuild:${local.account_region}:report-group/pipeline-${var.application}-*" + "arn:aws:codebuild:${local.account_region}:report-group/pipeline-${var.application}-*", + "arn:aws:codebuild:${local.account_region}:report-group/${var.application}-${var.codebase}-*-codebase-deploy-manifests-*" ] } } -resource "aws_iam_role_policy" "ecr_access" { - name = "ecr-policy" +resource "aws_iam_role_policy" "ecr_access_for_codebuild_images" { + name = "${var.application}-${var.codebase}-ecr-access-for-codebuild-images" role = aws_iam_role.codebase_image_build.name - policy = data.aws_iam_policy_document.ecr_access.json + policy = data.aws_iam_policy_document.ecr_access_for_codebuild_images.json } -data "aws_iam_policy_document" "ecr_access" { +data "aws_iam_policy_document" "ecr_access_for_codebuild_images" { statement { effect = "Allow" actions = [ @@ -163,3 +166,214 @@ data "aws_iam_policy_document" "codestar_connection_access" { ] } } + +resource "aws_iam_role" "codebuild_manifests" { + name = "${var.application}-${var.codebase}-codebase-codebuild-manifests" + assume_role_policy = data.aws_iam_policy_document.assume_codebuild_role.json + tags = local.tags +} + +resource "aws_iam_role_policy" "artifact_store_access_for_codebuild_manifests" { + name = "${var.application}-${var.codebase}-artifact-store-access-for-codebuild-manifests" + role = aws_iam_role.codebuild_manifests.name + policy = data.aws_iam_policy_document.access_artifact_store.json +} + +data "aws_iam_policy_document" "access_artifact_store" { + # checkov:skip=CKV_AWS_111:Permissions required to change ACLs on uploaded artifacts + # checkov:skip=CKV_AWS_356:Permissions required to upload artifacts + statement { + effect = "Allow" + + actions = [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject", + ] + + resources = [ + aws_s3_bucket.artifact_store.arn, + "${aws_s3_bucket.artifact_store.arn}/*" + ] + } + + statement { + effect = "Allow" + + actions = [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + ] + + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "kms:GenerateDataKey", + "kms:Decrypt" + ] + resources = [ + aws_kms_key.artifact_store_kms_key.arn + ] + } +} + +resource "aws_iam_role_policy" "log_access_for_codebuild_manifests" { + name = "${var.application}-${var.codebase}-log-access-for-codebuild-manifests" + role = aws_iam_role.codebuild_manifests.name + policy = data.aws_iam_policy_document.log_access_for_codebuild.json +} + +resource "aws_iam_role_policy" "ecs_access_for_codebuild_manifests" { + name = "${var.application}-${var.codebase}-ecs-access-for-codebuild-manifests" + role = aws_iam_role.codebuild_manifests.name + policy = data.aws_iam_policy_document.ecs_access_for_codebuild_manifests.json +} + +data "aws_iam_policy_document" "ecs_access_for_codebuild_manifests" { + dynamic "statement" { + for_each = local.pipeline_environments + content { + effect = "Allow" + actions = [ + "ecs:ListServices" + ] + resources = [ + "arn:aws:ecs:${local.account_region}:service/${var.application}-${statement.value}/*" + ] + } + } +} + +resource "aws_iam_role" "codebase_deploy_pipeline" { + name = "${var.application}-${var.codebase}-codebase-pipeline" + assume_role_policy = data.aws_iam_policy_document.assume_codepipeline_role.json + tags = local.tags +} + +data "aws_iam_policy_document" "assume_codepipeline_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["codepipeline.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role_policy" "ecr_access_for_codebase_pipeline" { + name = "${var.application}-${var.codebase}-ecr-access-for-codebase-pipeline" + role = aws_iam_role.codebase_deploy_pipeline.name + policy = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.json +} + +data "aws_iam_policy_document" "ecr_access_for_codebase_pipeline" { + statement { + effect = "Allow" + actions = [ + "ecr:DescribeImages" + ] + resources = [ + aws_ecr_repository.this.arn + ] + } +} + +resource "aws_iam_role_policy" "artifact_store_access_for_codebase_pipeline" { + name = "${var.application}-${var.codebase}-artifact-store-access-for-codebase-pipeline" + role = aws_iam_role.codebase_deploy_pipeline.name + policy = data.aws_iam_policy_document.access_artifact_store.json +} + +resource "aws_iam_role_policy" "ecs_deploy_access_for_codebase_pipeline" { + name = "${var.application}-${var.codebase}-ecs-deploy-access-for-codebase-pipeline" + role = aws_iam_role.codebase_deploy_pipeline.name + policy = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.json +} + +data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { + dynamic "statement" { + for_each = local.pipeline_environments + content { + effect = "Allow" + actions = [ + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:TagResource" + ] + resources = [ + "arn:aws:ecs:${local.account_region}:cluster/${var.application}-${statement.value}", + "arn:aws:ecs:${local.account_region}:service/${var.application}-${statement.value}/*" + ] + } + } + + dynamic "statement" { + for_each = local.pipeline_environments + content { + effect = "Allow" + actions = [ + "ecs:DescribeTasks", + "ecs:TagResource" + ] + resources = [ + "arn:aws:ecs:${local.account_region}:cluster/${var.application}-${statement.value}", + "arn:aws:ecs:${local.account_region}:task/${var.application}-${statement.value}/*" + ] + } + } + + dynamic "statement" { + for_each = local.pipeline_environments + content { + effect = "Allow" + actions = [ + "ecs:RunTask", + "ecs:TagResource" + ] + resources = ["arn:aws:ecs:${local.account_region}:task-definition/${var.application}-${statement.value}-*:*"] + } + } + + dynamic "statement" { + for_each = local.pipeline_environments + content { + effect = "Allow" + actions = [ + "ecs:ListTasks" + ] + resources = [ + "arn:aws:ecs:${local.account_region}:container-instance/${var.application}-${statement.value}/*" + ] + } + } + + statement { + effect = "Allow" + actions = [ + "ecs:RegisterTaskDefinition", + "ecs:DescribeTaskDefinition" + ] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "iam:PassRole" + ] + resources = ["*"] + condition { + test = "StringLike" + values = ["ecs-tasks.amazonaws.com"] + variable = "iam:PassedToService" + } + } +} diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index 49dddaae3..f77936b4b 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -7,7 +7,33 @@ locals { account_region = "${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}" - ecr_name = "${var.application}/${var.codebase}" - pipeline_branches = distinct([for pipeline in var.pipelines : pipeline.branch if lookup(pipeline, "branch", null) != null]) - tagged_pipeline = length([for pipeline in var.pipelines : true if lookup(pipeline, "tag", null) == true]) > 0 + ecr_name = "${var.application}/${var.codebase}" + pipeline_branches = distinct([ + for pipeline in var.pipelines : pipeline.branch if lookup(pipeline, "branch", null) != null + ]) + tagged_pipeline = length([for pipeline in var.pipelines : true if lookup(pipeline, "tag", null) == true]) > 0 + + pipeline_map = { for id, val in var.pipelines : id => val } + pipeline_environments = flatten([for pipeline in local.pipeline_map : [for env in pipeline.environments : env.name]]) + + services = sort(flatten([ + for run_group in var.services : [for service in flatten(values(run_group)) : service] + ])) + + service_export_names = sort(flatten([ + for run_group in var.services : [for service in flatten(values(run_group)) : upper(replace(service, "-", "_"))] + ])) + + service_order_list = flatten([ + for index, group in var.services : [ + for key, services in group : [ + for sorted_service in local.services : [ + for service in services : { + name = service + order = index + 1 + } if service == sorted_service + ] + ] + ] + ]) } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 32c6c4533..4f39bd4a0 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -8,16 +8,16 @@ override_data { } override_data { - target = data.aws_iam_policy_document.codebuild_logs + target = data.aws_iam_policy_document.log_access_for_codebuild values = { json = "{\"Sid\": \"CodeBuildLogs\"}" } } override_data { - target = data.aws_iam_policy_document.ecr_access + target = data.aws_iam_policy_document.ecr_access_for_codebuild_images values = { - json = "{\"Sid\": \"ECRAccess\"}" + json = "{\"Sid\": \"CodeBuildImageECRAccess\"}" } } @@ -28,11 +28,58 @@ override_data { } } +override_data { + target = data.aws_iam_policy_document.assume_codepipeline_role + values = { + json = "{\"Sid\": \"AssumeCodepipelineRole\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.ecs_access_for_codebuild_manifests + values = { + json = "{\"Sid\": \"CodeBuildDeployManifestECS\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline + values = { + json = "{\"Sid\": \"CodePipelineECSDeploy\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.assume_event_bridge_policy + values = { + json = "{\"Sid\": \"AssumeEventBridge\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.event_bridge_pipeline_trigger + values = { + json = "{\"Sid\": \"EventBridgePipelineTrigger\"}" + } +} + variables { application = "my-app" codebase = "my-codebase" repository = "my-repository" additional_ecr_repository = "my-additional-repository" + services = [ + { + "run_group_1" : [ + "service-1" + ] + }, + { + "run_group_2" : [ + "service-2" + ] + } + ] pipelines = [ { name = "main", @@ -74,7 +121,32 @@ run "test_ecr" { } } -run "test_codebuild" { +run "test_artifact_store" { + command = plan + + assert { + condition = aws_s3_bucket.artifact_store.bucket == "my-app-my-codebase-codebase-pipeline-artifact-store" + error_message = "Should be: my-app-my-codebase-codebase-pipeline-artifact-store" + } + assert { + condition = aws_kms_alias.artifact_store_kms_alias.name == "alias/my-app-my-codebase-codebase-pipeline-artifact-store-key" + error_message = "Should be: alias/my-app-my-codebase-codebase-pipeline-artifact-store-key" + } + assert { + condition = [for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[0].condition : true if el.variable == "aws:SecureTransport"][0] == true + error_message = "Should be: aws:SecureTransport" + } + assert { + condition = data.aws_iam_policy_document.artifact_store_bucket_policy.statement[0].effect == "Deny" + error_message = "Should be: Deny" + } + assert { + condition = [for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[0].actions : true if el == "s3:*"][0] == true + error_message = "Should be: s3:*" + } +} + +run "test_codebuild_images" { command = plan assert { @@ -195,25 +267,76 @@ run "test_codebuild" { ] == true error_message = "Should be: type = 'EVENT' and pattern = 'PUSH'" } +} + +run "test_main_branch_filter" { + command = plan + + variables { + pipelines = [ + { + name = "main", + branch = "main", + environments = [ + { name = "dev" }, + { name = "prod", requires_approval = true } + ] + } + ] + } + + assert { + condition = [ + for el in aws_codebuild_webhook.codebuild_webhook.filter_group : true + if[ + for filter in el.filter : true + if filter.type == "HEAD_REF" && filter.pattern == "^refs/heads/main$" + ][ + 0 + ] == true + ][ + 0 + ] == true + error_message = "Should be: type = 'HEAD_REF' and pattern = '^refs/heads/main$'" + } +} + +run "test_tagged_branch_filter" { + command = plan + + variables { + pipelines = [ + { + name = "tagged", + tag = true, + environments = [ + { name = "staging" }, + { name = "prod", requires_approval = true } + ] + } + ] + } + assert { condition = [ for el in aws_codebuild_webhook.codebuild_webhook.filter_group : true if[ for filter in el.filter : true - if filter.type == "HEAD_REF" && (filter.pattern == "^refs/heads/main$" || filter.pattern == "^refs/tags/.*") + if filter.type == "HEAD_REF" && filter.pattern == "^refs/tags/.*" ][ 0 ] == true ][ 0 ] == true - error_message = "Should be: type = 'HEAD_REF' and pattern = '^refs/heads/main$' or '^refs/tags/.*'" + error_message = "Should be: type = 'HEAD_REF' and pattern = '^refs/tags/.*'" } } run "test_iam" { command = plan + # CodeBuild image build assert { condition = aws_iam_role.codebase_image_build.name == "my-app-my-codebase-codebase-image-build" error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" @@ -227,21 +350,149 @@ run "test_iam" { error_message = "Should be: ${jsonencode(var.expected_tags)}" } assert { - condition = aws_iam_role_policy.codebuild_logs.name == "log-policy" - error_message = "Should be: 'log-policy'" + condition = data.aws_iam_policy_document.assume_codebuild_role.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.assume_codebuild_role.statement[0].actions) == "sts:AssumeRole" + error_message = "Should be: sts:AssumeRole" } assert { - condition = aws_iam_role_policy.codebuild_logs.role == "my-app-my-codebase-codebase-image-build" + condition = one(data.aws_iam_policy_document.assume_codebuild_role.statement[0].principals).type == "Service" + error_message = "Should be: Service" + } + assert { + condition = contains(one(data.aws_iam_policy_document.assume_codebuild_role.statement[0].principals).identifiers, "codebuild.amazonaws.com") + error_message = "Should contain: codebuild.amazonaws.com" + } + assert { + condition = aws_iam_role_policy.log_access_for_codebuild_images.name == "my-app-my-codebase-log-access-for-codebuild-images" + error_message = "Should be: 'my-app-my-codebase-log-access-for-codebuild-images'" + } + assert { + condition = aws_iam_role_policy.log_access_for_codebuild_images.role == "my-app-my-codebase-codebase-image-build" error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" } assert { - condition = aws_iam_role_policy.ecr_access.name == "ecr-policy" - error_message = "Should be: 'ecr-policy'" + condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[0].actions == toset(["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:TagLogGroup"]) + error_message = "Unexpected actions" } assert { - condition = aws_iam_role_policy.ecr_access.role == "my-app-my-codebase-codebase-image-build" + condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[1].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[1].actions == toset([ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ]) + error_message = "Unexpected actions" + } + assert { + condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[1].resources == toset([ + "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/my-app-my-codebase-*-codebase-deploy-manifests-*", + "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/my-app-my-codebase-codebase-image-build-*", + "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/pipeline-my-app-*" + ]) + error_message = "Unexpected resources" + } + assert { + condition = aws_iam_role_policy.ecr_access_for_codebuild_images.name == "my-app-my-codebase-ecr-access-for-codebuild-images" + error_message = "Should be: 'my-app-my-codebase-ecr-access-for-codebuild-images'" + } + assert { + condition = aws_iam_role_policy.ecr_access_for_codebuild_images.role == "my-app-my-codebase-codebase-image-build" error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" } + assert { + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[0].actions) == "ecr:GetAuthorizationToken" + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[0].resources) == "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/pipeline-my-app-*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[1].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[1].actions == toset([ + "ecr:GetAuthorizationToken", + "ecr-public:GetAuthorizationToken", + "sts:GetServiceBearerToken" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[1].resources) == "*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[2].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[2].actions == toset([ + "ecr-public:DescribeImageScanFindings", + "ecr-public:GetLifecyclePolicyPreview", + "ecr-public:GetDownloadUrlForLayer", + "ecr-public:BatchGetImage", + "ecr-public:DescribeImages", + "ecr-public:ListTagsForResource", + "ecr-public:BatchCheckLayerAvailability", + "ecr-public:GetLifecyclePolicy", + "ecr-public:GetRepositoryPolicy", + "ecr-public:PutImage", + "ecr-public:InitiateLayerUpload", + "ecr-public:UploadLayerPart", + "ecr-public:CompleteLayerUpload", + "ecr-public:BatchDeleteImage", + "ecr-public:DescribeRepositories", + "ecr-public:ListImages" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[2].resources) == "arn:aws:ecr-public::${data.aws_caller_identity.current.account_id}:repository/*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[3].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[3].actions == toset([ + "ecr:DescribeImageScanFindings", + "ecr:GetLifecyclePolicyPreview", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:DescribeImages", + "ecr:ListTagsForResource", + "ecr:BatchCheckLayerAvailability", + "ecr:GetLifecyclePolicy", + "ecr:GetRepositoryPolicy", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:BatchDeleteImage", + "ecr:DescribeRepositories", + "ecr:ListImages" + ]) + error_message = "Unexpected actions" + } assert { condition = aws_iam_role_policy.codestar_connection_access.name == "codestar-connection-policy" error_message = "Should be: 'codestar-connection-policy'" @@ -250,4 +501,1293 @@ run "test_iam" { condition = aws_iam_role_policy.codestar_connection_access.role == "my-app-my-codebase-codebase-image-build" error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" } + assert { + condition = data.aws_iam_policy_document.codestar_connection_access.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.codestar_connection_access.statement[0].actions == toset([ + "codestar-connections:GetConnectionToken", + "codestar-connections:UseConnection" + ]) + error_message = "Unexpected actions" + } + assert { + condition = aws_iam_role_policy_attachment.ssm_access.role == "my-app-my-codebase-codebase-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" + } + assert { + condition = aws_iam_role_policy_attachment.ssm_access.policy_arn == "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess" + error_message = "Should be: 'arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess'" + } + + # CodeBuild deploy manifests + assert { + condition = aws_iam_role.codebuild_manifests.name == "my-app-my-codebase-codebase-codebuild-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-codebuild-manifests'" + } + assert { + condition = aws_iam_role.codebuild_manifests.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" + error_message = "Should be: {\"Sid\": \"AssumeCodebuildRole\"}" + } + assert { + condition = jsonencode(aws_iam_role.codebuild_manifests.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.name == "my-app-my-codebase-artifact-store-access-for-codebuild-manifests" + error_message = "Should be: 'my-app-my-codebase-artifact-store-access-for-codebuild-manifests'" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-codebuild-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-codebuild-manifests'" + } + assert { + condition = data.aws_iam_policy_document.access_artifact_store.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.access_artifact_store.statement[0].actions == toset([ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning", + "s3:PutObjectAcl", + "s3:PutObject", + ]) + error_message = "Unexpected actions" + } + assert { + condition = data.aws_iam_policy_document.access_artifact_store.statement[1].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.access_artifact_store.statement[1].actions == toset([ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.access_artifact_store.statement[1].resources) == "*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.access_artifact_store.statement[2].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.access_artifact_store.statement[2].actions == toset([ + "kms:GenerateDataKey", + "kms:Decrypt" + ]) + error_message = "Unexpected actions" + } + assert { + condition = aws_iam_role_policy.log_access_for_codebuild_manifests.name == "my-app-my-codebase-log-access-for-codebuild-manifests" + error_message = "Should be: 'my-app-my-codebase-log-access-for-codebuild-manifests'" + } + assert { + condition = aws_iam_role_policy.log_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-codebuild-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-codebuild-manifests'" + } + assert { + condition = aws_iam_role_policy.ecs_access_for_codebuild_manifests.name == "my-app-my-codebase-ecs-access-for-codebuild-manifests" + error_message = "Should be: 'my-app-my-codebase-ecs-access-for-codebuild-manifests'" + } + assert { + condition = aws_iam_role_policy.ecs_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-codebuild-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-codebuild-manifests'" + } + assert { + condition = aws_iam_role_policy.ecs_access_for_codebuild_manifests.policy == "{\"Sid\": \"CodeBuildDeployManifestECS\"}" + error_message = "Should be: {\"Sid\": \"CodeBuildDeployManifestECS\"}" + } + assert { + condition = data.aws_iam_policy_document.ecs_access_for_codebuild_manifests.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_access_for_codebuild_manifests.statement[0].actions) == "ecs:ListServices" + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_access_for_codebuild_manifests.statement[0].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/my-app-dev/*" + error_message = "Unexpected resources" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_access_for_codebuild_manifests.statement[1].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/my-app-staging/*" + error_message = "Unexpected resources" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_access_for_codebuild_manifests.statement[2].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/my-app-prod/*" + error_message = "Unexpected resources" + } + + # CodePipeline + assert { + condition = aws_iam_role.codebase_deploy_pipeline.name == "my-app-my-codebase-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" + } + assert { + condition = aws_iam_role.codebase_deploy_pipeline.assume_role_policy == "{\"Sid\": \"AssumeCodepipelineRole\"}" + error_message = "Should be: {\"Sid\": \"AssumeCodepipelineRole\"}" + } + assert { + condition = jsonencode(aws_iam_role.codebase_deploy_pipeline.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = data.aws_iam_policy_document.assume_codepipeline_role.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.assume_codepipeline_role.statement[0].actions) == "sts:AssumeRole" + error_message = "Should be: sts:AssumeRole" + } + assert { + condition = one(data.aws_iam_policy_document.assume_codepipeline_role.statement[0].principals).type == "Service" + error_message = "Should be: Service" + } + assert { + condition = contains(one(data.aws_iam_policy_document.assume_codepipeline_role.statement[0].principals).identifiers, "codepipeline.amazonaws.com") + error_message = "Should contain: codepipeline.amazonaws.com" + } + assert { + condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.name == "my-app-my-codebase-ecr-access-for-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-ecr-access-for-codebase-pipeline'" + } + assert { + condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" + } + assert { + condition = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.statement[0].actions) == "ecr:DescribeImages" + error_message = "Unexpected actions" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.name == "my-app-my-codebase-artifact-store-access-for-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-artifact-store-access-for-codebase-pipeline'" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" + } + assert { + condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.name == "my-app-my-codebase-ecs-deploy-access-for-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-ecs-deploy-access-for-codebase-pipeline'" + } + assert { + condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" + } + assert { + condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.policy == "{\"Sid\": \"CodePipelineECSDeploy\"}" + error_message = "Should be: {\"Sid\": \"CodePipelineECSDeploy\"}" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[0].actions == toset([ + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[0].resources == toset([ + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/my-app-dev", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/my-app-dev/*" + ]) + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[1].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[1].actions == toset([ + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[1].resources == toset([ + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/my-app-staging", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/my-app-staging/*" + ]) + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[2].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[2].actions == toset([ + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[2].resources == toset([ + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/my-app-prod", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/my-app-prod/*" + ]) + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[3].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[3].actions == toset([ + "ecs:DescribeTasks", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[3].resources == toset([ + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/my-app-dev", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task/my-app-dev/*" + ]) + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[4].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[4].actions == toset([ + "ecs:DescribeTasks", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[4].resources == toset([ + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/my-app-staging", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task/my-app-staging/*" + ]) + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].actions == toset([ + "ecs:DescribeTasks", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].resources == toset([ + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/my-app-prod", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task/my-app-prod/*" + ]) + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[6].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[6].actions == toset([ + "ecs:RunTask", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[6].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task-definition/my-app-dev-*:*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[7].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[7].actions == toset([ + "ecs:RunTask", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[7].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task-definition/my-app-staging-*:*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[8].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[8].actions == toset([ + "ecs:RunTask", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[8].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task-definition/my-app-prod-*:*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[9].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[9].actions) == "ecs:ListTasks" + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[9].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:container-instance/my-app-dev/*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[10].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[10].actions) == "ecs:ListTasks" + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[10].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:container-instance/my-app-staging/*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[11].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[11].actions) == "ecs:ListTasks" + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[11].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:container-instance/my-app-prod/*" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[12].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[12].actions == toset([ + "ecs:DescribeTaskDefinition", + "ecs:RegisterTaskDefinition" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[12].resources) == "*" + error_message = "Unexpected resources" + } + + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[13].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[13].actions) == "iam:PassRole" + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[13].resources) == "*" + error_message = "Unexpected resources" + } + assert { + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[13].condition : el.test][0] == "StringLike" + error_message = "Should be: aws:SecureTransport" + } + assert { + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[13].condition : one(el.values)][0] == "ecs-tasks.amazonaws.com" + error_message = "Should be: aws:SecureTransport" + } + assert { + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[13].condition : el.variable][0] == "iam:PassedToService" + error_message = "Should be: aws:SecureTransport" + } +} + +run "test_codebuild_manifests" { + command = plan + + assert { + condition = aws_codebuild_project.codebase_deploy_manifests[0].name == "my-app-my-codebase-main-codebase-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-main-codebase-deploy-manifests'" + } + assert { + condition = aws_codebuild_project.codebase_deploy_manifests[0].description == "Create image deploy manifests to deploy services" + error_message = "Should be: 'Create image deploy manifests to deploy services'" + } + assert { + condition = aws_codebuild_project.codebase_deploy_manifests[0].build_timeout == 5 + error_message = "Should be: 5" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy_manifests[0].artifacts).type == "CODEPIPELINE" + error_message = "Should be: 'CODEPIPELINE'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy_manifests[0].cache).type == "S3" + error_message = "Should be: 'S3'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy_manifests[0].cache).location == "my-app-my-codebase-codebase-pipeline-artifact-store" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-artifact-store'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy_manifests[0].environment).compute_type == "BUILD_GENERAL1_SMALL" + error_message = "Should be: 'BUILD_GENERAL1_SMALL'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy_manifests[0].environment).image == "aws/codebuild/amazonlinux2-x86_64-standard:5.0" + error_message = "Should be: 'aws/codebuild/amazonlinux2-x86_64-standard:5.0'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy_manifests[0].environment).type == "LINUX_CONTAINER" + error_message = "Should be: 'LINUX_CONTAINER'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy_manifests[0].environment).image_pull_credentials_type == "CODEBUILD" + error_message = "Should be: 'CODEBUILD'" + } + assert { + condition = aws_codebuild_project.codebase_deploy_manifests[0].logs_config[0].cloudwatch_logs[ + 0 + ].group_name == "codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group'" + } + assert { + condition = aws_codebuild_project.codebase_deploy_manifests[0].logs_config[0].cloudwatch_logs[ + 0 + ].stream_name == "codebuild/my-app-my-codebase-codebase-deploy-manifests/log-stream" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy-manifests/log-stream'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy_manifests[0].source).type == "CODEPIPELINE" + error_message = "Should be: 'CODEPIPELINE'" + } + assert { + condition = length(regexall(".*\"exported-variables\":\\[\"CLUSTER_NAME_DEV\".*", aws_codebuild_project.codebase_deploy_manifests[0].source[0].buildspec)) > 0 + error_message = "Should contain: '\"exported-variables\":[\"CLUSTER_NAME_DEV\"'" + } + assert { + condition = jsonencode(aws_codebuild_project.codebase_deploy_manifests[0].tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = aws_kms_key.codebuild_kms_key.description == "KMS Key for my-app my-codebase CodeBuild encryption" + error_message = "Should be: KMS Key for my-app my-codebase CodeBuild encryption" + } + + assert { + condition = aws_kms_key.codebuild_kms_key.enable_key_rotation == true + error_message = "Should be: true" + } + + assert { + condition = jsonencode(aws_kms_key.codebuild_kms_key.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + + # Cloudwatch config: + assert { + condition = aws_cloudwatch_log_group.codebase_deploy_manifests.name == "codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group'" + } + assert { + condition = aws_cloudwatch_log_group.codebase_deploy_manifests.retention_in_days == 90 + error_message = "Should be: 90" + } + assert { + condition = aws_cloudwatch_log_stream.codebase_deploy_manifests.name == "codebuild/my-app-my-codebase-codebase-deploy-manifests/log-stream" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy-manifests/log-stream'" + } + assert { + condition = aws_cloudwatch_log_stream.codebase_deploy_manifests.log_group_name == "codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group'" + } +} + +run "test_main_pipeline" { + command = plan + + assert { + condition = aws_codepipeline.codebase_pipeline[0].name == "my-app-my-codebase-main-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-main-codebase-pipeline'" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].variable[0].name == "IMAGE_TAG" + error_message = "Should be: 'IMAGE_TAG'" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].variable[0].default_value == "branch-main" + error_message = "Should be: 'branch-main'" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].variable[0].description == "Tagged image in ECR to deploy" + error_message = "Should be: 'Tagged image in ECR to deploy'" + } + assert { + condition = tolist(aws_codepipeline.codebase_pipeline[0].artifact_store)[0].location == "my-app-my-codebase-codebase-pipeline-artifact-store" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-artifact-store'" + } + assert { + condition = tolist(aws_codepipeline.codebase_pipeline[0].artifact_store)[0].type == "S3" + error_message = "Should be: 'S3'" + } + assert { + condition = tolist(aws_codepipeline.codebase_pipeline[0].artifact_store)[0].encryption_key[0].type == "KMS" + error_message = "Should be: 'KMS'" + } + assert { + condition = jsonencode(aws_codepipeline.codebase_pipeline[0].tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = length(aws_codepipeline.codebase_pipeline[0].stage) == 3 + error_message = "Should be: 3" + } + + # Source stage + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].name == "Source" + error_message = "Should be: Source" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].name == "Source" + error_message = "Should be: Source" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].category == "Source" + error_message = "Should be: Source" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].owner == "AWS" + error_message = "Should be: AWS" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].provider == "ECR" + error_message = "Should be: ECR" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].version == "1" + error_message = "Should be: 1" + } + assert { + condition = one(aws_codepipeline.codebase_pipeline[0].stage[0].action[0].output_artifacts) == "source_output" + error_message = "Should be: source_output" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].namespace == "source_ecr" + error_message = "Should be: source_ecr" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.RepositoryName == "my-app/my-codebase" + error_message = "Should be: my-app/my-codebase" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.ImageTag == "branch-main" + error_message = "Should be: branch-main" + } + + # Create-Deploy-Manifests stage + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].name == "Create-Deploy-Manifests" + error_message = "Should be: Create-Deploy-Manifests" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].name == "CreateManifests" + error_message = "Should be: CreateManifests" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].category == "Build" + error_message = "Should be: Build" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].owner == "AWS" + error_message = "Should be: AWS" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].provider == "CodeBuild" + error_message = "Should be: CodeBuild" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].version == "1" + error_message = "Should be: 1" + } + assert { + condition = one(aws_codepipeline.codebase_pipeline[0].stage[1].action[0].input_artifacts) == "source_output" + error_message = "Should be: source_output" + } + assert { + condition = one(aws_codepipeline.codebase_pipeline[0].stage[1].action[0].output_artifacts) == "manifest_output" + error_message = "Should be: manifest_output" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-main-codebase-deploy-manifests" + error_message = "Should be: my-app-my-codebase-main-codebase-deploy-manifests" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[\\\"dev\\\"]\"},{\"name\":\"SERVICES\",\"value\":\"[\\\"service-1\\\",\\\"service-2\\\"]\"},{\"name\":\"REPOSITORY_URL\",\"value\":\"${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" + error_message = "Configuration environment variables incorrect" + } + + # Deploy dev environment stage + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].name == "Deploy-dev" + error_message = "Should be: Deploy-dev" + } + + # Deploy service-1 action + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].name == "service-1" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].category == "Deploy" + error_message = "Action category incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].owner == "AWS" + error_message = "Action owner incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].provider == "ECS" + error_message = "Action provider incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].version == "1" + error_message = "Action Version incorrect" + } + assert { + condition = length(aws_codepipeline.codebase_pipeline[0].stage[2].action[0].input_artifacts) == 1 + error_message = "Input artifacts incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].input_artifacts[0] == "manifest_output" + error_message = "Input artifacts incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].run_order == 2 + error_message = "Run order incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_DEV}" + error_message = "Configuration ClusterName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].configuration.ServiceName == "#{build_manifest.SERVICE_NAME_DEV_SERVICE_1}" + error_message = "Configuration ServiceName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].configuration.FileName == "image-definitions-service-1.json" + error_message = "Configuration FileName incorrect" + } + + # Deploy service-2 action + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].name == "service-2" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 3 + error_message = "Run order incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_DEV}" + error_message = "Configuration ClusterName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].configuration.ServiceName == "#{build_manifest.SERVICE_NAME_DEV_SERVICE_2}" + error_message = "Configuration ServiceName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].configuration.FileName == "image-definitions-service-2.json" + error_message = "Configuration FileName incorrect" + } +} + +run "test_tagged_pipeline" { + command = plan + + assert { + condition = aws_codepipeline.codebase_pipeline[1].name == "my-app-my-codebase-tagged-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-tagged-codebase-pipeline'" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].variable[0].default_value == "tag-latest" + error_message = "Should be: 'tag-latest'" + } + assert { + condition = length(aws_codepipeline.codebase_pipeline[1].stage) == 4 + error_message = "Should be: 4" + } + + # Source stage + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[0].action[0].configuration.ImageTag == "tag-latest" + error_message = "Should be: tag-latest" + } + + # Create-Deploy-Manifests stage + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[1].name == "Create-Deploy-Manifests" + error_message = "Should be: Create-Deploy-Manifests" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-tagged-codebase-deploy-manifests" + error_message = "Should be: my-app-my-codebase-tagged-codebase-deploy-manifests" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[\\\"staging\\\",\\\"prod\\\"]\"},{\"name\":\"SERVICES\",\"value\":\"[\\\"service-1\\\",\\\"service-2\\\"]\"},{\"name\":\"REPOSITORY_URL\",\"value\":\"${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" + error_message = "Configuration environment variables incorrect" + } + + # Deploy staging environment stage + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].name == "Deploy-staging" + error_message = "Should be: Deploy-staging" + } + + # Deploy service-1 action + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].name == "service-1" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].run_order == 2 + error_message = "Run order incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_STAGING}" + error_message = "Configuration ClusterName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].configuration.ServiceName == "#{build_manifest.SERVICE_NAME_STAGING_SERVICE_1}" + error_message = "Configuration ServiceName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].configuration.FileName == "image-definitions-service-1.json" + error_message = "Configuration FileName incorrect" + } + + # Deploy service-2 action + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].name == "service-2" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].run_order == 3 + error_message = "Run order incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_STAGING}" + error_message = "Configuration ClusterName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.ServiceName == "#{build_manifest.SERVICE_NAME_STAGING_SERVICE_2}" + error_message = "Configuration ServiceName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.FileName == "image-definitions-service-2.json" + error_message = "Configuration FileName incorrect" + } + + # Deploy prod environment stage + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].name == "Deploy-prod" + error_message = "Should be: Deploy-prod" + } + + # Approval action + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].name == "Approve-prod" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].category == "Approval" + error_message = "Action category incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].owner == "AWS" + error_message = "Action owner incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].provider == "Manual" + error_message = "Action provider incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].version == "1" + error_message = "Action Version incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].run_order == 1 + error_message = "Run order incorrect" + } + + # Deploy service-1 action + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].name == "service-1" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].run_order == 2 + error_message = "Run order incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_PROD}" + error_message = "Configuration ClusterName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].configuration.ServiceName == "#{build_manifest.SERVICE_NAME_PROD_SERVICE_1}" + error_message = "Configuration ServiceName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].configuration.FileName == "image-definitions-service-1.json" + error_message = "Configuration FileName incorrect" + } + + # Deploy service-2 action + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].name == "service-2" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].run_order == 3 + error_message = "Run order incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_PROD}" + error_message = "Configuration ClusterName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].configuration.ServiceName == "#{build_manifest.SERVICE_NAME_PROD_SERVICE_2}" + error_message = "Configuration ServiceName incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].configuration.FileName == "image-definitions-service-2.json" + error_message = "Configuration FileName incorrect" + } +} + +run "test_event_bridge" { + command = plan + + # Main pipeline trigger + assert { + condition = aws_cloudwatch_event_rule.ecr_image_publish[0].name == "my-app-my-codebase-ecr-image-publish-main" + error_message = "Should be: 'my-app-my-codebase-ecr-image-publish-main'" + } + assert { + condition = aws_cloudwatch_event_rule.ecr_image_publish[0].description == "Trigger main deploy pipeline when an ECR image is published" + error_message = "Should be: 'Trigger main deploy pipeline when an ECR image is published'" + } + assert { + condition = aws_cloudwatch_event_rule.ecr_image_publish[0].event_pattern == "{\"detail\":{\"action-type\":[\"PUSH\"],\"image-tag\":[\"branch-main\"],\"repository-name\":[\"my-app/my-codebase\"],\"result\":[\"SUCCESS\"]},\"detail-type\":[\"ECR Image Action\"],\"source\":[\"aws.ecr\"]}" + error_message = "Event pattern is incorrect" + } + assert { + condition = aws_cloudwatch_event_target.codepipeline[0].rule == "my-app-my-codebase-ecr-image-publish-main" + error_message = "Should be: 'my-app-my-codebase-ecr-image-publish-main'" + } + + # Tagged pipeline trigger + assert { + condition = aws_cloudwatch_event_rule.ecr_image_publish[1].name == "my-app-my-codebase-ecr-image-publish-tagged" + error_message = "Should be: 'my-app-my-codebase-ecr-image-publish-tagged'" + } + assert { + condition = aws_cloudwatch_event_rule.ecr_image_publish[1].description == "Trigger tagged deploy pipeline when an ECR image is published" + error_message = "Should be: 'Trigger tagged deploy pipeline when an ECR image is published'" + } + assert { + condition = aws_cloudwatch_event_rule.ecr_image_publish[1].event_pattern == "{\"detail\":{\"action-type\":[\"PUSH\"],\"image-tag\":[\"tag-latest\"],\"repository-name\":[\"my-app/my-codebase\"],\"result\":[\"SUCCESS\"]},\"detail-type\":[\"ECR Image Action\"],\"source\":[\"aws.ecr\"]}" + error_message = "Event pattern is incorrect" + } + assert { + condition = aws_cloudwatch_event_target.codepipeline[1].rule == "my-app-my-codebase-ecr-image-publish-tagged" + error_message = "Should be: 'my-app-my-codebase-ecr-image-publish-tagged'" + } + + # IAM roles + assert { + condition = aws_iam_role.event_bridge_pipeline_trigger.name == "my-app-my-codebase-event-bridge-pipeline-trigger" + error_message = "Should be: 'my-app-my-codebase-event-bridge-pipeline-trigger'" + } + assert { + condition = aws_iam_role.event_bridge_pipeline_trigger.assume_role_policy == "{\"Sid\": \"AssumeEventBridge\"}" + error_message = "Should be: {\"Sid\": \"AssumeEventBridge\"}" + } + assert { + condition = jsonencode(aws_iam_role.event_bridge_pipeline_trigger.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = aws_iam_role_policy.event_bridge_pipeline_trigger.name == "my-app-my-codebase-pipeline-trigger-access-for-event-bridge" + error_message = "Should be: 'my-app-my-codebase-pipeline-trigger-access-for-event-bridge'" + } + assert { + condition = aws_iam_role_policy.event_bridge_pipeline_trigger.role == "my-app-my-codebase-event-bridge-pipeline-trigger" + error_message = "Should be: 'my-app-my-codebase-event-bridge-pipeline-trigger'" + } + assert { + condition = aws_iam_role_policy.event_bridge_pipeline_trigger.policy == "{\"Sid\": \"EventBridgePipelineTrigger\"}" + error_message = "Unexpected policy" + } + + # IAM Policy documents + assert { + condition = data.aws_iam_policy_document.event_bridge_pipeline_trigger.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.event_bridge_pipeline_trigger.statement[0].actions) == "codepipeline:StartPipelineExecution" + error_message = "Should be: codepipeline:StartPipelineExecution" + } + assert { + condition = one(data.aws_iam_policy_document.event_bridge_pipeline_trigger.statement[0].resources) == "arn:aws:codepipeline:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:my-app-my-codebase-main-codebase-pipeline" + error_message = "Unexpected resources" + } + assert { + condition = data.aws_iam_policy_document.assume_event_bridge_policy.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.assume_event_bridge_policy.statement[0].actions) == "sts:AssumeRole" + error_message = "Should be: sts:AssumeRole" + } + assert { + condition = one(data.aws_iam_policy_document.assume_event_bridge_policy.statement[0].principals).type == "Service" + error_message = "Should be: Service" + } + assert { + condition = contains(one(data.aws_iam_policy_document.assume_event_bridge_policy.statement[0].principals).identifiers, "events.amazonaws.com") + error_message = "Should contain: events.amazonaws.com" + } +} + +run "test_pipeline_single_run_group" { + command = plan + + variables { + services = [ + { + "run_group_1" : [ + "service-1", + "service-2", + "service-3", + "service-4" + ] + } + ] + } + + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[\\\"dev\\\"]\"},{\"name\":\"SERVICES\",\"value\":\"[\\\"service-1\\\",\\\"service-2\\\",\\\"service-3\\\",\\\"service-4\\\"]\"},{\"name\":\"REPOSITORY_URL\",\"value\":\"${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" + error_message = "Configuration environment variables incorrect" + } + + # service-1 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].name == "service-1" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].run_order == 2 + error_message = "Run order incorrect" + } + + # service-2 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].name == "service-2" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 2 + error_message = "Run order incorrect" + } + + # service-3 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].name == "service-3" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].run_order == 2 + error_message = "Run order incorrect" + } + + # service-4 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].name == "service-4" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].run_order == 2 + error_message = "Run order incorrect" + } +} + +run "test_pipeline_multiple_run_groups" { + command = plan + + variables { + services = [ + { + "run_group_1" : [ + "service-1" + ] + }, + { + "run_group_2" : [ + "service-2", + "service-3" + ] + }, + { + "run_group_3" : [ + "service-4" + ] + }, + { + "run_group_4" : [ + "service-5", + "service-6", + "service-7" + ] + } + ] + } + + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[\\\"dev\\\"]\"},{\"name\":\"SERVICES\",\"value\":\"[\\\"service-1\\\",\\\"service-2\\\",\\\"service-3\\\",\\\"service-4\\\",\\\"service-5\\\",\\\"service-6\\\",\\\"service-7\\\"]\"},{\"name\":\"REPOSITORY_URL\",\"value\":\"${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" + error_message = "Configuration environment variables incorrect" + } + + # service-1 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].name == "service-1" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].run_order == 2 + error_message = "Run order incorrect" + } + + # service-2 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].name == "service-2" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 3 + error_message = "Run order incorrect" + } + + # service-3 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].name == "service-3" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].run_order == 3 + error_message = "Run order incorrect" + } + + # service-4 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].name == "service-4" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].run_order == 4 + error_message = "Run order incorrect" + } + + # service-5 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[4].name == "service-5" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[4].run_order == 5 + error_message = "Run order incorrect" + } + + # service-6 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[5].name == "service-6" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[5].run_order == 5 + error_message = "Run order incorrect" + } + + # service-7 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[6].name == "service-7" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[6].run_order == 5 + error_message = "Run order incorrect" + } +} + +run "test_pipeline_multiple_run_groups_multiple_environment_approval" { + command = plan + + variables { + services = [ + { + "run_group_1" : [ + "service-1" + ] + }, + { + "run_group_2" : [ + "service-2", + "service-3" + ] + }, + { + "run_group_3" : [ + "service-4" + ] + } + ] + pipelines = [ + { + name = "main", + branch = "main", + environments = [ + { name = "dev" }, + { name = "prod", requires_approval = true } + ] + } + ] + } + + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[\\\"dev\\\",\\\"prod\\\"]\"},{\"name\":\"SERVICES\",\"value\":\"[\\\"service-1\\\",\\\"service-2\\\",\\\"service-3\\\",\\\"service-4\\\"]\"},{\"name\":\"REPOSITORY_URL\",\"value\":\"${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" + error_message = "Configuration environment variables incorrect" + } + + # Dev + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].name == "Deploy-dev" + error_message = "Should be: Deploy-dev" + } + + # service-1 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].name == "service-1" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].run_order == 2 + error_message = "Run order incorrect" + } + + # service-2 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].name == "service-2" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 3 + error_message = "Run order incorrect" + } + + # service-3 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].name == "service-3" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].run_order == 3 + error_message = "Run order incorrect" + } + + # service-4 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].name == "service-4" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].run_order == 4 + error_message = "Run order incorrect" + } + + # Prod + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].name == "Deploy-prod" + error_message = "Should be: Deploy-prod" + } + + # Approval + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[0].name == "Approve-prod" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[0].run_order == 1 + error_message = "Run order incorrect" + } + + # service-1 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[1].name == "service-1" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[1].run_order == 2 + error_message = "Run order incorrect" + } + + # service-2 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[2].name == "service-2" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[2].run_order == 3 + error_message = "Run order incorrect" + } + + # service-3 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[3].name == "service-3" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[3].run_order == 3 + error_message = "Run order incorrect" + } + + # service-4 + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[4].name == "service-4" + error_message = "Action name incorrect" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[4].run_order == 4 + error_message = "Run order incorrect" + } } diff --git a/codebase-pipelines/variables.tf b/codebase-pipelines/variables.tf index 813067eb5..d79c7f1e2 100644 --- a/codebase-pipelines/variables.tf +++ b/codebase-pipelines/variables.tf @@ -15,5 +15,21 @@ variable "additional_ecr_repository" { } variable "pipelines" { + type = list(object( + { + name = string + branch = optional(string) + tag = optional(bool) + environments = list(object( + { + name = string + requires_approval = optional(bool) + } + )) + } + )) +} + +variable "services" { type = any } diff --git a/environment-pipelines/artifactstore.tf b/environment-pipelines/artifactstore.tf index 0efd09a8b..2b92fb1fe 100644 --- a/environment-pipelines/artifactstore.tf +++ b/environment-pipelines/artifactstore.tf @@ -1,14 +1,30 @@ resource "aws_s3_bucket" "artifact_store" { - # checkov:skip=CKV_AWS_144: Cross Region Replication not Required - # checkov:skip=CKV2_AWS_62: Requires wider discussion around log/event ingestion before implementing. To be picked up on conclusion of DBTP-974 - # checkov:skip=CKV2_AWS_61: This bucket is only used for the pipeline artifacts, so no requirement for lifecycle configuration - # checkov:skip=CKV_AWS_21: This bucket is only used for the pipeline artifacts, so no requirement for versioning - # checkov:skip=CKV_AWS_18: Requires wider discussion around log/event ingestion before implementing. To be picked up on conclusion of DBTP-974 + # checkov:skip=CKV_AWS_144: It's just a pipeline artifacts bucket, cross-region replication is not needed. + # checkov:skip=CKV2_AWS_62: It's just a pipeline artifacts bucket, event notifications are not needed. + # checkov:skip=CKV_AWS_21: It's just a pipeline artifacts bucket, versioning is not needed. + # checkov:skip=CKV_AWS_18: It's just a pipeline artifacts bucket, access logging is not needed. bucket = "${var.application}-${var.pipeline_name}-environment-pipeline-artifact-store" tags = local.tags } +resource "aws_s3_bucket_lifecycle_configuration" "lifecycle_rule" { + bucket = aws_s3_bucket.artifact_store.id + + rule { + id = "delete-after-7-days" + status = "Enabled" + + abort_incomplete_multipart_upload { + days_after_initiation = 1 + } + + expiration { + days = 7 + } + } +} + data "aws_iam_policy_document" "artifact_store_bucket_policy" { statement { principals {