From 3f34fbfd33f603dc79deeef91ca4155577b7448a Mon Sep 17 00:00:00 2001 From: James Francis Date: Wed, 6 Nov 2024 11:06:23 +0000 Subject: [PATCH 01/71] initial ecr import terraform --- codebase-pipelines/ecr.tf | 33 ++++++++++++++++++++++++ codebase-pipelines/providers.tf | 9 +++++++ codebase-pipelines/tests/unit.tftest.hcl | 18 +++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 codebase-pipelines/ecr.tf create mode 100644 codebase-pipelines/providers.tf create mode 100644 codebase-pipelines/tests/unit.tftest.hcl diff --git a/codebase-pipelines/ecr.tf b/codebase-pipelines/ecr.tf new file mode 100644 index 000000000..4520e7a28 --- /dev/null +++ b/codebase-pipelines/ecr.tf @@ -0,0 +1,33 @@ +variable "pipeline_name" { + type = string +} + +variable "application" { + type = string +} + +locals { + ecr_names = { + (var.pipeline_name) = "${var.application}/${var.pipeline_name}" + } +} + +import { + for_each = local.ecr_names + to = aws_ecr_repository.this[each.key] + id = each.value +} + +resource "aws_ecr_repository" "this" { + for_each = local.ecr_names + name = each.value + + tags = { + copilot-pipeline = each.key + copilot-application = "demodjango" + } + + image_scanning_configuration { + scan_on_push = true + } +} diff --git a/codebase-pipelines/providers.tf b/codebase-pipelines/providers.tf new file mode 100644 index 000000000..631e33505 --- /dev/null +++ b/codebase-pipelines/providers.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "~> 1.7" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5" + } + } +} diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl new file mode 100644 index 000000000..71a444cf4 --- /dev/null +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -0,0 +1,18 @@ +mock_provider "aws" { + override_resource { + target = aws_ecr_repository.demodjango_api + values = { + id = "demodjango/api" + } + } +} + +run "data_migration_unit_test" { + command = plan + + assert { + condition = aws_ecr_repository.demodjango_api.name == "demodjango/api" + error_message = "Name should be: demodjango/api" + } +} + From a59d34b4525e936225dc501d3e6806e2ccee5f6b Mon Sep 17 00:00:00 2001 From: James Francis Date: Wed, 6 Nov 2024 12:04:50 +0000 Subject: [PATCH 02/71] wip: use mapping when creating ecr repos --- codebase-pipelines/ecr.tf | 46 +++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/codebase-pipelines/ecr.tf b/codebase-pipelines/ecr.tf index 4520e7a28..53748e7fa 100644 --- a/codebase-pipelines/ecr.tf +++ b/codebase-pipelines/ecr.tf @@ -1,25 +1,45 @@ -variable "pipeline_name" { +# variable "pipeline_name" { +# type = string +# } +# +variable "application" { type = string } +# +# locals { +# ecr_names = { +# (var.pipeline_name) = "${var.application}/${var.pipeline_name}" +# } +# } +# +# import { +# for_each = local.ecr_names +# to = aws_ecr_repository.this[each.key] +# id = each.value +# } -variable "application" { +variable "ecr_names" { + type = map(string) +} + +variable "all_codebases" { + type = any +} + +variable "additional_ecr_repository" { type = string } -locals { - ecr_names = { - (var.pipeline_name) = "${var.application}/${var.pipeline_name}" - } +variable "repository" { + type = string } -import { - for_each = local.ecr_names - to = aws_ecr_repository.this[each.key] - id = each.value +variable "codebase_name" { + type = string } resource "aws_ecr_repository" "this" { - for_each = local.ecr_names + for_each = var.ecr_names name = each.value tags = { @@ -31,3 +51,7 @@ resource "aws_ecr_repository" "this" { scan_on_push = true } } + +#resource "aws_ecr_repository" "some_ecr_repo" { +# name = "demodjango/application" +#} From b997edd0b23d769c113f8f9f6b4ecd3f1ab5f6c4 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Wed, 6 Nov 2024 14:30:45 +0000 Subject: [PATCH 03/71] Refactor importing ecr repo --- codebase-pipelines/ecr.tf | 42 +++------------------------------------ 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/codebase-pipelines/ecr.tf b/codebase-pipelines/ecr.tf index 53748e7fa..a444e15ae 100644 --- a/codebase-pipelines/ecr.tf +++ b/codebase-pipelines/ecr.tf @@ -1,57 +1,21 @@ -# variable "pipeline_name" { -# type = string -# } -# variable "application" { type = string } -# -# locals { -# ecr_names = { -# (var.pipeline_name) = "${var.application}/${var.pipeline_name}" -# } -# } -# -# import { -# for_each = local.ecr_names -# to = aws_ecr_repository.this[each.key] -# id = each.value -# } - -variable "ecr_names" { - type = map(string) -} variable "all_codebases" { type = any } -variable "additional_ecr_repository" { - type = string -} - -variable "repository" { - type = string -} - -variable "codebase_name" { - type = string -} - resource "aws_ecr_repository" "this" { - for_each = var.ecr_names - name = each.value + for_each = var.all_codebases + name = "${var.application}/${each.key}" tags = { copilot-pipeline = each.key - copilot-application = "demodjango" + copilot-application = var.application } image_scanning_configuration { scan_on_push = true } } - -#resource "aws_ecr_repository" "some_ecr_repo" { -# name = "demodjango/application" -#} From f57a2a3e6d89fa6b70a8462e05a849bc8d196407 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Wed, 6 Nov 2024 18:03:53 +0000 Subject: [PATCH 04/71] Move for loop out of ecr resource; Add codebuild project and supporting resources --- codebase-pipelines/buildspec.yml | 29 ++++++++ codebase-pipelines/codebuild.tf | 117 +++++++++++++++++++++++++++++++ codebase-pipelines/ecr.tf | 13 +--- codebase-pipelines/iam.tf | 54 ++++++++++++++ codebase-pipelines/locals.tf | 11 +++ codebase-pipelines/variables.tf | 15 ++++ 6 files changed, 228 insertions(+), 11 deletions(-) create mode 100644 codebase-pipelines/buildspec.yml create mode 100644 codebase-pipelines/codebuild.tf create mode 100644 codebase-pipelines/iam.tf create mode 100644 codebase-pipelines/locals.tf create mode 100644 codebase-pipelines/variables.tf diff --git a/codebase-pipelines/buildspec.yml b/codebase-pipelines/buildspec.yml new file mode 100644 index 000000000..11a4b9158 --- /dev/null +++ b/codebase-pipelines/buildspec.yml @@ -0,0 +1,29 @@ +version: 0.2 +env: + git-credential-helper: yes + parameter-store: + SLACK_CHANNEL_ID: /codebuild/slack_oauth_channel + SLACK_TOKEN: /codebuild/slack_oauth_token + variables: + COLOR: false + CI: true +phases: + build: + commands: + - echo "Copilot environment is ${COPILOT_ENVIRONMENT}" + - /work/cli deploy --send-notifications + + post_build: + commands: + - | + if [ "${CODEBUILD_BUILD_SUCCEEDING}" != "1" ]; then + BUILD_ID_PREFIX=$(echo $CODEBUILD_BUILD_ID | cut -d':' -f1) + echo "BUILD_ID_PREFIX - ${BUILD_ID_PREFIX}" + + echo -e "\nInstalling dependencies" + pip install dbt-platform-helper + + MESSAGE=":no_entry::construction: Build failure in codebuild project: " + + platform-helper notify add-comment "${SLACK_CHANNEL_ID}" "${SLACK_TOKEN}" "" "${MESSAGE}" + fi diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf new file mode 100644 index 000000000..7d3f5637d --- /dev/null +++ b/codebase-pipelines/codebuild.tf @@ -0,0 +1,117 @@ +data "aws_codestarconnections_connection" "github_codestar_connection" { + name = var.application +} + +resource "aws_codebuild_project" "codebase_image_build" { + name = "${var.application}-${var.config.name}-codebase-image-build" + description = "Publish images on push to ${var.repository}" + build_timeout = 30 + service_role = aws_iam_role.codebase_build.arn + badge_enabled = true + + artifacts { + type = "NO_ARTIFACTS" + } + + cache { + type = "LOCAL" + location = "LOCAL_DOCKER_LAYER_CACHE" + } + + environment { + compute_type = "BUILD_GENERAL1_SMALL" + image = "public.ecr.aws/uktrade/ci-image-builder:tag-latest" + type = "LINUX_CONTAINER" + privileged_mode = true + + environment_variable { + name = "AWS_ACCOUNT_ID" + value = data.aws_caller_identity.current.account_id + } + + environment_variable { + name = "ECR_REPOSITORY" + value = var.repository + } + + environment_variable { + name = "CODESTAR_CONNECTION_ARN" + value = data.aws_codestarconnections_connection.github_codestar_connection.arn + } + + dynamic "environment_variable" { + for_each = var.additional_ecr_repository != null ? [1] : [] + content { + name = "ADDITIONAL_ECR_REPOSITORY" + value = var.additional_ecr_repository + } + } + } + + logs_config { + cloudwatch_logs { + group_name = aws_cloudwatch_log_group.codebase_image_build.name + stream_name = aws_cloudwatch_log_stream.codebase_image_build.name + } + } + + source { + type = "GITHUB" + buildspec = file("${path.module}/buildspec.yml") + location = "https://github.com/${var.repository}.git" + git_clone_depth = 0 + git_submodules_config { + fetch_submodules = false + } + } + + tags = local.tags +} + +resource "aws_cloudwatch_log_group" "codebase_image_build" { + # checkov:skip=CKV_AWS_338:Retains logs for 3 months instead of 1 year + # checkov:skip=CKV_AWS_158: To be reworked + name = "codebuild/${var.application}-${var.config.name}-codebase-image-build/log-group" + retention_in_days = 90 + # kms_key_id = aws_kms_key.codebuild_kms_key.arn +} + +resource "aws_cloudwatch_log_stream" "codebase_image_build" { + name = "codebuild/${var.application}-${var.config.name}-codebase-image-build/log-stream" + log_group_name = aws_cloudwatch_log_group.codebase_image_build.name +} + +resource "aws_codebuild_webhook" "example" { + project_name = aws_codebuild_project.codebase_image_build.name + build_type = "BUILD" + + dynamic "filter_group" { + for_each = local.pipeline_branches + content { + filter { + type = "EVENT" + pattern = "PUSH" + } + + filter { + type = "HEAD_REF" + pattern = "^refs/heads/${filter_group.value}$" + } + } + } + + dynamic "filter_group" { + for_each = local.tagged_pipeline ? [1] : [0] + content { + filter { + type = "EVENT" + pattern = "PUSH" + } + + filter { + type = "HEAD_REF" + pattern = "^refs/tags/.*" + } + } + } +} diff --git a/codebase-pipelines/ecr.tf b/codebase-pipelines/ecr.tf index a444e15ae..ce0960498 100644 --- a/codebase-pipelines/ecr.tf +++ b/codebase-pipelines/ecr.tf @@ -1,17 +1,8 @@ -variable "application" { - type = string -} - -variable "all_codebases" { - type = any -} - resource "aws_ecr_repository" "this" { - for_each = var.all_codebases - name = "${var.application}/${each.key}" + name = local.ecr_name tags = { - copilot-pipeline = each.key + copilot-pipeline = var.config.name copilot-application = var.application } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf new file mode 100644 index 000000000..34a8be5b1 --- /dev/null +++ b/codebase-pipelines/iam.tf @@ -0,0 +1,54 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +resource "aws_iam_role" "codebase_build" { + name = "${var.application}-${var.config.name}-codebase-codebuild" + assume_role_policy = data.aws_iam_policy_document.assume_codebuild_role.json + tags = local.tags +} + +data "aws_iam_policy_document" "assume_codebuild_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["codebuild.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role_policy_attachment" "ssm_access" { + role = aws_iam_role.codebase_build.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess" +} + +resource "aws_iam_role_policy_attachment" "cloudformation_access" { + role = aws_iam_role.codebase_build.name + policy_arn = "arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess" +} + +resource "aws_iam_role_policy" "codebuild_policy" { + name = "${var.application}-${var.config.name}-log-policy" + role = aws_iam_role.codebase_build.name + policy = data.aws_iam_policy_document.write_codebuild_logs.json +} + +data "aws_iam_policy_document" "write_codebuild_logs" { + statement { + sid = "CloudWatchLogs" + effect = "Allow" + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:TagLogGroup" + ] + resources = [ + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${aws_cloudwatch_log_group.codebase_image_build.name}", + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${aws_cloudwatch_log_group.codebase_image_build.name}:*" + ] + } +} diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf new file mode 100644 index 000000000..00827dfef --- /dev/null +++ b/codebase-pipelines/locals.tf @@ -0,0 +1,11 @@ +locals { + tags = { + application = var.application + copilot-application = var.application + managed-by = "DBT Platform - Terraform" + } + + ecr_name = "${var.application}/${var.config.name}" + pipeline_branches = distinct([for pipeline in var.config.pipelines : pipeline.branch if lookup(pipeline, "branch", null) != null]) + tagged_pipeline = length([for pipeline in var.config.pipelines : true if lookup(pipeline, "tag", null) == true]) > 0 +} diff --git a/codebase-pipelines/variables.tf b/codebase-pipelines/variables.tf new file mode 100644 index 000000000..fc0d3a673 --- /dev/null +++ b/codebase-pipelines/variables.tf @@ -0,0 +1,15 @@ +variable "application" { + type = string +} + +variable "repository" { + type = string +} + +variable "additional_ecr_repository" { + type = string +} + +variable "config" { + type = any +} From 4c2f12f5261c016b79885b6bddca1c6d7482a4f8 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 12:14:42 +0000 Subject: [PATCH 05/71] Change config variable to pipelines and add codebase name; Add IAM policies --- codebase-pipelines/codebuild.tf | 8 +-- codebase-pipelines/ecr.tf | 2 +- codebase-pipelines/iam.tf | 121 ++++++++++++++++++++++++++++---- codebase-pipelines/locals.tf | 8 ++- codebase-pipelines/variables.tf | 6 +- 5 files changed, 123 insertions(+), 22 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 7d3f5637d..3871e1976 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -3,10 +3,10 @@ data "aws_codestarconnections_connection" "github_codestar_connection" { } resource "aws_codebuild_project" "codebase_image_build" { - name = "${var.application}-${var.config.name}-codebase-image-build" + name = "${var.application}-${var.codebase}-codebase-image-build" description = "Publish images on push to ${var.repository}" build_timeout = 30 - service_role = aws_iam_role.codebase_build.arn + service_role = aws_iam_role.codebase_image_build.arn badge_enabled = true artifacts { @@ -71,13 +71,13 @@ resource "aws_codebuild_project" "codebase_image_build" { resource "aws_cloudwatch_log_group" "codebase_image_build" { # checkov:skip=CKV_AWS_338:Retains logs for 3 months instead of 1 year # checkov:skip=CKV_AWS_158: To be reworked - name = "codebuild/${var.application}-${var.config.name}-codebase-image-build/log-group" + name = "codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group" retention_in_days = 90 # kms_key_id = aws_kms_key.codebuild_kms_key.arn } resource "aws_cloudwatch_log_stream" "codebase_image_build" { - name = "codebuild/${var.application}-${var.config.name}-codebase-image-build/log-stream" + name = "codebuild/${var.application}-${var.codebase}-codebase-image-build/log-stream" log_group_name = aws_cloudwatch_log_group.codebase_image_build.name } diff --git a/codebase-pipelines/ecr.tf b/codebase-pipelines/ecr.tf index ce0960498..c22008e4d 100644 --- a/codebase-pipelines/ecr.tf +++ b/codebase-pipelines/ecr.tf @@ -2,7 +2,7 @@ resource "aws_ecr_repository" "this" { name = local.ecr_name tags = { - copilot-pipeline = var.config.name + copilot-pipeline = var.codebase copilot-application = var.application } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 34a8be5b1..301dd09ce 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -1,8 +1,8 @@ data "aws_caller_identity" "current" {} data "aws_region" "current" {} -resource "aws_iam_role" "codebase_build" { - name = "${var.application}-${var.config.name}-codebase-codebuild" +resource "aws_iam_role" "codebase_image_build" { + name = "${var.application}-${var.codebase}-codebase-image-build" assume_role_policy = data.aws_iam_policy_document.assume_codebuild_role.json tags = local.tags } @@ -12,7 +12,7 @@ data "aws_iam_policy_document" "assume_codebuild_role" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["codebuild.amazonaws.com"] } @@ -21,24 +21,23 @@ data "aws_iam_policy_document" "assume_codebuild_role" { } resource "aws_iam_role_policy_attachment" "ssm_access" { - role = aws_iam_role.codebase_build.name + role = aws_iam_role.codebase_image_build.name policy_arn = "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess" } resource "aws_iam_role_policy_attachment" "cloudformation_access" { - role = aws_iam_role.codebase_build.name + role = aws_iam_role.codebase_image_build.name policy_arn = "arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess" } -resource "aws_iam_role_policy" "codebuild_policy" { - name = "${var.application}-${var.config.name}-log-policy" - role = aws_iam_role.codebase_build.name - policy = data.aws_iam_policy_document.write_codebuild_logs.json +resource "aws_iam_role_policy" "codebuild_logs" { + name = "${aws_iam_role.codebase_image_build.name}-log-policy" + role = aws_iam_role.codebase_image_build.name + policy = data.aws_iam_policy_document.codebuild_logs.json } -data "aws_iam_policy_document" "write_codebuild_logs" { +data "aws_iam_policy_document" "codebuild_logs" { statement { - sid = "CloudWatchLogs" effect = "Allow" actions = [ "logs:CreateLogGroup", @@ -47,8 +46,104 @@ data "aws_iam_policy_document" "write_codebuild_logs" { "logs:TagLogGroup" ] resources = [ - "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${aws_cloudwatch_log_group.codebase_image_build.name}", - "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${aws_cloudwatch_log_group.codebase_image_build.name}:*" + "arn:aws:logs:${local.account_region}:log-group:/aws/codebuild/${aws_cloudwatch_log_group.codebase_image_build.name}", + "arn:aws:logs:${local.account_region}:log-group:/aws/codebuild/${aws_cloudwatch_log_group.codebase_image_build.name}:*" + ] + } + + statement { + effect = "Allow" + actions = [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ] + 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}-*" + ] + } +} + +resource "aws_iam_role_policy" "ecr_access" { + name = "${aws_iam_role.codebase_image_build.name}-ecr-policy" + role = aws_iam_role.codebase_image_build.name + policy = data.aws_iam_policy_document.ecr_access.json +} + +data "aws_iam_policy_document" "ecr_access" { + statement { + effect = "Allow" + actions = [ + "ecr:GetAuthorizationToken" + ] + resources = [ + "arn:aws:codebuild:${local.account_region}:report-group/pipeline-${var.application}-*" + ] + } + + statement { + effect = "Allow" + actions = [ + "ecr-public:GetAuthorizationToken", + "sts:GetServiceBearerToken" + ] + resources = [ + "*" + ] + } + + statement { + effect = "Allow" + actions = [ + "ecr-public:*" + ] + resources = [ + "arn:aws:ecr-public::${data.aws_caller_identity.current.account_id}:repository/*" + ] + } + + statement { + effect = "Allow" + actions = [ + "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" + ] + resources = [ + "*" + ] + } +} + +resource "aws_iam_role_policy" "codestar_connection_access" { + name = "${aws_iam_role.codebase_image_build.name}-codestar-connection-policy" + role = aws_iam_role.codebase_image_build.name + policy = data.aws_iam_policy_document.codestar_connection_access.json +} + +data "aws_iam_policy_document" "codestar_connection_access" { + statement { + effect = "Allow" + actions = [ + "codestar-connections:GetConnectionToken", + "codestar-connections:UseConnection" + ] + resources = [ + data.aws_codestarconnections_connection.github_codestar_connection.arn ] } } diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index 00827dfef..49dddaae3 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -5,7 +5,9 @@ locals { managed-by = "DBT Platform - Terraform" } - ecr_name = "${var.application}/${var.config.name}" - pipeline_branches = distinct([for pipeline in var.config.pipelines : pipeline.branch if lookup(pipeline, "branch", null) != null]) - tagged_pipeline = length([for pipeline in var.config.pipelines : true if lookup(pipeline, "tag", null) == true]) > 0 + 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 } diff --git a/codebase-pipelines/variables.tf b/codebase-pipelines/variables.tf index fc0d3a673..813067eb5 100644 --- a/codebase-pipelines/variables.tf +++ b/codebase-pipelines/variables.tf @@ -2,6 +2,10 @@ variable "application" { type = string } +variable "codebase" { + type = string +} + variable "repository" { type = string } @@ -10,6 +14,6 @@ variable "additional_ecr_repository" { type = string } -variable "config" { +variable "pipelines" { type = any } From d9646bdabc97d719db23bb7799f5fabf80fd3a4d Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 12:16:20 +0000 Subject: [PATCH 06/71] Initial codebuild unit test --- codebase-pipelines/tests/unit.tftest.hcl | 69 ++++++++++++++++++++---- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 71a444cf4..ecb5198a5 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -1,18 +1,67 @@ -mock_provider "aws" { - override_resource { - target = aws_ecr_repository.demodjango_api - values = { - id = "demodjango/api" - } +mock_provider "aws" {} + +override_data { + target = data.aws_iam_policy_document.assume_codebuild_role + values = { + json = "{\"Sid\": \"AssumeCodebuildRole\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.codebuild_logs + values = { + json = "{\"Sid\": \"CodeBuildLogs\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.ecr_access + values = { + json = "{\"Sid\": \"ECRAccess\"}" } } -run "data_migration_unit_test" { +override_data { + target = data.aws_iam_policy_document.codestar_connection_access + values = { + json = "{\"Sid\": \"CodeStarConnectionAccess\"}" + } +} + +variables { + application = "my-app" + codebase = "my-codebase" + repository = "my-repository" + additional_ecr_repository = "my-additional-repository" + expected_tags = { + application = "my-app" + copilot-application = "my-app" + managed-by = "DBT Platform - Terraform" + } + pipelines = [ + { + name = "main", + branch = "main", + environments = [ + { name = "dev" } + ] + }, + { + name = "tagged", + tag = true, + environments = [ + { name = "staging" }, + { name = "prod", requires_approval = true } + ] + } + ] +} + +run "test_codebuild" { command = plan assert { - condition = aws_ecr_repository.demodjango_api.name == "demodjango/api" - error_message = "Name should be: demodjango/api" + condition = aws_codebuild_project.codebase_image_build.name == "my-app-my-codebase-codebase-image-build" + error_message = "Should be: my-app-my-codebase-codebase-image-build" } } - From 135863f4874ef85bf336b4c02ccbacf22d5e32f9 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 12:21:27 +0000 Subject: [PATCH 07/71] Add correct image build buildspec --- codebase-pipelines/buildspec.yml | 41 +++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/codebase-pipelines/buildspec.yml b/codebase-pipelines/buildspec.yml index 11a4b9158..abddc0b3f 100644 --- a/codebase-pipelines/buildspec.yml +++ b/codebase-pipelines/buildspec.yml @@ -1,29 +1,48 @@ version: 0.2 + env: - git-credential-helper: yes parameter-store: SLACK_CHANNEL_ID: /codebuild/slack_oauth_channel SLACK_TOKEN: /codebuild/slack_oauth_token - variables: - COLOR: false - CI: true + phases: + install: + commands: + - > + if [ -f .copilot/phases/install.sh ]; then + bash .copilot/phases/install.sh; + fi + + pre_build: + commands: + - > + if [ -f .copilot/phases/pre_build.sh ]; then + bash .copilot/phases/pre_build.sh; + fi + build: commands: - - echo "Copilot environment is ${COPILOT_ENVIRONMENT}" - - /work/cli deploy --send-notifications + - > + if [ -f .copilot/phases/build.sh ]; then + bash .copilot/phases/build.sh; + fi + - /work/cli build --publish --send-notifications post_build: commands: - - | + - > + if [ -f .copilot/phases/post_build.sh ]; then + bash .copilot/phases/post_build.sh; + fi + if [ "${CODEBUILD_BUILD_SUCCEEDING}" != "1" ]; then BUILD_ID_PREFIX=$(echo $CODEBUILD_BUILD_ID | cut -d':' -f1) echo "BUILD_ID_PREFIX - ${BUILD_ID_PREFIX}" - + echo -e "\nInstalling dependencies" pip install dbt-platform-helper - - MESSAGE=":no_entry::construction: Build failure in codebuild project: " - + + MESSAGE=":no_entry::building_construction: Image build failure in codebuild project: " + platform-helper notify add-comment "${SLACK_CHANNEL_ID}" "${SLACK_TOKEN}" "" "${MESSAGE}" fi From 83b3c17b36da09bf416c7de44790f37ef0cfb281 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 12:29:45 +0000 Subject: [PATCH 08/71] Codebuild tests --- codebase-pipelines/codebuild.tf | 2 +- codebase-pipelines/iam.tf | 2 +- codebase-pipelines/tests/unit.tftest.hcl | 88 ++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 3871e1976..9970d361f 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -81,7 +81,7 @@ resource "aws_cloudwatch_log_stream" "codebase_image_build" { log_group_name = aws_cloudwatch_log_group.codebase_image_build.name } -resource "aws_codebuild_webhook" "example" { +resource "aws_codebuild_webhook" "codebuild_webhook" { project_name = aws_codebuild_project.codebase_image_build.name build_type = "BUILD" diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 301dd09ce..d60335bb2 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -12,7 +12,7 @@ data "aws_iam_policy_document" "assume_codebuild_role" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["codebuild.amazonaws.com"] } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index ecb5198a5..cc26b637b 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -64,4 +64,92 @@ run "test_codebuild" { condition = aws_codebuild_project.codebase_image_build.name == "my-app-my-codebase-codebase-image-build" error_message = "Should be: my-app-my-codebase-codebase-image-build" } + assert { + condition = aws_codebuild_project.codebase_image_build.description == "Publish images on push to my-repository" + error_message = "Should be: 'Publish images on push to my-repository'" + } + assert { + condition = aws_codebuild_project.codebase_image_build.build_timeout == 30 + error_message = "Should be: 30" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.artifacts).type == "NO_ARTIFACTS" + error_message = "Should be: 'NO_ARTIFACTS'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.cache).type == "LOCAL" + error_message = "Should be: 'LOCAL'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.cache).location == "LOCAL_DOCKER_LAYER_CACHE" + error_message = "Should be: 'LOCAL_DOCKER_LAYER_CACHE'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.environment).compute_type == "BUILD_GENERAL1_SMALL" + error_message = "Should be: 'BUILD_GENERAL1_SMALL'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.environment).image == "public.ecr.aws/uktrade/ci-image-builder:tag-latest" + error_message = "Should be: 'public.ecr.aws/uktrade/ci-image-builder:tag-latest'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.environment).type == "LINUX_CONTAINER" + error_message = "Should be: 'LINUX_CONTAINER'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.environment).privileged_mode == true + error_message = "Should be: true" + } + assert { + condition = aws_codebuild_project.codebase_image_build.logs_config[0].cloudwatch_logs[ + 0 + ].group_name == "codebuild/my-app-my-codebase-codebase-image-build/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-image-build/log-group'" + } + assert { + condition = aws_codebuild_project.codebase_image_build.logs_config[0].cloudwatch_logs[ + 0 + ].stream_name == "codebuild/my-app-my-codebase-codebase-image-build/log-stream" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-image-build/log-stream'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.source).type == "GITHUB" + error_message = "Should be: 'GITHUB'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.source).location == "https://github.com/my-repository.git" + error_message = "Should be: 'https://github.com/my-repository.git'" + } + assert { + condition = length(regexall(".*/work/cli build.*", aws_codebuild_project.codebase_image_build.source[0].buildspec)) > 0 + error_message = "Should contain: '/work/cli build'" + } + assert { + condition = jsonencode(aws_codebuild_project.codebase_image_build.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + + # Cloudwatch config: + assert { + condition = aws_cloudwatch_log_group.codebase_image_build.name == "codebuild/my-app-my-codebase-codebase-image-build/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-image-build/log-group'" + } + assert { + condition = aws_cloudwatch_log_group.codebase_image_build.retention_in_days == 90 + error_message = "Should be: 90" + } + assert { + condition = aws_cloudwatch_log_stream.codebase_image_build.name == "codebuild/my-app-my-codebase-codebase-image-build/log-stream" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-image-build/log-stream'" + } + assert { + condition = aws_cloudwatch_log_stream.codebase_image_build.log_group_name == "codebuild/my-app-my-codebase-codebase-image-build/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-image-build/log-group'" + } + + # Webhook config: + assert { + condition = aws_codebuild_webhook.codebuild_webhook.project_name == "my-app-my-codebase-codebase-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" + } } From cecc294f5210dc9f1ae3ac7028d9f46c994528e7 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 14:42:56 +0000 Subject: [PATCH 09/71] Tests for webhook filters --- codebase-pipelines/tests/unit.tftest.hcl | 31 ++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index cc26b637b..0e8624cd8 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -102,13 +102,13 @@ run "test_codebuild" { } assert { condition = aws_codebuild_project.codebase_image_build.logs_config[0].cloudwatch_logs[ - 0 + 0 ].group_name == "codebuild/my-app-my-codebase-codebase-image-build/log-group" error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-image-build/log-group'" } assert { condition = aws_codebuild_project.codebase_image_build.logs_config[0].cloudwatch_logs[ - 0 + 0 ].stream_name == "codebuild/my-app-my-codebase-codebase-image-build/log-stream" error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-image-build/log-stream'" } @@ -152,4 +152,31 @@ run "test_codebuild" { condition = aws_codebuild_webhook.codebuild_webhook.project_name == "my-app-my-codebase-codebase-image-build" error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" } + assert { + condition = aws_codebuild_webhook.codebuild_webhook.build_type == "BUILD" + error_message = "Should be: 'BUILD'" + } + + assert { + condition = length(aws_codebuild_webhook.codebuild_webhook.filter_group) == 2 + error_message = "Should be: 2" + } + assert { + condition = [ + for el in aws_codebuild_webhook.codebuild_webhook.filter_group : true + if[for filter in el.filter : true if filter.type == "EVENT" && filter.pattern == "PUSH"][0] == true + ][ + 0 + ] == true + error_message = "Should be: type = 'EVENT' and pattern = 'PUSH'" + } + 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/.*")][0] == true + ][ + 0 + ] == true + error_message = "Should be: type = 'HEAD_REF' and pattern = '^refs/heads/main$' or '^refs/tags/.*'" + } } From 9bea12ea983cba71f88a3f4df3c5b5049e381d1a Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 14:46:59 +0000 Subject: [PATCH 10/71] ECR tests --- codebase-pipelines/tests/unit.tftest.hcl | 38 +++++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 0e8624cd8..d2f8a411c 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -33,11 +33,6 @@ variables { codebase = "my-codebase" repository = "my-repository" additional_ecr_repository = "my-additional-repository" - expected_tags = { - application = "my-app" - copilot-application = "my-app" - managed-by = "DBT Platform - Terraform" - } pipelines = [ { name = "main", @@ -55,6 +50,28 @@ variables { ] } ] + expected_codebuild_tags = { + application = "my-app" + copilot-application = "my-app" + managed-by = "DBT Platform - Terraform" + } + expected_ecr_tags = { + copilot-pipeline = "my-codebase" + copilot-application = "my-app" + } +} + +run "test_ecr" { + command = plan + + assert { + condition = aws_ecr_repository.this.name == "my-app/my-codebase" + error_message = "Should be: my-app/my-codebase" + } + assert { + condition = jsonencode(aws_ecr_repository.this.tags) == jsonencode(var.expected_ecr_tags) + error_message = "Should be: ${jsonencode(var.expected_ecr_tags)}" + } } run "test_codebuild" { @@ -125,8 +142,8 @@ run "test_codebuild" { error_message = "Should contain: '/work/cli build'" } assert { - condition = jsonencode(aws_codebuild_project.codebase_image_build.tags) == jsonencode(var.expected_tags) - error_message = "Should be: ${jsonencode(var.expected_tags)}" + condition = jsonencode(aws_codebuild_project.codebase_image_build.tags) == jsonencode(var.expected_codebuild_tags) + error_message = "Should be: ${jsonencode(var.expected_codebuild_tags)}" } # Cloudwatch config: @@ -173,7 +190,12 @@ run "test_codebuild" { 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/.*")][0] == true + if[ + for filter in el.filter : true + if filter.type == "HEAD_REF" && (filter.pattern == "^refs/heads/main$" || filter.pattern == "^refs/tags/.*") + ][ + 0 + ] == true ][ 0 ] == true From c4a6d3432f0dfacb18d1f183f10536e7a07b4119 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 14:55:49 +0000 Subject: [PATCH 11/71] Iam policy tests --- codebase-pipelines/tests/unit.tftest.hcl | 47 ++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index d2f8a411c..012678248 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -50,7 +50,7 @@ variables { ] } ] - expected_codebuild_tags = { + expected_tags = { application = "my-app" copilot-application = "my-app" managed-by = "DBT Platform - Terraform" @@ -142,8 +142,8 @@ run "test_codebuild" { error_message = "Should contain: '/work/cli build'" } assert { - condition = jsonencode(aws_codebuild_project.codebase_image_build.tags) == jsonencode(var.expected_codebuild_tags) - error_message = "Should be: ${jsonencode(var.expected_codebuild_tags)}" + condition = jsonencode(aws_codebuild_project.codebase_image_build.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" } # Cloudwatch config: @@ -202,3 +202,44 @@ run "test_codebuild" { error_message = "Should be: type = 'HEAD_REF' and pattern = '^refs/heads/main$' or '^refs/tags/.*'" } } + +run "test_iam" { + command = plan + + 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'" + } + assert { + condition = aws_iam_role.codebase_image_build.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" + error_message = "Should be: {\"Sid\": \"AssumeCodebuildRole\"}" + } + assert { + condition = jsonencode(aws_iam_role.codebase_image_build.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = aws_iam_role_policy.codebuild_logs.name == "my-app-my-codebase-codebase-image-build-log-policy" + error_message = "Should be: 'my-app-my-codebase-codebase-image-build-log-policy'" + } + assert { + condition = aws_iam_role_policy.codebuild_logs.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 == "my-app-my-codebase-codebase-image-build-ecr-policy" + error_message = "Should be: 'my-app-my-codebase-codebase-image-build-ecr-policy'" + } + assert { + condition = aws_iam_role_policy.ecr_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.codestar_connection_access.name == "my-app-my-codebase-codebase-image-build-codestar-connection-policy" + error_message = "Should be: 'my-app-my-codebase-codebase-image-build-codestar-connection-policy'" + } + assert { + 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'" + } +} From bc6fd58fdc3720b30f6dd11fd9db55ab1d42648f Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 16:20:58 +0000 Subject: [PATCH 12/71] Permissions fixes --- codebase-pipelines/codebuild.tf | 6 ++--- codebase-pipelines/iam.tf | 23 ++++++++-------- codebase-pipelines/tests/unit.tftest.hcl | 34 +++++++++++++++++------- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 9970d361f..97e403d96 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -14,8 +14,8 @@ resource "aws_codebuild_project" "codebase_image_build" { } cache { - type = "LOCAL" - location = "LOCAL_DOCKER_LAYER_CACHE" + type = "LOCAL" + modes = ["LOCAL_DOCKER_LAYER_CACHE"] } environment { @@ -31,7 +31,7 @@ resource "aws_codebuild_project" "codebase_image_build" { environment_variable { name = "ECR_REPOSITORY" - value = var.repository + value = local.ecr_name } environment_variable { diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index d60335bb2..fbd47f4ce 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -12,7 +12,7 @@ data "aws_iam_policy_document" "assume_codebuild_role" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["codebuild.amazonaws.com"] } @@ -25,13 +25,8 @@ resource "aws_iam_role_policy_attachment" "ssm_access" { policy_arn = "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess" } -resource "aws_iam_role_policy_attachment" "cloudformation_access" { - role = aws_iam_role.codebase_image_build.name - policy_arn = "arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess" -} - resource "aws_iam_role_policy" "codebuild_logs" { - name = "${aws_iam_role.codebase_image_build.name}-log-policy" + name = "log-policy" role = aws_iam_role.codebase_image_build.name policy = data.aws_iam_policy_document.codebuild_logs.json } @@ -46,8 +41,9 @@ data "aws_iam_policy_document" "codebuild_logs" { "logs:TagLogGroup" ] resources = [ - "arn:aws:logs:${local.account_region}:log-group:/aws/codebuild/${aws_cloudwatch_log_group.codebase_image_build.name}", - "arn:aws:logs:${local.account_region}:log-group:/aws/codebuild/${aws_cloudwatch_log_group.codebase_image_build.name}:*" + aws_cloudwatch_log_group.codebase_image_build.arn, + "${aws_cloudwatch_log_group.codebase_image_build.arn}:*", + "arn:aws:logs:${local.account_region}:log-group:*" ] } @@ -68,7 +64,7 @@ data "aws_iam_policy_document" "codebuild_logs" { } resource "aws_iam_role_policy" "ecr_access" { - name = "${aws_iam_role.codebase_image_build.name}-ecr-policy" + name = "ecr-policy" role = aws_iam_role.codebase_image_build.name policy = data.aws_iam_policy_document.ecr_access.json } @@ -121,7 +117,10 @@ data "aws_iam_policy_document" "ecr_access" { "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload", - "ecr:BatchDeleteImage" + "ecr:BatchDeleteImage", + "ecr:DescribeRepositories", + "ecr:ListImages", + "ecr:GetAuthorizationToken" ] resources = [ "*" @@ -130,7 +129,7 @@ data "aws_iam_policy_document" "ecr_access" { } resource "aws_iam_role_policy" "codestar_connection_access" { - name = "${aws_iam_role.codebase_image_build.name}-codestar-connection-policy" + name = "codestar-connection-policy" role = aws_iam_role.codebase_image_build.name policy = data.aws_iam_policy_document.codestar_connection_access.json } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 012678248..3482b1f51 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -98,7 +98,7 @@ run "test_codebuild" { error_message = "Should be: 'LOCAL'" } assert { - condition = one(aws_codebuild_project.codebase_image_build.cache).location == "LOCAL_DOCKER_LAYER_CACHE" + condition = one(aws_codebuild_project.codebase_image_build.cache).modes[0] == "LOCAL_DOCKER_LAYER_CACHE" error_message = "Should be: 'LOCAL_DOCKER_LAYER_CACHE'" } assert { @@ -110,8 +110,24 @@ run "test_codebuild" { error_message = "Should be: 'public.ecr.aws/uktrade/ci-image-builder:tag-latest'" } assert { - condition = one(aws_codebuild_project.codebase_image_build.environment).type == "LINUX_CONTAINER" - error_message = "Should be: 'LINUX_CONTAINER'" + condition = one(aws_codebuild_project.codebase_image_build.environment).environment_variable[1].name == "ECR_REPOSITORY" + error_message = "Should be: 'ECR_REPOSITORY'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.environment).environment_variable[1].value == "my-app/my-codebase" + error_message = "Should be: 'my-app/my-codebase'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.environment).environment_variable[3].name == "ADDITIONAL_ECR_REPOSITORY" + error_message = "Should be: 'ADDITIONAL_ECR_REPOSITORY'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.environment).environment_variable[3].value == "my-additional-repository" + error_message = "Should be: 'my-additional-repository'" + } + assert { + condition = one(aws_codebuild_project.codebase_image_build.environment).privileged_mode == true + error_message = "Should be: true" } assert { condition = one(aws_codebuild_project.codebase_image_build.environment).privileged_mode == true @@ -219,24 +235,24 @@ run "test_iam" { error_message = "Should be: ${jsonencode(var.expected_tags)}" } assert { - condition = aws_iam_role_policy.codebuild_logs.name == "my-app-my-codebase-codebase-image-build-log-policy" - error_message = "Should be: 'my-app-my-codebase-codebase-image-build-log-policy'" + condition = aws_iam_role_policy.codebuild_logs.name == "log-policy" + error_message = "Should be: 'log-policy'" } assert { condition = aws_iam_role_policy.codebuild_logs.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 == "my-app-my-codebase-codebase-image-build-ecr-policy" - error_message = "Should be: 'my-app-my-codebase-codebase-image-build-ecr-policy'" + condition = aws_iam_role_policy.ecr_access.name == "ecr-policy" + error_message = "Should be: 'ecr-policy'" } assert { condition = aws_iam_role_policy.ecr_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.codestar_connection_access.name == "my-app-my-codebase-codebase-image-build-codestar-connection-policy" - error_message = "Should be: 'my-app-my-codebase-codebase-image-build-codestar-connection-policy'" + condition = aws_iam_role_policy.codestar_connection_access.name == "codestar-connection-policy" + error_message = "Should be: 'codestar-connection-policy'" } assert { condition = aws_iam_role_policy.codestar_connection_access.role == "my-app-my-codebase-codebase-image-build" From ec908edfdba90321e56ad2509fb06816eb9ca653 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 17:10:28 +0000 Subject: [PATCH 13/71] Fix checkov warnings --- codebase-pipelines/codebuild.tf | 7 +++---- codebase-pipelines/ecr.tf | 2 ++ codebase-pipelines/iam.tf | 26 +++++++++++++++++++++----- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 97e403d96..84e1e7e17 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -19,10 +19,9 @@ resource "aws_codebuild_project" "codebase_image_build" { } environment { - compute_type = "BUILD_GENERAL1_SMALL" - image = "public.ecr.aws/uktrade/ci-image-builder:tag-latest" - type = "LINUX_CONTAINER" - privileged_mode = true + compute_type = "BUILD_GENERAL1_SMALL" + image = "public.ecr.aws/uktrade/ci-image-builder:tag-latest" + type = "LINUX_CONTAINER" environment_variable { name = "AWS_ACCOUNT_ID" diff --git a/codebase-pipelines/ecr.tf b/codebase-pipelines/ecr.tf index c22008e4d..f68998f92 100644 --- a/codebase-pipelines/ecr.tf +++ b/codebase-pipelines/ecr.tf @@ -1,4 +1,6 @@ resource "aws_ecr_repository" "this" { + # checkov:skip=CKV_AWS_136:Not using KMS to encrypt repositories + # checkov:skip=CKV_AWS_51:ECR image tags can't be immutable name = local.ecr_name tags = { diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index fbd47f4ce..8b7437da6 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -12,7 +12,7 @@ data "aws_iam_policy_document" "assume_codebuild_role" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["codebuild.amazonaws.com"] } @@ -81,8 +81,10 @@ data "aws_iam_policy_document" "ecr_access" { } statement { + # checkov:skip=CKV_AWS_107:GetAuthorizationToken required for ci-image-builder effect = "Allow" actions = [ + "ecr:GetAuthorizationToken", "ecr-public:GetAuthorizationToken", "sts:GetServiceBearerToken" ] @@ -94,7 +96,22 @@ data "aws_iam_policy_document" "ecr_access" { statement { effect = "Allow" actions = [ - "ecr-public:*" + "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" ] resources = [ "arn:aws:ecr-public::${data.aws_caller_identity.current.account_id}:repository/*" @@ -119,11 +136,10 @@ data "aws_iam_policy_document" "ecr_access" { "ecr:CompleteLayerUpload", "ecr:BatchDeleteImage", "ecr:DescribeRepositories", - "ecr:ListImages", - "ecr:GetAuthorizationToken" + "ecr:ListImages" ] resources = [ - "*" + aws_ecr_repository.this.arn ] } } From fc44383ffe8059005dca7fa8592bc7cb85260670 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 7 Nov 2024 17:14:40 +0000 Subject: [PATCH 14/71] Remove unnecessary codebuild test --- codebase-pipelines/tests/unit.tftest.hcl | 8 -------- 1 file changed, 8 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 3482b1f51..32c6c4533 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -125,14 +125,6 @@ run "test_codebuild" { condition = one(aws_codebuild_project.codebase_image_build.environment).environment_variable[3].value == "my-additional-repository" error_message = "Should be: 'my-additional-repository'" } - assert { - condition = one(aws_codebuild_project.codebase_image_build.environment).privileged_mode == true - error_message = "Should be: true" - } - assert { - condition = one(aws_codebuild_project.codebase_image_build.environment).privileged_mode == true - error_message = "Should be: true" - } assert { condition = aws_codebuild_project.codebase_image_build.logs_config[0].cloudwatch_logs[ 0 From 8cfcfff9501a8fa72c2f105e4ce88f6c836dd246 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 8 Nov 2024 10:31:02 +0000 Subject: [PATCH 15/71] Remove commented line --- codebase-pipelines/codebuild.tf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 84e1e7e17..f856a209a 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -69,10 +69,9 @@ resource "aws_codebuild_project" "codebase_image_build" { resource "aws_cloudwatch_log_group" "codebase_image_build" { # checkov:skip=CKV_AWS_338:Retains logs for 3 months instead of 1 year - # checkov:skip=CKV_AWS_158: To be reworked + # checkov:skip=CKV_AWS_158:To be reworked name = "codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group" retention_in_days = 90 - # kms_key_id = aws_kms_key.codebuild_kms_key.arn } resource "aws_cloudwatch_log_stream" "codebase_image_build" { From 7ae1fd62e3bea1107fc396248a3ec7fa66ec6ba5 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 8 Nov 2024 12:24:47 +0000 Subject: [PATCH 16/71] Initial pipeline config --- codebase-pipelines/artifactstore.tf | 92 +++++++++++++++++++++++++++++ codebase-pipelines/codepipeline.tf | 68 +++++++++++++++++++++ codebase-pipelines/iam.tf | 74 +++++++++++++++++++++++ codebase-pipelines/locals.tf | 4 ++ codebase-pipelines/variables.tf | 4 ++ 5 files changed, 242 insertions(+) create mode 100644 codebase-pipelines/artifactstore.tf create mode 100644 codebase-pipelines/codepipeline.tf diff --git a/codebase-pipelines/artifactstore.tf b/codebase-pipelines/artifactstore.tf new file mode 100644 index 000000000..5687ff656 --- /dev/null +++ b/codebase-pipelines/artifactstore.tf @@ -0,0 +1,92 @@ +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 + bucket = "${var.application}-${var.codebase}-codebase-pipeline-artifact-store" + + tags = local.tags +} + +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({ + Id = "key-default-1" + 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/codepipeline.tf b/codebase-pipelines/codepipeline.tf new file mode 100644 index 000000000..660c2f007 --- /dev/null +++ b/codebase-pipelines/codepipeline.tf @@ -0,0 +1,68 @@ +resource "aws_codepipeline" "environment_pipeline" { + for_each = toset(var.pipelines) + name = "${var.application}-${var.codebase}-${each.value}-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" + + variable { + name = "IMAGE_TAG" + default_value = each.value.tag ? "tag-latest" : 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 = "Create-Deploy-Manifests" + + action { + name = "CreateManifests" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + output_artifacts = ["manifest_output"] + version = "1" + + configuration = { + ProjectName = "${var.application}-${var.codebase}-codebase-pipeline-manifests" + EnvironmentVariables : jsonencode([ + { name : "APPLICATION", value : var.application }, + { name : "ENVIRONMENTS", value : [for env in each.value.environments : env.name] }, + { name : "SERVICES", value : local.services }, + { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } + ]) + } + } + } + + dynamic "stage" { + for_each = each.value.environments + content { + name = stage.value.name + + dynamic "action" { + for_each = local.run_groups + content { + name = action.value + category = "Deploy" + owner = "AWS" + provider = "ECS" + version = "1" + input_artifacts = ["manifest_output"] + run_order = action.key + 1 + } + } + } + } + + tags = local.tags +} diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 8b7437da6..333024e0b 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -162,3 +162,77 @@ data "aws_iam_policy_document" "codestar_connection_access" { ] } } + +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" "codebase_deploy_pipeline_manifest_codebuild" { + name = "${var.application}-${var.codebase}-codebase-pipeline-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_codebase_pipeline" { + name = "${var.application}-${var.codebase}-artifact-store-access-for-codebase-pipeline" + role = aws_iam_role.codebase_deploy_pipeline_manifest_codebuild.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 + ] + } +} diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index 49dddaae3..d89abad15 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -10,4 +10,8 @@ locals { 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 + + run_groups = [for run_group in var.services : [for service in run_group : service]] + services = flatten(local.run_groups) + } diff --git a/codebase-pipelines/variables.tf b/codebase-pipelines/variables.tf index 813067eb5..6e1e074e1 100644 --- a/codebase-pipelines/variables.tf +++ b/codebase-pipelines/variables.tf @@ -17,3 +17,7 @@ variable "additional_ecr_repository" { variable "pipelines" { type = any } + +variable "services" { + type = any +} From 8d4d04b342b3809b9f11b2b9982aa92f74e26c0f Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 8 Nov 2024 16:37:10 +0000 Subject: [PATCH 17/71] Work out service deploy stage run order --- codebase-pipelines/codepipeline.tf | 34 ++++++++++++------------ codebase-pipelines/iam.tf | 2 +- codebase-pipelines/locals.tf | 28 +++++++++++++++---- codebase-pipelines/tests/unit.tftest.hcl | 30 +++++++++++++++++++++ codebase-pipelines/variables.tf | 13 ++++++++- 5 files changed, 83 insertions(+), 24 deletions(-) diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 660c2f007..72254a602 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -1,13 +1,13 @@ -resource "aws_codepipeline" "environment_pipeline" { - for_each = toset(var.pipelines) - name = "${var.application}-${var.codebase}-${each.value}-pipeline" +resource "aws_codepipeline" "codebase_pipeline" { + for_each = local.pipeline_map + name = "${var.application}-${var.codebase}-${each.value.name}-pipeline" role_arn = aws_iam_role.codebase_deploy_pipeline.arn - depends_on = [aws_iam_role_policy.artifact_store_access_for_codebase_pipeline] + depends_on = [aws_iam_role_policy.artifact_store_access_for_codebase_pipeline] pipeline_type = "V2" variable { name = "IMAGE_TAG" - default_value = each.value.tag ? "tag-latest" : each.value.branch + default_value = coalesce(each.value.tag, false) ? "tag-latest" : each.value.branch description = "Tagged image in ECR to deploy" } @@ -25,12 +25,12 @@ resource "aws_codepipeline" "environment_pipeline" { name = "Create-Deploy-Manifests" action { - name = "CreateManifests" - category = "Build" - owner = "AWS" - provider = "CodeBuild" + name = "CreateManifests" + category = "Build" + owner = "AWS" + provider = "CodeBuild" output_artifacts = ["manifest_output"] - version = "1" + version = "1" configuration = { ProjectName = "${var.application}-${var.codebase}-codebase-pipeline-manifests" @@ -50,15 +50,15 @@ resource "aws_codepipeline" "environment_pipeline" { name = stage.value.name dynamic "action" { - for_each = local.run_groups + for_each = local.service_order_map content { - name = action.value - category = "Deploy" - owner = "AWS" - provider = "ECS" - version = "1" + name = action.key + category = "Deploy" + owner = "AWS" + provider = "ECS" + version = "1" input_artifacts = ["manifest_output"] - run_order = action.key + 1 + run_order = action.value.order } } } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 333024e0b..215b83b62 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -185,7 +185,7 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { resource "aws_iam_role" "codebase_deploy_pipeline_manifest_codebuild" { name = "${var.application}-${var.codebase}-codebase-pipeline-manifests" assume_role_policy = data.aws_iam_policy_document.assume_codebuild_role.json - tags = local.tags + tags = local.tags } resource "aws_iam_role_policy" "artifact_store_access_for_codebase_pipeline" { diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index d89abad15..468aa891e 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -7,11 +7,29 @@ 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 - run_groups = [for run_group in var.services : [for service in run_group : service]] - services = flatten(local.run_groups) + pipeline_map = { for id, val in var.pipelines : id => val } + # ["web","api","celery-worker","celery-beat"] + services = flatten([for run_group in var.services : [for service in run_group : service]]) + + # [{"name":"web","order":1},{"name":"api","order":2},{"name":"celery-worker","order":2},{"name":"celery-beat","order":2}] + service_order_list = flatten([ + for index, group in var.services : [ + for key, services in group : [ + for service in services : { + name = service + order = index + 1 + } + ] + ] + ]) + + # {"api":{"order":2},"celery-beat":{"order":2},"celery-worker":{"order":2},"web":{"order":1}} + service_order_map = { for service in local.service_order_list : service.name => { order = service.order } } } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 32c6c4533..3735cad3a 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -28,11 +28,32 @@ override_data { } } +override_data { + target = data.aws_iam_policy_document.assume_codepipeline_role + values = { + json = "{\"Sid\": \"AssumeCodepipelineRole\"}" + } +} + variables { application = "my-app" codebase = "my-codebase" repository = "my-repository" additional_ecr_repository = "my-additional-repository" + services = [ + { + "run_group_1" : [ + "web" + ] + }, + { + "run_group_2" : [ + "api", + "celery-worker", + "celery-beat" + ] + } + ] pipelines = [ { name = "main", @@ -251,3 +272,12 @@ run "test_iam" { error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" } } + +run "test_pipeline" { + command = plan + + assert { + condition = aws_codepipeline.codebase_pipeline[0].name == "my-app-my-codebase-main-pipeline" + error_message = "Should be: 'my-app-my-codebase-main-pipeline'" + } +} diff --git a/codebase-pipelines/variables.tf b/codebase-pipelines/variables.tf index 6e1e074e1..b1bb3a9a7 100644 --- a/codebase-pipelines/variables.tf +++ b/codebase-pipelines/variables.tf @@ -15,7 +15,18 @@ variable "additional_ecr_repository" { } variable "pipelines" { - type = any + type = list(object( + { + name = string + branch = optional(string) + tag = optional(bool) + environments = list(object( + { + name = string + } + )) + } + )) } variable "services" { From 5e723ceceb8b1b181004552027500e41ef6cdddb Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 8 Nov 2024 16:47:58 +0000 Subject: [PATCH 18/71] Add ECS deploy configuration --- codebase-pipelines/codepipeline.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 72254a602..b0e441242 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -31,6 +31,7 @@ resource "aws_codepipeline" "codebase_pipeline" { provider = "CodeBuild" output_artifacts = ["manifest_output"] version = "1" + namespace = "build_manifest" configuration = { ProjectName = "${var.application}-${var.codebase}-codebase-pipeline-manifests" @@ -59,6 +60,11 @@ resource "aws_codepipeline" "codebase_pipeline" { version = "1" input_artifacts = ["manifest_output"] run_order = action.value.order + configuration = { + ClusterName = "#{build_manifest.${upper(stage.value.name)}_CLUSTER_NAME}" + ServiceName = "#{build_manifest.${upper(stage.value.name)}_${upper(action.key)}_SERVICE_NAME}" + FileName = "image-definitions-${action.key}.json" + } } } } From 43af43238aaf3b202d3776997d008ee0fef51984 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 8 Nov 2024 17:02:32 +0000 Subject: [PATCH 19/71] Add codebuild project for creating deploy manifest files --- .../{buildspec.yml => buildspec-images.yml} | 0 codebase-pipelines/buildspec-manifests.yml | 0 .../{codebuild.tf => codebuild-images.tf} | 2 +- codebase-pipelines/codebuild-manifests.tf | 72 +++++++++++++++++++ codebase-pipelines/codepipeline.tf | 2 +- codebase-pipelines/iam.tf | 6 +- 6 files changed, 77 insertions(+), 5 deletions(-) rename codebase-pipelines/{buildspec.yml => buildspec-images.yml} (100%) create mode 100644 codebase-pipelines/buildspec-manifests.yml rename codebase-pipelines/{codebuild.tf => codebuild-images.tf} (97%) create mode 100644 codebase-pipelines/codebuild-manifests.tf 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..e69de29bb diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild-images.tf similarity index 97% rename from codebase-pipelines/codebuild.tf rename to codebase-pipelines/codebuild-images.tf index f856a209a..771f23d90 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild-images.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 { diff --git a/codebase-pipelines/codebuild-manifests.tf b/codebase-pipelines/codebuild-manifests.tf new file mode 100644 index 000000000..493cfbaee --- /dev/null +++ b/codebase-pipelines/codebuild-manifests.tf @@ -0,0 +1,72 @@ +resource "aws_codebuild_project" "codebase_deploy_manifests" { + name = "${var.application}-${var.codebase}-codebase-deploy-manifests" + description = "Create image deploy manifests to deploy services" + build_timeout = 5 + service_role = aws_iam_role.codebase_deploy_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 = file("${path.module}/buildspec-manifests.yml") + } + + 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({ + Id = "key-default-1" + 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: To be reworked + 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 index b0e441242..3831ec1c4 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -34,7 +34,7 @@ resource "aws_codepipeline" "codebase_pipeline" { namespace = "build_manifest" configuration = { - ProjectName = "${var.application}-${var.codebase}-codebase-pipeline-manifests" + ProjectName = aws_codebuild_project.codebase_deploy_manifests.name EnvironmentVariables : jsonencode([ { name : "APPLICATION", value : var.application }, { name : "ENVIRONMENTS", value : [for env in each.value.environments : env.name] }, diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 215b83b62..19c73be0c 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -182,15 +182,15 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { } } -resource "aws_iam_role" "codebase_deploy_pipeline_manifest_codebuild" { - name = "${var.application}-${var.codebase}-codebase-pipeline-manifests" +resource "aws_iam_role" "codebase_deploy_manifests" { + name = "${var.application}-${var.codebase}-codebase-deploy-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_codebase_pipeline" { name = "${var.application}-${var.codebase}-artifact-store-access-for-codebase-pipeline" - role = aws_iam_role.codebase_deploy_pipeline_manifest_codebuild.name + role = aws_iam_role.codebase_deploy_manifests.name policy = data.aws_iam_policy_document.access_artifact_store.json } From 3f49bd28f57a07fd2882a9b0a99f7e012aff0114 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 11 Nov 2024 17:44:43 +0000 Subject: [PATCH 20/71] Update buildspec for deploy manifests --- codebase-pipelines/buildspec-manifests.yml | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index e69de29bb..9cd0f16b7 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -0,0 +1,28 @@ +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: + - "**/*" From f177e2d7e3bcbce38909604a0992efa36b4e983c Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 11 Nov 2024 17:45:25 +0000 Subject: [PATCH 21/71] Pass variables to buildspec template file --- codebase-pipelines/codebuild-manifests.tf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/codebase-pipelines/codebuild-manifests.tf b/codebase-pipelines/codebuild-manifests.tf index 493cfbaee..ef54803fe 100644 --- a/codebase-pipelines/codebuild-manifests.tf +++ b/codebase-pipelines/codebuild-manifests.tf @@ -1,8 +1,9 @@ resource "aws_codebuild_project" "codebase_deploy_manifests" { - name = "${var.application}-${var.codebase}-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.codebase_deploy_manifests.arn + service_role = aws_iam_role.codebuild_manifests.arn encryption_key = aws_kms_key.artifact_store_kms_key.arn artifacts { @@ -30,7 +31,7 @@ resource "aws_codebuild_project" "codebase_deploy_manifests" { source { type = "CODEPIPELINE" - buildspec = file("${path.module}/buildspec-manifests.yml") + 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 From 3e82991068e50edd2aff96195578382bf0819fee Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 11 Nov 2024 17:46:56 +0000 Subject: [PATCH 22/71] Add source stage; Fix run order list sorting; --- codebase-pipelines/codepipeline.tf | 43 ++++++++++++++++++++++-------- codebase-pipelines/locals.tf | 23 +++++++++------- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 3831ec1c4..1c998971c 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -1,13 +1,13 @@ resource "aws_codepipeline" "codebase_pipeline" { for_each = local.pipeline_map - name = "${var.application}-${var.codebase}-${each.value.name}-pipeline" + 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" variable { name = "IMAGE_TAG" - default_value = coalesce(each.value.tag, false) ? "tag-latest" : each.value.branch + default_value = coalesce(each.value.tag, false) ? "tag-latest" : "branch-${each.value.branch}" description = "Tagged image in ECR to deploy" } @@ -21,6 +21,25 @@ resource "aws_codepipeline" "codebase_pipeline" { } } + 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" @@ -29,16 +48,18 @@ resource "aws_codepipeline" "codebase_pipeline" { category = "Build" owner = "AWS" provider = "CodeBuild" + input_artifacts = ["source_output"] output_artifacts = ["manifest_output"] version = "1" - namespace = "build_manifest" + namespace = "build_manifest" configuration = { - ProjectName = aws_codebuild_project.codebase_deploy_manifests.name + ProjectName = "${var.application}-${var.codebase}-${each.value.name}-codebase-deploy-manifests" EnvironmentVariables : jsonencode([ { name : "APPLICATION", value : var.application }, - { name : "ENVIRONMENTS", value : [for env in each.value.environments : env.name] }, - { name : "SERVICES", value : local.services }, + { name : "ENVIRONMENTS", value : jsonencode([for env in each.value.environments : env.name]) }, + { name : "SERVICES", value : jsonencode(local.services) }, + { name : "REPOSITORY_URL", value : aws_ecr_repository.this.repository_url }, { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } ]) } @@ -51,9 +72,9 @@ resource "aws_codepipeline" "codebase_pipeline" { name = stage.value.name dynamic "action" { - for_each = local.service_order_map + for_each = local.service_order_list content { - name = action.key + name = action.value.name category = "Deploy" owner = "AWS" provider = "ECS" @@ -61,9 +82,9 @@ resource "aws_codepipeline" "codebase_pipeline" { input_artifacts = ["manifest_output"] run_order = action.value.order configuration = { - ClusterName = "#{build_manifest.${upper(stage.value.name)}_CLUSTER_NAME}" - ServiceName = "#{build_manifest.${upper(stage.value.name)}_${upper(action.key)}_SERVICE_NAME}" - FileName = "image-definitions-${action.key}.json" + 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" } } } diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index 468aa891e..d145c2b99 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -16,20 +16,25 @@ locals { pipeline_map = { for id, val in var.pipelines : id => val } # ["web","api","celery-worker","celery-beat"] - services = flatten([for run_group in var.services : [for service in run_group : service]]) + services = sort(flatten([ + for run_group in var.services : [for service in flatten(values(run_group)) : service] + ])) - # [{"name":"web","order":1},{"name":"api","order":2},{"name":"celery-worker","order":2},{"name":"celery-beat","order":2}] + service_export_names = sort(flatten([ + for run_group in var.services : [for service in flatten(values(run_group)) : upper(replace(service, "-", "_"))] + ])) + + # [{"name":"web","order":1},{"name":"api","order":2},{"name":"celery-beat","order":2},{"name":"celery-worker","order":2}] service_order_list = flatten([ for index, group in var.services : [ for key, services in group : [ - for service in services : { - name = service - order = index + 1 - } + for sorted_service in local.services : [ + for service in services : { + name = service + order = index + 1 + } if service == sorted_service + ] ] ] ]) - - # {"api":{"order":2},"celery-beat":{"order":2},"celery-worker":{"order":2},"web":{"order":1}} - service_order_map = { for service in local.service_order_list : service.name => { order = service.order } } } From 23776cfb8c334866822b0b804b8aba7ff2155759 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 11 Nov 2024 17:47:27 +0000 Subject: [PATCH 23/71] Add manifest codebuild permissions --- codebase-pipelines/iam.tf | 81 +++++++++++++++++++----- codebase-pipelines/tests/unit.tftest.hcl | 41 ++++++++---- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 19c73be0c..b69583dfe 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 = [ @@ -182,15 +185,39 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { } } -resource "aws_iam_role" "codebase_deploy_manifests" { - name = "${var.application}-${var.codebase}-codebase-deploy-manifests" - assume_role_policy = data.aws_iam_policy_document.assume_codebuild_role.json - tags = local.tags +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_manifests.name + role = aws_iam_role.codebase_deploy_pipeline.name + policy = data.aws_iam_policy_document.access_artifact_store.json +} + +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 } @@ -236,3 +263,27 @@ data "aws_iam_policy_document" "access_artifact_store" { ] } } + +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" { + statement { + effect = "Allow" + actions = [ + "ecs:ListServices" + ] + resources = [ + "arn:aws:ecs:${local.account_region}:service/${var.application}-*/*" + ] + } +} diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 3735cad3a..2e1dea019 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\"}" } } @@ -35,6 +35,14 @@ override_data { } } +override_data { + target = data.aws_iam_policy_document.ecs_access_for_codebuild_manifests + values = { + json = "{\"Sid\": \"CodeBuildDeployManifestECS\"}" + } +} + + variables { application = "my-app" codebase = "my-codebase" @@ -95,7 +103,7 @@ run "test_ecr" { } } -run "test_codebuild" { +run "test_codebuild_images" { command = plan assert { @@ -248,19 +256,19 @@ 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 = 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.codebuild_logs.role == "my-app-my-codebase-codebase-image-build" + 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 = 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.role == "my-app-my-codebase-codebase-image-build" + 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 { @@ -277,7 +285,16 @@ run "test_pipeline" { command = plan assert { - condition = aws_codepipeline.codebase_pipeline[0].name == "my-app-my-codebase-main-pipeline" - error_message = "Should be: 'my-app-my-codebase-main-pipeline'" + 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'" + } +} + +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'" } } From 20b6dbf060882a246562898c2fbed064be4b6be9 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 12 Nov 2024 10:11:35 +0000 Subject: [PATCH 24/71] Tighten ecs permissions --- codebase-pipelines/iam.tf | 19 +++++++++++-------- codebase-pipelines/locals.tf | 3 ++- codebase-pipelines/tests/unit.tftest.hcl | 5 +++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index b69583dfe..79d0d2b48 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -277,13 +277,16 @@ resource "aws_iam_role_policy" "ecs_access_for_codebuild_manifests" { } data "aws_iam_policy_document" "ecs_access_for_codebuild_manifests" { - statement { - effect = "Allow" - actions = [ - "ecs:ListServices" - ] - resources = [ - "arn:aws:ecs:${local.account_region}:service/${var.application}-*/*" - ] + 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}/*" + ] + } } } diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index d145c2b99..8cb113f90 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -13,7 +13,8 @@ locals { ]) 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_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]]) # ["web","api","celery-worker","celery-beat"] services = sort(flatten([ diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 2e1dea019..81f740d8f 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -297,4 +297,9 @@ run "test_codebuild_manifests" { 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].name == "test" +# error_message = "Should be: ${jsonencode(local.pipeline_environments)}" +# } } From e12e9a7f7ad4b92b967974484e2e32d78044e476 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 12 Nov 2024 14:01:40 +0000 Subject: [PATCH 25/71] IAM permissions for ECS deploy --- codebase-pipelines/iam.tf | 55 ++++++++++++++++++++++++ codebase-pipelines/tests/unit.tftest.hcl | 15 +++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 79d0d2b48..96d0bf115 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -290,3 +290,58 @@ data "aws_iam_policy_document" "ecs_access_for_codebuild_manifests" { } } } + +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 { + actions = [ + "ecs:UpdateService" + ] + 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 { + actions = [ + "ecs:RunTask" + ] + resources = ["arn:aws:ecs:${local.account_region}:task-definition/${var.application}-${statement.value}-*:*"] + } + } + + statement { + actions = [ + "ecs:TagResource", + "ecs:RegisterTaskDefinition", + "ecs:DescribeTaskDefinition", + "ecs:DescribeServices", + "ecs:DescribeTasks", + "ecs:ListTasks" + ] + resources = ["*"] + } + + statement { + actions = [ + "iam:PassRole" + ] + resources = ["*"] + condition { + test = "StringLike" + values = ["ecs-tasks.amazonaws.com"] + variable = "iam:PassedToService" + } + } +} diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 81f740d8f..d570fb310 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -42,6 +42,13 @@ override_data { } } +override_data { + target = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline + values = { + json = "{\"Sid\": \"CodePipelineECSDeploy\"}" + } +} + variables { application = "my-app" @@ -298,8 +305,8 @@ run "test_codebuild_manifests" { error_message = "Should be: 'my-app-my-codebase-main-codebase-deploy-manifests'" } -# assert { -# condition = aws_codebuild_project.codebase_deploy_manifests[0].name == "test" -# error_message = "Should be: ${jsonencode(local.pipeline_environments)}" -# } + # assert { + # condition = aws_codebuild_project.codebase_deploy_manifests[0].name == "test" + # error_message = "Should be: ${jsonencode(local.pipeline_environments)}" + # } } From 9f5d0ae4b81a976e9d965858e8ab66a62a543077 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 12 Nov 2024 15:18:37 +0000 Subject: [PATCH 26/71] Event bridge trigger for pipelines --- codebase-pipelines/codepipeline.tf | 4 +- codebase-pipelines/eventbridge.tf | 63 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 codebase-pipelines/eventbridge.tf diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 1c998971c..da31f4cd8 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -4,6 +4,7 @@ resource "aws_codepipeline" "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" @@ -69,7 +70,8 @@ resource "aws_codepipeline" "codebase_pipeline" { dynamic "stage" { for_each = each.value.environments content { - name = stage.value.name + name = "Deploy-${stage.value.name}" + dynamic "action" { for_each = local.service_order_list diff --git a/codebase-pipelines/eventbridge.tf b/codebase-pipelines/eventbridge.tf new file mode 100644 index 000000000..b54527483 --- /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 = "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}-ecs-deploy-access-for-codebase-pipeline" + 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" + ] + } + } +} From 102857347110fb6484ba751b589314dac99deb4c Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 12 Nov 2024 15:47:38 +0000 Subject: [PATCH 27/71] Add approval stages --- codebase-pipelines/codebuild-images.tf | 2 +- codebase-pipelines/codepipeline.tf | 60 +++++++++++++++----------- codebase-pipelines/eventbridge.tf | 2 +- codebase-pipelines/variables.tf | 1 + 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/codebase-pipelines/codebuild-images.tf b/codebase-pipelines/codebuild-images.tf index 771f23d90..598f3d7fe 100644 --- a/codebase-pipelines/codebuild-images.tf +++ b/codebase-pipelines/codebuild-images.tf @@ -99,7 +99,7 @@ resource "aws_codebuild_webhook" "codebuild_webhook" { } dynamic "filter_group" { - for_each = local.tagged_pipeline ? [1] : [0] + for_each = local.tagged_pipeline ? [1] : [] content { filter { type = "EVENT" diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index da31f4cd8..c55d11ad8 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -1,9 +1,9 @@ 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" + 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 { @@ -26,12 +26,12 @@ resource "aws_codepipeline" "codebase_pipeline" { name = "Source" action { - name = "Source" - category = "Source" - owner = "AWS" - provider = "ECR" - version = "1" - namespace = "source_ecr" + name = "Source" + category = "Source" + owner = "AWS" + provider = "ECR" + version = "1" + namespace = "source_ecr" output_artifacts = ["source_output"] configuration = { @@ -45,14 +45,14 @@ resource "aws_codepipeline" "codebase_pipeline" { name = "Create-Deploy-Manifests" action { - name = "CreateManifests" - category = "Build" - owner = "AWS" - provider = "CodeBuild" - input_artifacts = ["source_output"] + name = "CreateManifests" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + input_artifacts = ["source_output"] output_artifacts = ["manifest_output"] - version = "1" - namespace = "build_manifest" + version = "1" + namespace = "build_manifest" configuration = { ProjectName = "${var.application}-${var.codebase}-${each.value.name}-codebase-deploy-manifests" @@ -73,16 +73,28 @@ resource "aws_codepipeline" "codebase_pipeline" { 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" + name = action.value.name + category = "Deploy" + owner = "AWS" + provider = "ECS" + version = "1" input_artifacts = ["manifest_output"] - run_order = action.value.order + 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, "-", "_"))}}" diff --git a/codebase-pipelines/eventbridge.tf b/codebase-pipelines/eventbridge.tf index b54527483..1666cb943 100644 --- a/codebase-pipelines/eventbridge.tf +++ b/codebase-pipelines/eventbridge.tf @@ -1,6 +1,6 @@ resource "aws_cloudwatch_event_rule" "ecr_image_publish" { for_each = local.pipeline_map - name = "ecr-image-publish-${each.value.name}" + 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({ diff --git a/codebase-pipelines/variables.tf b/codebase-pipelines/variables.tf index b1bb3a9a7..650996dfa 100644 --- a/codebase-pipelines/variables.tf +++ b/codebase-pipelines/variables.tf @@ -23,6 +23,7 @@ variable "pipelines" { environments = list(object( { name = string + requires_approval = optional(bool) } )) } From b121509943bb28e793b3cc883b4f55039cb46ff3 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 12 Nov 2024 16:22:02 +0000 Subject: [PATCH 28/71] Refactor codebuild jobs; Formatting --- codebase-pipelines/buildspec.yml | 48 --------- codebase-pipelines/codebuild-images.tf | 115 ---------------------- codebase-pipelines/codebuild-manifests.tf | 73 -------------- codebase-pipelines/codebuild.tf | 79 ++++++++++++++- codebase-pipelines/codepipeline.tf | 40 ++++---- codebase-pipelines/eventbridge.tf | 2 +- codebase-pipelines/tests/unit.tftest.hcl | 13 +++ codebase-pipelines/variables.tf | 2 +- 8 files changed, 112 insertions(+), 260 deletions(-) delete mode 100644 codebase-pipelines/buildspec.yml delete mode 100644 codebase-pipelines/codebuild-images.tf delete mode 100644 codebase-pipelines/codebuild-manifests.tf diff --git a/codebase-pipelines/buildspec.yml b/codebase-pipelines/buildspec.yml deleted file mode 100644 index abddc0b3f..000000000 --- a/codebase-pipelines/buildspec.yml +++ /dev/null @@ -1,48 +0,0 @@ -version: 0.2 - -env: - parameter-store: - SLACK_CHANNEL_ID: /codebuild/slack_oauth_channel - SLACK_TOKEN: /codebuild/slack_oauth_token - -phases: - install: - commands: - - > - if [ -f .copilot/phases/install.sh ]; then - bash .copilot/phases/install.sh; - fi - - pre_build: - commands: - - > - if [ -f .copilot/phases/pre_build.sh ]; then - bash .copilot/phases/pre_build.sh; - fi - - build: - commands: - - > - if [ -f .copilot/phases/build.sh ]; then - bash .copilot/phases/build.sh; - fi - - /work/cli build --publish --send-notifications - - post_build: - commands: - - > - if [ -f .copilot/phases/post_build.sh ]; then - bash .copilot/phases/post_build.sh; - fi - - if [ "${CODEBUILD_BUILD_SUCCEEDING}" != "1" ]; then - BUILD_ID_PREFIX=$(echo $CODEBUILD_BUILD_ID | cut -d':' -f1) - echo "BUILD_ID_PREFIX - ${BUILD_ID_PREFIX}" - - echo -e "\nInstalling dependencies" - pip install dbt-platform-helper - - MESSAGE=":no_entry::building_construction: Image build failure in codebuild project: " - - platform-helper notify add-comment "${SLACK_CHANNEL_ID}" "${SLACK_TOKEN}" "" "${MESSAGE}" - fi diff --git a/codebase-pipelines/codebuild-images.tf b/codebase-pipelines/codebuild-images.tf deleted file mode 100644 index 598f3d7fe..000000000 --- a/codebase-pipelines/codebuild-images.tf +++ /dev/null @@ -1,115 +0,0 @@ -data "aws_codestarconnections_connection" "github_codestar_connection" { - name = var.application -} - -resource "aws_codebuild_project" "codebase_image_build" { - name = "${var.application}-${var.codebase}-codebase-image-build" - description = "Publish images on push to ${var.repository}" - build_timeout = 30 - service_role = aws_iam_role.codebase_image_build.arn - badge_enabled = true - - artifacts { - type = "NO_ARTIFACTS" - } - - cache { - type = "LOCAL" - modes = ["LOCAL_DOCKER_LAYER_CACHE"] - } - - environment { - compute_type = "BUILD_GENERAL1_SMALL" - image = "public.ecr.aws/uktrade/ci-image-builder:tag-latest" - type = "LINUX_CONTAINER" - - environment_variable { - name = "AWS_ACCOUNT_ID" - value = data.aws_caller_identity.current.account_id - } - - environment_variable { - name = "ECR_REPOSITORY" - value = local.ecr_name - } - - environment_variable { - name = "CODESTAR_CONNECTION_ARN" - value = data.aws_codestarconnections_connection.github_codestar_connection.arn - } - - dynamic "environment_variable" { - for_each = var.additional_ecr_repository != null ? [1] : [] - content { - name = "ADDITIONAL_ECR_REPOSITORY" - value = var.additional_ecr_repository - } - } - } - - logs_config { - cloudwatch_logs { - group_name = aws_cloudwatch_log_group.codebase_image_build.name - stream_name = aws_cloudwatch_log_stream.codebase_image_build.name - } - } - - source { - type = "GITHUB" - buildspec = file("${path.module}/buildspec-images.yml") - location = "https://github.com/${var.repository}.git" - git_clone_depth = 0 - git_submodules_config { - fetch_submodules = false - } - } - - tags = local.tags -} - -resource "aws_cloudwatch_log_group" "codebase_image_build" { - # checkov:skip=CKV_AWS_338:Retains logs for 3 months instead of 1 year - # checkov:skip=CKV_AWS_158:To be reworked - name = "codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group" - retention_in_days = 90 -} - -resource "aws_cloudwatch_log_stream" "codebase_image_build" { - name = "codebuild/${var.application}-${var.codebase}-codebase-image-build/log-stream" - log_group_name = aws_cloudwatch_log_group.codebase_image_build.name -} - -resource "aws_codebuild_webhook" "codebuild_webhook" { - project_name = aws_codebuild_project.codebase_image_build.name - build_type = "BUILD" - - dynamic "filter_group" { - for_each = local.pipeline_branches - content { - filter { - type = "EVENT" - pattern = "PUSH" - } - - filter { - type = "HEAD_REF" - pattern = "^refs/heads/${filter_group.value}$" - } - } - } - - dynamic "filter_group" { - for_each = local.tagged_pipeline ? [1] : [] - content { - filter { - type = "EVENT" - pattern = "PUSH" - } - - filter { - type = "HEAD_REF" - pattern = "^refs/tags/.*" - } - } - } -} diff --git a/codebase-pipelines/codebuild-manifests.tf b/codebase-pipelines/codebuild-manifests.tf deleted file mode 100644 index ef54803fe..000000000 --- a/codebase-pipelines/codebuild-manifests.tf +++ /dev/null @@ -1,73 +0,0 @@ -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({ - Id = "key-default-1" - 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: To be reworked - 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/codebuild.tf b/codebase-pipelines/codebuild.tf index bd6600d66..00fe2aa28 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 { @@ -69,7 +69,7 @@ resource "aws_codebuild_project" "codebase_image_build" { resource "aws_cloudwatch_log_group" "codebase_image_build" { # 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 + # checkov:skip=CKV_AWS_158:To be reworked name = "codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group" retention_in_days = 90 } @@ -113,3 +113,78 @@ 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({ + Id = "key-default-1" + 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: To be reworked + 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 index c55d11ad8..5e7cc61f6 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -2,7 +2,7 @@ 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] + depends_on = [aws_iam_role_policy.artifact_store_access_for_codebase_pipeline] pipeline_type = "V2" execution_mode = "QUEUED" @@ -26,12 +26,12 @@ resource "aws_codepipeline" "codebase_pipeline" { name = "Source" action { - name = "Source" - category = "Source" - owner = "AWS" - provider = "ECR" - version = "1" - namespace = "source_ecr" + name = "Source" + category = "Source" + owner = "AWS" + provider = "ECR" + version = "1" + namespace = "source_ecr" output_artifacts = ["source_output"] configuration = { @@ -45,14 +45,14 @@ resource "aws_codepipeline" "codebase_pipeline" { name = "Create-Deploy-Manifests" action { - name = "CreateManifests" - category = "Build" - owner = "AWS" - provider = "CodeBuild" - input_artifacts = ["source_output"] + name = "CreateManifests" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + input_artifacts = ["source_output"] output_artifacts = ["manifest_output"] - version = "1" - namespace = "build_manifest" + version = "1" + namespace = "build_manifest" configuration = { ProjectName = "${var.application}-${var.codebase}-${each.value.name}-codebase-deploy-manifests" @@ -88,13 +88,13 @@ resource "aws_codepipeline" "codebase_pipeline" { dynamic "action" { for_each = local.service_order_list content { - name = action.value.name - category = "Deploy" - owner = "AWS" - provider = "ECS" - version = "1" + name = action.value.name + category = "Deploy" + owner = "AWS" + provider = "ECS" + version = "1" input_artifacts = ["manifest_output"] - run_order = action.value.order + 1 + 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, "-", "_"))}}" diff --git a/codebase-pipelines/eventbridge.tf b/codebase-pipelines/eventbridge.tf index 1666cb943..97a4a18c4 100644 --- a/codebase-pipelines/eventbridge.tf +++ b/codebase-pipelines/eventbridge.tf @@ -39,7 +39,7 @@ data "aws_iam_policy_document" "assume_event_bridge_policy" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["events.amazonaws.com"] } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index d570fb310..3179db9aa 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -49,6 +49,19 @@ override_data { } } +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" diff --git a/codebase-pipelines/variables.tf b/codebase-pipelines/variables.tf index 650996dfa..d79c7f1e2 100644 --- a/codebase-pipelines/variables.tf +++ b/codebase-pipelines/variables.tf @@ -22,7 +22,7 @@ variable "pipelines" { tag = optional(bool) environments = list(object( { - name = string + name = string requires_approval = optional(bool) } )) From 33f23a293bf6cede2827bcb6083e1791528dad5b Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 12 Nov 2024 17:54:25 +0000 Subject: [PATCH 29/71] Tighten ECS deploy permissions --- codebase-pipelines/iam.tf | 41 ++++++++++++---- codebase-pipelines/locals.tf | 2 - codebase-pipelines/tests/unit.tftest.hcl | 59 +++++++++++++++++++++++- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 14058ad16..8417dd3bf 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -303,10 +303,12 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { for_each = local.pipeline_environments content { actions = [ - "ecs:UpdateService" + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:TagResource" ] resources = [ - "arn:aws:ecs:${local.account_region}:cluster/${var.application}-${statement.value}/*", + "arn:aws:ecs:${local.account_region}:cluster/${var.application}-${statement.value}", "arn:aws:ecs:${local.account_region}:service/${var.application}-${statement.value}/*" ] } @@ -316,20 +318,43 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { for_each = local.pipeline_environments content { actions = [ - "ecs:RunTask" + "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 { + 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 { + actions = [ + "ecs:ListTasks" + ] + resources = [ + "arn:aws:ecs:${local.account_region}:container-instance/${var.application}-${statement.value}/*" + ] + } + } + statement { actions = [ - "ecs:TagResource", "ecs:RegisterTaskDefinition", - "ecs:DescribeTaskDefinition", - "ecs:DescribeServices", - "ecs:DescribeTasks", - "ecs:ListTasks" + "ecs:DescribeTaskDefinition" ] resources = ["*"] } diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index 8cb113f90..f77936b4b 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -16,7 +16,6 @@ locals { 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]]) - # ["web","api","celery-worker","celery-beat"] services = sort(flatten([ for run_group in var.services : [for service in flatten(values(run_group)) : service] ])) @@ -25,7 +24,6 @@ locals { for run_group in var.services : [for service in flatten(values(run_group)) : upper(replace(service, "-", "_"))] ])) - # [{"name":"web","order":1},{"name":"api","order":2},{"name":"celery-beat","order":2},{"name":"celery-worker","order":2}] service_order_list = flatten([ for index, group in var.services : [ for key, services in group : [ diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 3179db9aa..55c4ca53d 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -244,19 +244,69 @@ run "test_codebuild_images" { ] == 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/.*'" } } @@ -323,3 +373,8 @@ run "test_codebuild_manifests" { # error_message = "Should be: ${jsonencode(local.pipeline_environments)}" # } } + +run "test_event_bridge" { + command = plan + +} From 548a02282f201900f6133d492e0e900303169588 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Wed, 13 Nov 2024 12:37:18 +0000 Subject: [PATCH 30/71] Test manifest codebuild; Deploy pipeline; Event bridge trigger --- codebase-pipelines/codebuild.tf | 2 +- codebase-pipelines/codepipeline.tf | 3 +- codebase-pipelines/eventbridge.tf | 2 +- codebase-pipelines/iam.tf | 86 +-- codebase-pipelines/tests/unit.tftest.hcl | 631 ++++++++++++++++++++++- 5 files changed, 665 insertions(+), 59 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 00fe2aa28..cc0c2d0e0 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -155,7 +155,7 @@ resource "aws_codebuild_project" "codebase_deploy_manifests" { } resource "aws_kms_key" "codebuild_kms_key" { - description = "KMS Key for ${var.application}-${var.codebase} CodeBuild encryption" + description = "KMS Key for ${var.application} ${var.codebase} CodeBuild encryption" enable_key_rotation = true policy = jsonencode({ diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 5e7cc61f6..e1a728039 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -60,7 +60,7 @@ resource "aws_codepipeline" "codebase_pipeline" { { 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 : aws_ecr_repository.this.repository_url }, + { 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}" } ]) } @@ -72,7 +72,6 @@ resource "aws_codepipeline" "codebase_pipeline" { content { name = "Deploy-${stage.value.name}" - dynamic "action" { for_each = coalesce(stage.value.requires_approval, false) ? [1] : [] content { diff --git a/codebase-pipelines/eventbridge.tf b/codebase-pipelines/eventbridge.tf index 97a4a18c4..404c0e803 100644 --- a/codebase-pipelines/eventbridge.tf +++ b/codebase-pipelines/eventbridge.tf @@ -29,7 +29,7 @@ resource "aws_iam_role" "event_bridge_pipeline_trigger" { } resource "aws_iam_role_policy" "event_bridge_pipeline_trigger" { - name = "${var.application}-${var.codebase}-ecs-deploy-access-for-codebase-pipeline" + 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 } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 8417dd3bf..4b2ecd888 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -167,49 +167,6 @@ data "aws_iam_policy_document" "codestar_connection_access" { } } -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" "codebuild_manifests" { name = "${var.application}-${var.codebase}-codebase-codebuild-manifests" assume_role_policy = data.aws_iam_policy_document.assume_codebuild_role.json @@ -292,6 +249,49 @@ data "aws_iam_policy_document" "ecs_access_for_codebuild_manifests" { } } +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 diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 55c4ca53d..099e3ce9f 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -71,14 +71,12 @@ variables { services = [ { "run_group_1" : [ - "web" + "service-1" ] }, { "run_group_2" : [ - "api", - "celery-worker", - "celery-beat" + "service-2" ] } ] @@ -123,6 +121,31 @@ run "test_ecr" { } } +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 @@ -313,6 +336,7 @@ run "test_tagged_branch_filter" { 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'" @@ -349,32 +373,615 @@ 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'" } + + # 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 = 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'" + } + + # 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 = 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 = 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'" + } } -run "test_pipeline" { +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_codebuild_manifests" { +run "test_tagged_pipeline" { 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'" + 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" } - # assert { - # condition = aws_codebuild_project.codebase_deploy_manifests[0].name == "test" - # error_message = "Should be: ${jsonencode(local.pipeline_environments)}" - # } + # 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 + 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'" + } } + +# run "test_multiple_codebases" { +# command = plan +# +# assert { +# condition = aws_codepipeline.environment_pipeline.variable[0].default_value == "branch-main" +# error_message = "Should be: 'NONE'" +# } +# +# } From 9e913a092a1228fe84e2e7bd888f5bfcbcc09521 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Wed, 13 Nov 2024 13:02:51 +0000 Subject: [PATCH 31/71] Test run groups --- codebase-pipelines/tests/unit.tftest.hcl | 321 ++++++++++++++++++++++- 1 file changed, 311 insertions(+), 10 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 099e3ce9f..0e8aad659 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -917,7 +917,7 @@ run "test_tagged_pipeline" { run "test_event_bridge" { command = plan -# Main pipeline trigger + # 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'" @@ -976,12 +976,313 @@ run "test_event_bridge" { } } -# run "test_multiple_codebases" { -# command = plan -# -# assert { -# condition = aws_codepipeline.environment_pipeline.variable[0].default_value == "branch-main" -# error_message = "Should be: 'NONE'" -# } -# -# } +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" + } +} From 8a170107a01a50537f1598a6d9202f354bdff152 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Wed, 13 Nov 2024 13:10:05 +0000 Subject: [PATCH 32/71] Fix checkov comments --- codebase-pipelines/codebuild.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index cc0c2d0e0..07903a4cf 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -69,7 +69,7 @@ resource "aws_codebuild_project" "codebase_image_build" { resource "aws_cloudwatch_log_group" "codebase_image_build" { # checkov:skip=CKV_AWS_338:Retains logs for 3 months instead of 1 year - # checkov:skip=CKV_AWS_158:To be reworked + # checkov:skip=CKV_AWS_158:Log groups encrypted using default encryption key instead of KMS CMK name = "codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group" retention_in_days = 90 } @@ -179,7 +179,7 @@ resource "aws_kms_key" "codebuild_kms_key" { 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: To be reworked + # 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 } From e70227b9f4f5fcbe214571fef0ebf5207978811a Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 14 Nov 2024 11:48:45 +0000 Subject: [PATCH 33/71] Add codebase pipeline deploy role to be deployed with each environment in the extensions module --- extensions/iam.tf | 158 +++++++++++++++++++++++++++++++++++++++++++ extensions/locals.tf | 2 + 2 files changed, 160 insertions(+) create mode 100644 extensions/iam.tf diff --git a/extensions/iam.tf b/extensions/iam.tf new file mode 100644 index 000000000..1e267c0fa --- /dev/null +++ b/extensions/iam.tf @@ -0,0 +1,158 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +resource "aws_iam_role" "codebase_pipeline_deploy_role" { + name = "${var.args.application}-${var.environment}-codebase-pipeline-deploy-role" + assume_role_policy = data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.json + tags = local.tags +} + +data "aws_iam_policy_document" "codebase_deploy_pipeline_assume_role_policy" { + statement { + effect = "Allow" + + principals { + type = "AWS" + identifiers = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.args.application}-*-codebase-pipeline" + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role_policy" "ecr_access_for_codebase_pipeline" { + name = "${var.args.application}-ecr-access-for-codebase-pipeline" + role = aws_iam_role.codebase_pipeline_deploy_role.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 = [ + "arn:aws:ecr:${local.region_account}:repository/${var.args.application}/*" + ] + } +} + +resource "aws_iam_role_policy" "artifact_store_access_for_codebase_pipeline" { + name = "${var.args.application}-artifact-store-access-for-codebase-pipeline" + role = aws_iam_role.codebase_pipeline_deploy_role.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 = [ + "arn:aws:s3:::${var.args.application}-*-codebase-pipeline-artifact-store/*", + "arn:aws:s3:::${var.args.application}-*-codebase-pipeline-artifact-store" + ] + } + + statement { + effect = "Allow" + + actions = [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + ] + + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "kms:GenerateDataKey", + "kms:Decrypt" + ] + resources = [ + "arn:aws:kms:${local.region_account}:key/*" + ] + } +} + +resource "aws_iam_role_policy" "ecs_deploy_access_for_codebase_pipeline" { + name = "${var.args.application}-ecs-deploy-access-for-codebase-pipeline" + role = aws_iam_role.codebase_pipeline_deploy_role.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" { + statement { + actions = [ + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:TagResource" + ] + resources = [ + "arn:aws:ecs:${local.region_account}:cluster/${var.args.application}-${var.environment}", + "arn:aws:ecs:${local.region_account}:service/${var.args.application}-${var.environment}/*" + ] + } + + statement { + actions = [ + "ecs:DescribeTasks", + "ecs:TagResource" + ] + resources = [ + "arn:aws:ecs:${local.region_account}:cluster/${var.args.application}-${var.environment}", + "arn:aws:ecs:${local.region_account}:task/${var.args.application}-${var.environment}/*" + ] + } + + statement { + actions = [ + "ecs:RunTask", + "ecs:TagResource" + ] + resources = [ + "arn:aws:ecs:${local.region_account}:task-definition/${var.args.application}-${var.environment}-*:*" + ] + } + + statement { + actions = [ + "ecs:ListTasks" + ] + resources = [ + "arn:aws:ecs:${local.region_account}:container-instance/${var.args.application}-${var.environment}/*" + ] + } + + statement { + actions = [ + "ecs:RegisterTaskDefinition", + "ecs:DescribeTaskDefinition" + ] + resources = ["*"] + } + + statement { + actions = [ + "iam:PassRole" + ] + resources = ["*"] + condition { + test = "StringLike" + values = ["ecs-tasks.amazonaws.com"] + variable = "iam:PassedToService" + } + } +} diff --git a/extensions/locals.tf b/extensions/locals.tf index f17e15927..da168b2aa 100644 --- a/extensions/locals.tf +++ b/extensions/locals.tf @@ -42,4 +42,6 @@ locals { copilot-application = var.args.application copilot-environment = var.environment } + + region_account = "${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}" } From fe7ba12f3cec83166893f59d4ae8253417e07ab9 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 14 Nov 2024 11:49:36 +0000 Subject: [PATCH 34/71] Assume environment codebase pipeline deploy role; Add account config to pipeline map; Permissions for cross account deployment --- codebase-pipelines/artifactstore.tf | 2 +- codebase-pipelines/codepipeline.tf | 2 +- codebase-pipelines/iam.tf | 102 +++-------------------- codebase-pipelines/locals.tf | 25 +++++- codebase-pipelines/tests/unit.tftest.hcl | 55 ++++++++---- codebase-pipelines/variables.tf | 4 + 6 files changed, 80 insertions(+), 110 deletions(-) diff --git a/codebase-pipelines/artifactstore.tf b/codebase-pipelines/artifactstore.tf index 5687ff656..80b960af2 100644 --- a/codebase-pipelines/artifactstore.tf +++ b/codebase-pipelines/artifactstore.tf @@ -55,7 +55,7 @@ resource "aws_kms_key" "artifact_store_kms_key" { "Sid" : "Enable IAM User Permissions", "Effect" : "Allow", "Principal" : { - "AWS" : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + "AWS" : [for id in local.deploy_account_ids : "arn:aws:iam::${id}:root"] }, "Action" : "kms:*", "Resource" : "*" diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index e1a728039..c91cf15e2 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -2,7 +2,6 @@ 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" @@ -99,6 +98,7 @@ resource "aws_codepipeline" "codebase_pipeline" { ServiceName = "#{build_manifest.SERVICE_NAME_${upper(stage.value.name)}_${upper(replace(action.value.name, "-", "_"))}}" FileName = "image-definitions-${action.value.name}.json" } + role_arn = stage.value.name == "hotfix" ? "arn:aws:iam::891377058512:role/demodjango-prod-codebase-pipeline-assume-role" : null } } } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 4b2ecd888..272298cff 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -243,7 +243,7 @@ data "aws_iam_policy_document" "ecs_access_for_codebuild_manifests" { "ecs:ListServices" ] resources = [ - "arn:aws:ecs:${local.account_region}:service/${var.application}-${statement.value}/*" + "arn:aws:ecs:${local.account_region}:service/${var.application}-${statement.value.name}/*" ] } } @@ -268,106 +268,26 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { } } -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 +resource "aws_iam_role_policy" "assume_codebase_pipeline_environment_deploy_role" { + for_each = toset([for env in local.pipeline_environments : env.name]) + name = "${var.application}-${var.codebase}-assume-${each.value}-codebase-pipeline-deploy-role" + role = aws_iam_role.codebase_deploy_pipeline.name + policy = data.aws_iam_policy_document.assume_codebase_pipeline_environment_deploy_role.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 { - 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 { - 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}/*" - ] - } - } - +data "aws_iam_policy_document" "assume_codebase_pipeline_environment_deploy_role" { dynamic "statement" { for_each = local.pipeline_environments - content { - 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" + "sts:AssumeRole" ] resources = [ - "arn:aws:ecs:${local.account_region}:container-instance/${var.application}-${statement.value}/*" + # TODO: Deploy this role to each environment + "arn:aws:iam::${statement.value.account.id}:role/${var.application}-${statement.value.name}-codebase-pipeline-deploy-role" ] } } - - statement { - actions = [ - "ecs:RegisterTaskDefinition", - "ecs:DescribeTaskDefinition" - ] - resources = ["*"] - } - - statement { - 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 f77936b4b..28f6e2ace 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -8,13 +8,34 @@ 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 - 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]]) + base_env_config = { + for name, config in var.env_config : name => merge(lookup(var.env_config, "*", {}), config) if name != "*" + } + + deploy_account_ids = distinct([for env in local.base_env_config : env.accounts.deploy.id]) + + pipeline_environment_account_map = { + for id, val in var.pipelines : id => { + "environments" : [ + for name, env in val.environments : merge(env, { + "account" : lookup(local.base_env_config, env.name, {}).accounts.deploy + }) + ] + } + } + + pipeline_map = { + for id, val in var.pipelines : id => merge(val, local.pipeline_environment_account_map[id]) + } + + pipeline_environments = flatten([for pipeline in local.pipeline_map : [for env in pipeline.environments : env]]) services = sort(flatten([ for run_group in var.services : [for service in flatten(values(run_group)) : service] diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 0e8aad659..d0ddff6ca 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -43,27 +43,47 @@ override_data { } override_data { - target = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline + target = data.aws_iam_policy_document.assume_event_bridge_policy values = { - json = "{\"Sid\": \"CodePipelineECSDeploy\"}" + json = "{\"Sid\": \"AssumeEventBridge\"}" } } override_data { - target = data.aws_iam_policy_document.assume_event_bridge_policy + target = data.aws_iam_policy_document.event_bridge_pipeline_trigger values = { - json = "{\"Sid\": \"AssumeEventBridge\"}" + json = "{\"Sid\": \"EventBridgePipelineTrigger\"}" } } override_data { - target = data.aws_iam_policy_document.event_bridge_pipeline_trigger + target = data.aws_iam_policy_document.assume_codebase_pipeline_environment_deploy_role values = { - json = "{\"Sid\": \"EventBridgePipelineTrigger\"}" + json = "{\"Sid\": \"AssumeEnvironmentDeployRole\"}" } } variables { + env_config = { + "*" = { + accounts = { + deploy = { + name = "sandbox" + id = "000123456789" + } + } + }, + "dev" = null, + "staging" = null, + "prod" = { + accounts = { + deploy = { + name = "prod" + id = "123456789000" + } + } + } + } application = "my-app" codebase = "my-codebase" repository = "my-repository" @@ -426,27 +446,27 @@ run "test_iam" { error_message = "Should be: ${jsonencode(var.expected_tags)}" } 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'" + condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["dev"].name == "my-app-my-codebase-assume-dev-codebase-pipeline-deploy-role" + error_message = "Should be: 'my-app-my-codebase-assume-dev-codebase-pipeline-deploy-role'" } assert { - condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" + condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["dev"].role == "my-app-my-codebase-codebase-pipeline" error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" } 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'" + condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["staging"].name == "my-app-my-codebase-assume-staging-codebase-pipeline-deploy-role" + error_message = "Should be: 'my-app-my-codebase-assume-staging-codebase-pipeline-deploy-role'" } assert { - condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" + condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["staging"].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'" + condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["prod"].name == "my-app-my-codebase-assume-prod-codebase-pipeline-deploy-role" + error_message = "Should be: 'my-app-my-codebase-assume-prod-codebase-pipeline-deploy-role'" } assert { - condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" + condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["prod"].role == "my-app-my-codebase-codebase-pipeline" error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" } } @@ -1285,4 +1305,9 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[4].run_order == 4 error_message = "Run order incorrect" } + +# assert { +# condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[4].run_order == 10 +# error_message = jsonencode(local.test) +# } } diff --git a/codebase-pipelines/variables.tf b/codebase-pipelines/variables.tf index d79c7f1e2..0d3044704 100644 --- a/codebase-pipelines/variables.tf +++ b/codebase-pipelines/variables.tf @@ -33,3 +33,7 @@ variable "pipelines" { variable "services" { type = any } + +variable "env_config" { + type = any +} From 3b103b0a9dfea2b37b68087049afd5e6e8d2eb47 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 14 Nov 2024 13:49:34 +0000 Subject: [PATCH 35/71] Fix permssions --- extensions/iam.tf | 27 ++++++++++++++------------- extensions/locals.tf | 2 -- extensions/variables.tf | 1 + 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/extensions/iam.tf b/extensions/iam.tf index 1e267c0fa..ed90720a0 100644 --- a/extensions/iam.tf +++ b/extensions/iam.tf @@ -10,12 +10,15 @@ resource "aws_iam_role" "codebase_pipeline_deploy_role" { data "aws_iam_policy_document" "codebase_deploy_pipeline_assume_role_policy" { statement { effect = "Allow" - principals { type = "AWS" - identifiers = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.args.application}-*-codebase-pipeline" + identifiers = ["arn:aws:iam::${var.args.pipeline_account_id}:root"] + } + condition { + test = "StringEquals" + values = ["codepipeline.amazonaws.com"] + variable = "aws:UserAgent" } - actions = ["sts:AssumeRole"] } } @@ -33,7 +36,7 @@ data "aws_iam_policy_document" "ecr_access_for_codebase_pipeline" { "ecr:DescribeImages" ] resources = [ - "arn:aws:ecr:${local.region_account}:repository/${var.args.application}/*" + "arn:aws:ecr:${var.args.pipeline_account_id}:repository/${var.args.application}/*" ] } } @@ -49,7 +52,6 @@ data "aws_iam_policy_document" "access_artifact_store" { # checkov:skip=CKV_AWS_356:Permissions required to upload artifacts statement { effect = "Allow" - actions = [ "s3:GetObject", "s3:GetObjectVersion", @@ -57,7 +59,6 @@ data "aws_iam_policy_document" "access_artifact_store" { "s3:PutObjectAcl", "s3:PutObject", ] - resources = [ "arn:aws:s3:::${var.args.application}-*-codebase-pipeline-artifact-store/*", "arn:aws:s3:::${var.args.application}-*-codebase-pipeline-artifact-store" @@ -82,7 +83,7 @@ data "aws_iam_policy_document" "access_artifact_store" { "kms:Decrypt" ] resources = [ - "arn:aws:kms:${local.region_account}:key/*" + "arn:aws:kms:${data.aws_region.current.name}:${var.args.pipeline_account_id}:key/*" ] } } @@ -101,8 +102,8 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { "ecs:TagResource" ] resources = [ - "arn:aws:ecs:${local.region_account}:cluster/${var.args.application}-${var.environment}", - "arn:aws:ecs:${local.region_account}:service/${var.args.application}-${var.environment}/*" + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/${var.args.application}-${var.environment}", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/${var.args.application}-${var.environment}/*" ] } @@ -112,8 +113,8 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { "ecs:TagResource" ] resources = [ - "arn:aws:ecs:${local.region_account}:cluster/${var.args.application}-${var.environment}", - "arn:aws:ecs:${local.region_account}:task/${var.args.application}-${var.environment}/*" + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/${var.args.application}-${var.environment}", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task/${var.args.application}-${var.environment}/*" ] } @@ -123,7 +124,7 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { "ecs:TagResource" ] resources = [ - "arn:aws:ecs:${local.region_account}:task-definition/${var.args.application}-${var.environment}-*:*" + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task-definition/${var.args.application}-${var.environment}-*:*" ] } @@ -132,7 +133,7 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { "ecs:ListTasks" ] resources = [ - "arn:aws:ecs:${local.region_account}:container-instance/${var.args.application}-${var.environment}/*" + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:container-instance/${var.args.application}-${var.environment}/*" ] } diff --git a/extensions/locals.tf b/extensions/locals.tf index da168b2aa..f17e15927 100644 --- a/extensions/locals.tf +++ b/extensions/locals.tf @@ -42,6 +42,4 @@ locals { copilot-application = var.args.application copilot-environment = var.environment } - - region_account = "${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}" } diff --git a/extensions/variables.tf b/extensions/variables.tf index 728455e8d..56cd45bc5 100644 --- a/extensions/variables.tf +++ b/extensions/variables.tf @@ -3,6 +3,7 @@ variable "args" { application = string, services = any, dns_account_id = string + pipeline_account_id = string }) } From 99aab6228b51baaa6b891c4a3abd5d3037f578e9 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 14 Nov 2024 18:48:53 +0000 Subject: [PATCH 36/71] Update env deploy permissions --- extensions/iam.tf | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/extensions/iam.tf b/extensions/iam.tf index ed90720a0..06ef59494 100644 --- a/extensions/iam.tf +++ b/extensions/iam.tf @@ -11,13 +11,16 @@ data "aws_iam_policy_document" "codebase_deploy_pipeline_assume_role_policy" { statement { effect = "Allow" principals { - type = "AWS" + type = "AWS" identifiers = ["arn:aws:iam::${var.args.pipeline_account_id}:root"] } condition { - test = "StringEquals" - values = ["codepipeline.amazonaws.com"] - variable = "aws:UserAgent" + test = "StringLike" + values = [ + "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline", + "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline-deploy-manifests" + ] + variable = "aws:PrincipalArn" } actions = ["sts:AssumeRole"] } @@ -36,7 +39,7 @@ data "aws_iam_policy_document" "ecr_access_for_codebase_pipeline" { "ecr:DescribeImages" ] resources = [ - "arn:aws:ecr:${var.args.pipeline_account_id}:repository/${var.args.application}/*" + "arn:aws:ecr:${data.aws_region.current.name}:${var.args.pipeline_account_id}:repository/${var.args.application}/*" ] } } @@ -99,7 +102,8 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { actions = [ "ecs:UpdateService", "ecs:DescribeServices", - "ecs:TagResource" + "ecs:TagResource", + "ecs:ListServices" ] resources = [ "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/${var.args.application}-${var.environment}", From 716a41de1c49d360c48fcb5de2aff62b00c0c211 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 14 Nov 2024 18:49:57 +0000 Subject: [PATCH 37/71] Assume env deploy role in pipeline and codebuild --- codebase-pipelines/buildspec-manifests.yml | 10 +++++++++- codebase-pipelines/codepipeline.tf | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index 9cd0f16b7..d099585fe 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -9,8 +9,16 @@ phases: build: commands: - set -e - - for env in $(echo $ENVIRONMENTS | jq -c -r '.[]'); + - for env in $(echo $ENVIRONMENTS | jq -c -r '.[].name'); do + unset AWS_ACCESS_KEY_ID; + unset AWS_SECRET_ACCESS_KEY; + unset AWS_SESSION_TOKEN; + ACCOUNT_ID=$(echo $ENVIRONMENTS | jq -c -r '.[] | select(.name=='\"$env\"').account.id'); + assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::$ACCOUNT_ID:role/$APPLICATION-$env-codebase-pipeline-deploy-role" --role-session-name "$env-codebase-pipeline-deploy"); + export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId); + export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey); + export AWS_SESSION_TOKEN=$(echo $assumed_role | jq -r .Credentials.SessionToken); EXPORT_ENV=$(echo $env | tr '[:lower:]' '[:upper:]'); export CLUSTER_NAME_$EXPORT_ENV="$APPLICATION-$env"; for svc in $(echo $SERVICES | jq -c -r '.[]'); diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index c91cf15e2..263eaa4c5 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -57,7 +57,7 @@ resource "aws_codepipeline" "codebase_pipeline" { 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 : "ENVIRONMENTS", value : jsonencode([for env in each.value.environments : env]) }, { 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}" } @@ -98,7 +98,7 @@ resource "aws_codepipeline" "codebase_pipeline" { ServiceName = "#{build_manifest.SERVICE_NAME_${upper(stage.value.name)}_${upper(replace(action.value.name, "-", "_"))}}" FileName = "image-definitions-${action.value.name}.json" } - role_arn = stage.value.name == "hotfix" ? "arn:aws:iam::891377058512:role/demodjango-prod-codebase-pipeline-assume-role" : null + role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy-role" } } } From 7f968c8cf2ff6844d08fc55ee4f86846a0ada1a9 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 14 Nov 2024 18:50:54 +0000 Subject: [PATCH 38/71] Allow deploy role artifact bucket access; Refactor permssions --- codebase-pipelines/artifactstore.tf | 26 ++++-- codebase-pipelines/codebuild.tf | 2 +- codebase-pipelines/iam.tf | 89 ++++++++++--------- codebase-pipelines/tests/unit.tftest.hcl | 104 ++++++++--------------- 4 files changed, 103 insertions(+), 118 deletions(-) diff --git a/codebase-pipelines/artifactstore.tf b/codebase-pipelines/artifactstore.tf index 80b960af2..904ee88a8 100644 --- a/codebase-pipelines/artifactstore.tf +++ b/codebase-pipelines/artifactstore.tf @@ -12,16 +12,13 @@ resource "aws_s3_bucket" "artifact_store" { data "aws_iam_policy_document" "artifact_store_bucket_policy" { statement { principals { - type = "*" + type = "*" identifiers = ["*"] } - actions = [ - "s3:*", + "s3:*" ] - effect = "Deny" - condition { test = "Bool" variable = "aws:SecureTransport" @@ -30,7 +27,24 @@ data "aws_iam_policy_document" "artifact_store_bucket_policy" { "false", ] } + resources = [ + aws_s3_bucket.artifact_store.arn, + "${aws_s3_bucket.artifact_store.arn}/*", + ] + } + statement { + effect = "Allow" + principals { + type = "AWS" + identifiers = [ + for env in local.pipeline_environments : + "arn:aws:iam::${env.account.id}:role/${var.application}-${env.name}-codebase-pipeline-deploy-role" + ] + } + actions = [ + "s3:*" + ] resources = [ aws_s3_bucket.artifact_store.arn, "${aws_s3_bucket.artifact_store.arn}/*", @@ -66,7 +80,7 @@ resource "aws_kms_key" "artifact_store_kms_key" { } resource "aws_kms_alias" "artifact_store_kms_alias" { - depends_on = [aws_kms_key.artifact_store_kms_key] + 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 } diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 07903a4cf..fb6fc6b1b 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -120,7 +120,7 @@ resource "aws_codebuild_project" "codebase_deploy_manifests" { 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 + service_role = aws_iam_role.codebase_deploy_manifests.arn encryption_key = aws_kms_key.artifact_store_kms_key.arn artifacts { diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 272298cff..7d6161df8 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -2,7 +2,7 @@ data "aws_caller_identity" "current" {} data "aws_region" "current" {} resource "aws_iam_role" "codebase_image_build" { - name = "${var.application}-${var.codebase}-codebase-image-build" + name = "${var.application}-${var.codebase}-codebase-pipeline-image-build" assume_role_policy = data.aws_iam_policy_document.assume_codebuild_role.json tags = local.tags } @@ -12,7 +12,7 @@ data "aws_iam_policy_document" "assume_codebuild_role" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["codebuild.amazonaws.com"] } @@ -25,8 +25,8 @@ resource "aws_iam_role_policy_attachment" "ssm_access" { policy_arn = "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess" } -resource "aws_iam_role_policy" "log_access_for_codebuild_images" { - name = "${var.application}-${var.codebase}-log-access-for-codebuild-images" +resource "aws_iam_role_policy" "log_access_for_codebase_image_build" { + name = "${var.application}-${var.codebase}-log-access-for-codebase-pipeline-image-build" role = aws_iam_role.codebase_image_build.name policy = data.aws_iam_policy_document.log_access_for_codebuild.json } @@ -66,13 +66,13 @@ data "aws_iam_policy_document" "log_access_for_codebuild" { } } -resource "aws_iam_role_policy" "ecr_access_for_codebuild_images" { - name = "${var.application}-${var.codebase}-ecr-access-for-codebuild-images" +resource "aws_iam_role_policy" "ecr_access_for_codebase_image_build" { + name = "${var.application}-${var.codebase}-ecr-access-for-codebase-pipeline-image-build" role = aws_iam_role.codebase_image_build.name - policy = data.aws_iam_policy_document.ecr_access_for_codebuild_images.json + policy = data.aws_iam_policy_document.ecr_access_for_codebase_image_build.json } -data "aws_iam_policy_document" "ecr_access_for_codebuild_images" { +data "aws_iam_policy_document" "ecr_access_for_codebase_image_build" { statement { effect = "Allow" actions = [ @@ -167,15 +167,15 @@ data "aws_iam_policy_document" "codestar_connection_access" { } } -resource "aws_iam_role" "codebuild_manifests" { - name = "${var.application}-${var.codebase}-codebase-codebuild-manifests" +resource "aws_iam_role" "codebase_deploy_manifests" { + name = "${var.application}-${var.codebase}-codebase-pipeline-deploy-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 + name = "${var.application}-${var.codebase}-artifact-store-access-for-codebase-pipeline-deploy-manifests" + role = aws_iam_role.codebase_deploy_manifests.name policy = data.aws_iam_policy_document.access_artifact_store.json } @@ -223,30 +223,15 @@ data "aws_iam_policy_document" "access_artifact_store" { } 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 + name = "${var.application}-${var.codebase}-log-access-for-codebase-pipeline-deploy-manifests" + role = aws_iam_role.codebase_deploy_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.name}/*" - ] - } - } +resource "aws_iam_role_policy" "codebuild_assume_environment_deploy_role" { + name = "${var.application}-${var.codebase}-environment-deploy-role-access-for-codebase-pipeline-deploy-manifests" + role = aws_iam_role.codebase_deploy_manifests.name + policy = data.aws_iam_policy_document.assume_environment_deploy_role.json } resource "aws_iam_role" "codebase_deploy_pipeline" { @@ -260,7 +245,7 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["codepipeline.amazonaws.com"] } @@ -268,14 +253,37 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { } } -resource "aws_iam_role_policy" "assume_codebase_pipeline_environment_deploy_role" { - for_each = toset([for env in local.pipeline_environments : env.name]) - name = "${var.application}-${var.codebase}-assume-${each.value}-codebase-pipeline-deploy-role" - role = aws_iam_role.codebase_deploy_pipeline.name - policy = data.aws_iam_policy_document.assume_codebase_pipeline_environment_deploy_role.json +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" "pipeline_assume_environment_deploy_role" { + name = "${var.application}-${var.codebase}-assume-environment-codebase-pipeline-deploy-role" + role = aws_iam_role.codebase_deploy_pipeline.name + policy = data.aws_iam_policy_document.assume_environment_deploy_role.json } -data "aws_iam_policy_document" "assume_codebase_pipeline_environment_deploy_role" { +data "aws_iam_policy_document" "assume_environment_deploy_role" { dynamic "statement" { for_each = local.pipeline_environments @@ -285,7 +293,6 @@ data "aws_iam_policy_document" "assume_codebase_pipeline_environment_deploy_role "sts:AssumeRole" ] resources = [ - # TODO: Deploy this role to each environment "arn:aws:iam::${statement.value.account.id}:role/${var.application}-${statement.value.name}-codebase-pipeline-deploy-role" ] } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index d0ddff6ca..744961ad4 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -15,7 +15,7 @@ override_data { } override_data { - target = data.aws_iam_policy_document.ecr_access_for_codebuild_images + target = data.aws_iam_policy_document.ecr_access_for_codebase_image_build values = { json = "{\"Sid\": \"CodeBuildImageECRAccess\"}" } @@ -35,13 +35,6 @@ override_data { } } -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.assume_event_bridge_policy values = { @@ -57,7 +50,7 @@ override_data { } override_data { - target = data.aws_iam_policy_document.assume_codebase_pipeline_environment_deploy_role + target = data.aws_iam_policy_document.assume_environment_deploy_role values = { json = "{\"Sid\": \"AssumeEnvironmentDeployRole\"}" } @@ -358,8 +351,8 @@ run "test_iam" { # 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'" + condition = aws_iam_role.codebase_image_build.name == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { condition = aws_iam_role.codebase_image_build.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" @@ -370,66 +363,58 @@ run "test_iam" { error_message = "Should be: ${jsonencode(var.expected_tags)}" } 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'" + condition = aws_iam_role_policy.log_access_for_codebase_image_build.name == "my-app-my-codebase-log-access-for-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-log-access-for-codebase-pipeline-image-build'" } 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'" + condition = aws_iam_role_policy.log_access_for_codebase_image_build.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } 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'" + condition = aws_iam_role_policy.ecr_access_for_codebase_image_build.name == "my-app-my-codebase-ecr-access-for-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-ecr-access-for-codebase-pipeline-image-build'" } 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'" + condition = aws_iam_role_policy.ecr_access_for_codebase_image_build.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { condition = aws_iam_role_policy.codestar_connection_access.name == "codestar-connection-policy" error_message = "Should be: 'codestar-connection-policy'" } assert { - 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'" + condition = aws_iam_role_policy.codestar_connection_access.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } # 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'" + condition = aws_iam_role.codebase_deploy_manifests.name == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" } assert { - condition = aws_iam_role.codebuild_manifests.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" + condition = aws_iam_role.codebase_deploy_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) + condition = jsonencode(aws_iam_role.codebase_deploy_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 = 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'" + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.name == "my-app-my-codebase-artifact-store-access-for-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-artifact-store-access-for-codebase-pipeline-deploy-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'" + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-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'" + condition = aws_iam_role_policy.log_access_for_codebuild_manifests.name == "my-app-my-codebase-log-access-for-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-log-access-for-codebase-pipeline-deploy-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'" + condition = aws_iam_role_policy.log_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" } # CodePipeline @@ -446,27 +431,11 @@ run "test_iam" { error_message = "Should be: ${jsonencode(var.expected_tags)}" } assert { - condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["dev"].name == "my-app-my-codebase-assume-dev-codebase-pipeline-deploy-role" - error_message = "Should be: 'my-app-my-codebase-assume-dev-codebase-pipeline-deploy-role'" - } - assert { - condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["dev"].role == "my-app-my-codebase-codebase-pipeline" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" - } - assert { - condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["staging"].name == "my-app-my-codebase-assume-staging-codebase-pipeline-deploy-role" - error_message = "Should be: 'my-app-my-codebase-assume-staging-codebase-pipeline-deploy-role'" + condition = aws_iam_role_policy.pipeline_assume_environment_deploy_role.name == "my-app-my-codebase-assume-environment-codebase-pipeline-deploy-role" + error_message = "Should be: 'my-app-my-codebase-assume-environment-codebase-pipeline-deploy-role'" } assert { - condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["staging"].role == "my-app-my-codebase-codebase-pipeline" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" - } - assert { - condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["prod"].name == "my-app-my-codebase-assume-prod-codebase-pipeline-deploy-role" - error_message = "Should be: 'my-app-my-codebase-assume-prod-codebase-pipeline-deploy-role'" - } - assert { - condition = aws_iam_role_policy.assume_codebase_pipeline_environment_deploy_role["prod"].role == "my-app-my-codebase-codebase-pipeline" + condition = aws_iam_role_policy.pipeline_assume_environment_deploy_role.role == "my-app-my-codebase-codebase-pipeline" error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" } } @@ -692,7 +661,7 @@ run "test_main_pipeline" { 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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" } @@ -803,7 +772,7 @@ run "test_tagged_pipeline" { 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"staging\\\",\\\"requires_approval\\\":null},{\\\"account\\\":{\\\"id\\\":\\\"123456789000\\\",\\\"name\\\":\\\"prod\\\"},\\\"name\\\":\\\"prod\\\",\\\"requires_approval\\\":true}]\"},{\"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" } @@ -1013,7 +982,7 @@ run "test_pipeline_single_run_group" { } 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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" } @@ -1090,7 +1059,7 @@ run "test_pipeline_multiple_run_groups" { } 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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" } @@ -1200,7 +1169,7 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { } 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null},{\\\"account\\\":{\\\"id\\\":\\\"123456789000\\\",\\\"name\\\":\\\"prod\\\"},\\\"name\\\":\\\"prod\\\",\\\"requires_approval\\\":true}]\"},{\"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" } @@ -1305,9 +1274,4 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[4].run_order == 4 error_message = "Run order incorrect" } - -# assert { -# condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[4].run_order == 10 -# error_message = jsonencode(local.test) -# } } From 7e1926380714e173b24bb721cb73c9ffa0fb256e Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 14 Nov 2024 18:51:15 +0000 Subject: [PATCH 39/71] Formatting --- codebase-pipelines/artifactstore.tf | 6 +++--- codebase-pipelines/iam.tf | 4 ++-- extensions/iam.tf | 6 +++--- extensions/variables.tf | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/codebase-pipelines/artifactstore.tf b/codebase-pipelines/artifactstore.tf index 904ee88a8..7ca78dc5c 100644 --- a/codebase-pipelines/artifactstore.tf +++ b/codebase-pipelines/artifactstore.tf @@ -12,7 +12,7 @@ resource "aws_s3_bucket" "artifact_store" { data "aws_iam_policy_document" "artifact_store_bucket_policy" { statement { principals { - type = "*" + type = "*" identifiers = ["*"] } actions = [ @@ -36,7 +36,7 @@ data "aws_iam_policy_document" "artifact_store_bucket_policy" { statement { effect = "Allow" principals { - type = "AWS" + type = "AWS" identifiers = [ for env in local.pipeline_environments : "arn:aws:iam::${env.account.id}:role/${var.application}-${env.name}-codebase-pipeline-deploy-role" @@ -80,7 +80,7 @@ resource "aws_kms_key" "artifact_store_kms_key" { } resource "aws_kms_alias" "artifact_store_kms_alias" { - depends_on = [aws_kms_key.artifact_store_kms_key] + 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 } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 7d6161df8..19bc8855c 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -12,7 +12,7 @@ data "aws_iam_policy_document" "assume_codebuild_role" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["codebuild.amazonaws.com"] } @@ -245,7 +245,7 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["codepipeline.amazonaws.com"] } diff --git a/extensions/iam.tf b/extensions/iam.tf index 06ef59494..e6223f47d 100644 --- a/extensions/iam.tf +++ b/extensions/iam.tf @@ -11,11 +11,11 @@ data "aws_iam_policy_document" "codebase_deploy_pipeline_assume_role_policy" { statement { effect = "Allow" principals { - type = "AWS" + type = "AWS" identifiers = ["arn:aws:iam::${var.args.pipeline_account_id}:root"] } condition { - test = "StringLike" + test = "StringLike" values = [ "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline", "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline-deploy-manifests" @@ -156,7 +156,7 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { resources = ["*"] condition { test = "StringLike" - values = ["ecs-tasks.amazonaws.com"] + values = ["ecs-tasks.amazonaws.com"] variable = "iam:PassedToService" } } diff --git a/extensions/variables.tf b/extensions/variables.tf index 56cd45bc5..92fd67dc5 100644 --- a/extensions/variables.tf +++ b/extensions/variables.tf @@ -1,8 +1,8 @@ variable "args" { type = object({ - application = string, - services = any, - dns_account_id = string + application = string, + services = any, + dns_account_id = string pipeline_account_id = string }) } From 3c7db1e6bc0fc42c990e321220c1b721b9e2e2eb Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 15 Nov 2024 10:56:44 +0000 Subject: [PATCH 40/71] Unit tests for stage role_arn and extensions IAM role --- codebase-pipelines/tests/unit.tftest.hcl | 74 +++++++++++++++++++-- extensions/tests/unit.tftest.hcl | 85 +++++++++++++++++++++++- 2 files changed, 153 insertions(+), 6 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 744961ad4..8f900ab51 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -146,7 +146,7 @@ run "test_artifact_store" { 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 + condition = [for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[0].condition : el.variable][0] == "aws:SecureTransport" error_message = "Should be: aws:SecureTransport" } assert { @@ -154,7 +154,19 @@ run "test_artifact_store" { 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 + condition = [for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[0].actions : el][0] == "s3:*" + error_message = "Should be: s3:*" + } + assert { + condition = [for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[1].principals : el.type][0] == "AWS" + error_message = "Should be: AWS" + } + assert { + condition = flatten([for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[1].principals : el.identifiers]) == ["arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy-role", "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy-role", "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy-role"] + error_message = "Bucket policy principals incorrect" + } + assert { + condition = [for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[1].actions : el][0] == "s3:*" error_message = "Should be: s3:*" } } @@ -416,6 +428,14 @@ run "test_iam" { condition = aws_iam_role_policy.log_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" } + assert { + condition = aws_iam_role_policy.codebuild_assume_environment_deploy_role.name == "my-app-my-codebase-environment-deploy-role-access-for-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-environment-deploy-role-access-for-codebase-pipeline-deploy-manifests'" + } + assert { + condition = aws_iam_role_policy.codebuild_assume_environment_deploy_role.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" + } # CodePipeline assert { @@ -430,6 +450,22 @@ run "test_iam" { condition = jsonencode(aws_iam_role.codebase_deploy_pipeline.tags) == jsonencode(var.expected_tags) error_message = "Should be: ${jsonencode(var.expected_tags)}" } + 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 = 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.pipeline_assume_environment_deploy_role.name == "my-app-my-codebase-assume-environment-codebase-pipeline-deploy-role" error_message = "Should be: 'my-app-my-codebase-assume-environment-codebase-pipeline-deploy-role'" @@ -511,12 +547,10 @@ run "test_codebuild_manifests" { 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)}" @@ -704,6 +738,10 @@ run "test_main_pipeline" { 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].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy-role" + error_message = "Role ARN incorrect" + } assert { condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_DEV}" error_message = "Configuration ClusterName incorrect" @@ -726,6 +764,10 @@ run "test_main_pipeline" { 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].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy-role" + error_message = "Role ARN incorrect" + } assert { condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_DEV}" error_message = "Configuration ClusterName incorrect" @@ -791,6 +833,10 @@ run "test_tagged_pipeline" { 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].role_arn == "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy-role" + error_message = "Role ARN incorrect" + } assert { condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_STAGING}" error_message = "Configuration ClusterName incorrect" @@ -813,6 +859,10 @@ run "test_tagged_pipeline" { 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].role_arn == "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy-role" + error_message = "Role ARN incorrect" + } assert { condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_STAGING}" error_message = "Configuration ClusterName incorrect" @@ -867,6 +917,10 @@ run "test_tagged_pipeline" { 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].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy-role" + error_message = "Role ARN incorrect" + } assert { condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_PROD}" error_message = "Configuration ClusterName incorrect" @@ -889,6 +943,10 @@ run "test_tagged_pipeline" { 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].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy-role" + error_message = "Role ARN incorrect" + } assert { condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_PROD}" error_message = "Configuration ClusterName incorrect" @@ -1188,6 +1246,10 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { 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].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy-role" + error_message = "Role ARN incorrect" + } # service-2 assert { @@ -1244,6 +1306,10 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[1].run_order == 2 error_message = "Run order incorrect" } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[1].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy-role" + error_message = "Role ARN incorrect" + } # service-2 assert { diff --git a/extensions/tests/unit.tftest.hcl b/extensions/tests/unit.tftest.hcl index 449b291b5..dee712277 100644 --- a/extensions/tests/unit.tftest.hcl +++ b/extensions/tests/unit.tftest.hcl @@ -33,7 +33,8 @@ variables { } } }, - dns_account_id = "123456" + dns_account_id = "123456" + pipeline_account_id = "000123456789" } application = "test-application" environment = "test-environment" @@ -214,7 +215,8 @@ run "opensearch_plan_medium_ha_service_test" { } } }, - dns_account_id = "123456" + dns_account_id = "123456" + pipeline_account_id = "000123456789" } environment = "test-environment" vpc_name = "test-vpc" @@ -257,3 +259,82 @@ run "opensearch_plan_medium_ha_service_test" { error_message = "Should be: 512" } } + +override_data { + target = data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy + values = { + json = "{\"Sid\": \"CodeBaseDeployAssumeRole\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline + values = { + json = "{\"Sid\": \"ECSDeployAccess\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.access_artifact_store + values = { + json = "{\"Sid\": \"ArtifactStoreAccess\"}" + } +} + +override_data { + target = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline + values = { + json = "{\"Sid\": \"ECSDeployAccess\"}" + } +} + +run "codebase_deploy_iam_test" { + command = plan + + variables { + expected_tags = { + application = var.args.application + environment = var.environment + managed-by = "DBT Platform - Terraform" + copilot-application = var.args.application + copilot-environment = var.environment + } + } + + assert { + condition = aws_iam_role.codebase_pipeline_deploy_role.name == "test-application-test-environment-codebase-pipeline-deploy-role" + error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" + } + assert { + condition = aws_iam_role.codebase_pipeline_deploy_role.assume_role_policy == "{\"Sid\": \"CodeBaseDeployAssumeRole\"}" + error_message = "Should be: {\"Sid\": \"CodeBaseDeployAssumeRole\"}" + } + assert { + condition = jsonencode(aws_iam_role.codebase_pipeline_deploy_role.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.name == "test-application-ecr-access-for-codebase-pipeline" + error_message = "Should be: 'test-application-ecr-access-for-codebase-pipeline'" + } + assert { + condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" + error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.name == "test-application-artifact-store-access-for-codebase-pipeline" + error_message = "Should be: 'test-application-artifact-store-access-for-codebase-pipeline'" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" + error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" + } + assert { + condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.name == "test-application-ecs-deploy-access-for-codebase-pipeline" + error_message = "Should be: 'test-application-ecs-deploy-access-for-codebase-pipeline'" + } + assert { + condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" + error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" + } +} From bae0d4d990df0b0a8a58dcbbd3c02b41ecb27cbc Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 15 Nov 2024 11:43:03 +0000 Subject: [PATCH 41/71] Fix linting and validate error --- example/main.tf | 1 + example/pipelines.yml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/example/main.tf b/example/main.tf index 70860b2e1..747df280c 100644 --- a/example/main.tf +++ b/example/main.tf @@ -3,6 +3,7 @@ locals { application = "my-application" services = yamldecode(file("${path.module}/extensions.yml")) dns_account_id = one([for env in yamldecode(file("${path.module}/pipelines.yml"))["environments"] : env if env["name"] == "my-environment"])["accounts"]["dns"]["id"] + pipeline_account_id = one([for env in yamldecode(file("${path.module}/pipelines.yml"))["environments"] : env if env["name"] == "default"])["accounts"]["deploy"]["id"] } } diff --git a/example/pipelines.yml b/example/pipelines.yml index 21b8ca057..3faeda8df 100644 --- a/example/pipelines.yml +++ b/example/pipelines.yml @@ -1,4 +1,12 @@ environments: + - name: default + accounts: + deploy: + name: "sandbox" + id: "000123456789" + dns: + name: "dev" + id: "000123456789" - name: my-environment accounts: deploy: From a4627be1382c848ed891c3c90f29520c576f0032 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 15 Nov 2024 11:46:11 +0000 Subject: [PATCH 42/71] Formatting --- example/main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/main.tf b/example/main.tf index 747df280c..7b000b861 100644 --- a/example/main.tf +++ b/example/main.tf @@ -1,8 +1,8 @@ locals { args = { - application = "my-application" - services = yamldecode(file("${path.module}/extensions.yml")) - dns_account_id = one([for env in yamldecode(file("${path.module}/pipelines.yml"))["environments"] : env if env["name"] == "my-environment"])["accounts"]["dns"]["id"] + application = "my-application" + services = yamldecode(file("${path.module}/extensions.yml")) + dns_account_id = one([for env in yamldecode(file("${path.module}/pipelines.yml"))["environments"] : env if env["name"] == "my-environment"])["accounts"]["dns"]["id"] pipeline_account_id = one([for env in yamldecode(file("${path.module}/pipelines.yml"))["environments"] : env if env["name"] == "default"])["accounts"]["deploy"]["id"] } } From 9c5bea2851aed03d4d9f0b7b947d440797c22b9e Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 15 Nov 2024 14:00:46 +0000 Subject: [PATCH 43/71] Only write one set of service deploy manifests --- codebase-pipelines/buildspec-manifests.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index d099585fe..f073fbf3f 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -23,11 +23,13 @@ phases: 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; + if [ ! -f image-definitions-$svc.json ]; then + 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; + fi done done From 34c6b76308a053fbdf4e624c89cf51e5387db83b Mon Sep 17 00:00:00 2001 From: James Francis Date: Fri, 15 Nov 2024 15:01:07 +0000 Subject: [PATCH 44/71] fix bug in manifests buildspec --- codebase-pipelines/buildspec-manifests.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index f073fbf3f..d099585fe 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -23,13 +23,11 @@ phases: export CLUSTER_NAME_$EXPORT_ENV="$APPLICATION-$env"; for svc in $(echo $SERVICES | jq -c -r '.[]'); do - if [ ! -f image-definitions-$svc.json ]; then - 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; - fi + 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 From ba342932aa287de53b3fd6b06945fa58b2c0792a Mon Sep 17 00:00:00 2001 From: James Francis Date: Fri, 15 Nov 2024 15:01:41 +0000 Subject: [PATCH 45/71] only use one codebuild for each codebase --- codebase-pipelines/codebuild.tf | 5 +- codebase-pipelines/codepipeline.tf | 116 ++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index fb6fc6b1b..3bd356d0c 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -116,8 +116,7 @@ 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" + name = "${var.application}-${var.codebase}-codebase-deploy-manifests" description = "Create image deploy manifests to deploy services" build_timeout = 5 service_role = aws_iam_role.codebase_deploy_manifests.arn @@ -148,7 +147,7 @@ resource "aws_codebuild_project" "codebase_deploy_manifests" { 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 }) + buildspec = templatefile("${path.module}/buildspec-manifests.yml", { application = var.application, environments = [for env in local.pipeline_environments : upper(env.name)], services = local.service_export_names }) } tags = local.tags diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 263eaa4c5..a2b4d52cb 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -54,7 +54,7 @@ resource "aws_codepipeline" "codebase_pipeline" { namespace = "build_manifest" configuration = { - ProjectName = "${var.application}-${var.codebase}-${each.value.name}-codebase-deploy-manifests" + ProjectName = "${var.application}-${var.codebase}-codebase-deploy-manifests" EnvironmentVariables : jsonencode([ { name : "APPLICATION", value : var.application }, { name : "ENVIRONMENTS", value : jsonencode([for env in each.value.environments : env]) }, @@ -106,3 +106,117 @@ resource "aws_codepipeline" "codebase_pipeline" { tags = local.tags } + + +# resource "aws_codepipeline" "manual_release_pipeline" { +# name = "${var.application}-${var.codebase}-manual-release-pipeline" +# role_arn = aws_iam_role.codebase_deploy_pipeline.arn +# pipeline_type = "V2" +# execution_mode = "QUEUED" +# +# variable { +# name = "IMAGE_TAG" +# default_value = "NONE" +# description = "Tagged image in ECR to deploy" +# } +# +# variable { +# name = "ENVIRONMENT_NAME" +# default_value = "NONE" +# description = "Envrionment to deploy to" +# } +# +# 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 +# } +# } +# } +# +# 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 : "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" +# } +# role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy-role" +# } +# } +# } +# } +# +# tags = local.tags +# } From 1b1a8c44820cdc721c3713f33ebe8dfc3bfb60f6 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 15 Nov 2024 15:01:42 +0000 Subject: [PATCH 46/71] Fix manifest bug --- codebase-pipelines/buildspec-manifests.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index f073fbf3f..d099585fe 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -23,13 +23,11 @@ phases: export CLUSTER_NAME_$EXPORT_ENV="$APPLICATION-$env"; for svc in $(echo $SERVICES | jq -c -r '.[]'); do - if [ ! -f image-definitions-$svc.json ]; then - 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; - fi + 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 From 57a825100ae2625c597d3135c6407f22639f2d4c Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 15 Nov 2024 16:31:04 +0000 Subject: [PATCH 47/71] Only output one deploy manifest per service --- codebase-pipelines/buildspec-manifests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index d099585fe..becdf4e0d 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -23,10 +23,12 @@ phases: 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; + if [ ! -f image-definitions-$svc.json ]; then + echo '[{"name":"'$svc'","imageUri":"'$REPOSITORY_URL':'$IMAGE_TAG'"}]' > image-definitions-$svc.json; + fi 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); + export SERVICE_NAME_$EXPORT_SVC=$(echo $SERVICE_NAME | cut -d '/' -f3); cat image-definitions-$svc.json; done done From be324346b016805896af25eea92d16fcf107a2c5 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 15 Nov 2024 17:16:05 +0000 Subject: [PATCH 48/71] Fix tests for codebuild project --- codebase-pipelines/codepipeline.tf | 2 +- codebase-pipelines/tests/unit.tftest.hcl | 40 ++++++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index a2b4d52cb..c1efd9353 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -54,7 +54,7 @@ resource "aws_codepipeline" "codebase_pipeline" { namespace = "build_manifest" configuration = { - ProjectName = "${var.application}-${var.codebase}-codebase-deploy-manifests" + ProjectName = aws_codebuild_project.codebase_deploy_manifests.name EnvironmentVariables : jsonencode([ { name : "APPLICATION", value : var.application }, { name : "ENVIRONMENTS", value : jsonencode([for env in each.value.environments : env]) }, diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 8f900ab51..6e88f0a52 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -480,67 +480,67 @@ 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'" + condition = aws_codebuild_project.codebase_deploy_manifests.name == "my-app-my-codebase-codebase-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-deploy-manifests'" } assert { - condition = aws_codebuild_project.codebase_deploy_manifests[0].description == "Create image deploy manifests to deploy services" + condition = aws_codebuild_project.codebase_deploy_manifests.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 + condition = aws_codebuild_project.codebase_deploy_manifests.build_timeout == 5 error_message = "Should be: 5" } assert { - condition = one(aws_codebuild_project.codebase_deploy_manifests[0].artifacts).type == "CODEPIPELINE" + condition = one(aws_codebuild_project.codebase_deploy_manifests.artifacts).type == "CODEPIPELINE" error_message = "Should be: 'CODEPIPELINE'" } assert { - condition = one(aws_codebuild_project.codebase_deploy_manifests[0].cache).type == "S3" + condition = one(aws_codebuild_project.codebase_deploy_manifests.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" + condition = one(aws_codebuild_project.codebase_deploy_manifests.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" + condition = one(aws_codebuild_project.codebase_deploy_manifests.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" + condition = one(aws_codebuild_project.codebase_deploy_manifests.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" + condition = one(aws_codebuild_project.codebase_deploy_manifests.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" + condition = one(aws_codebuild_project.codebase_deploy_manifests.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[ + condition = aws_codebuild_project.codebase_deploy_manifests.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[ + condition = aws_codebuild_project.codebase_deploy_manifests.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" + condition = one(aws_codebuild_project.codebase_deploy_manifests.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 + condition = length(regexall(".*\"exported-variables\":\\[\"CLUSTER_NAME_DEV\".*", aws_codebuild_project.codebase_deploy_manifests.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) + condition = jsonencode(aws_codebuild_project.codebase_deploy_manifests.tags) == jsonencode(var.expected_tags) error_message = "Should be: ${jsonencode(var.expected_tags)}" } assert { @@ -691,8 +691,8 @@ run "test_main_pipeline" { 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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-codebase-deploy-manifests" + error_message = "Should be: my-app-my-codebase-codebase-deploy-manifests" } assert { condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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}\"}]" @@ -810,8 +810,8 @@ run "test_tagged_pipeline" { 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" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-codebase-deploy-manifests" + error_message = "Should be: my-app-my-codebase-codebase-deploy-manifests" } assert { condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"staging\\\",\\\"requires_approval\\\":null},{\\\"account\\\":{\\\"id\\\":\\\"123456789000\\\",\\\"name\\\":\\\"prod\\\"},\\\"name\\\":\\\"prod\\\",\\\"requires_approval\\\":true}]\"},{\"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}\"}]" From 7fd8cb272231a35eda7e6f1d549695d46be68616 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 18 Nov 2024 12:22:20 +0000 Subject: [PATCH 49/71] Allow service manifest codebuild to export variables for any environment; Add release-pipeline config --- codebase-pipelines/buildspec-manifests.yml | 13 +- codebase-pipelines/codebuild.tf | 12 +- codebase-pipelines/codepipeline.tf | 215 ++++++++++----------- codebase-pipelines/tests/unit.tftest.hcl | 4 + 4 files changed, 126 insertions(+), 118 deletions(-) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index d099585fe..d9c56cea0 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -2,19 +2,26 @@ version: 0.2 env: ${jsonencode({ - exported-variables: flatten([ for env in environments : ["CLUSTER_NAME_${env}", [ for svc in services: "SERVICE_NAME_${env}_${svc}" ]]]) + exported-variables: flatten([ for env in environments : [ + "CLUSTER_NAME", + "CLUSTER_NAME_${env}", + [ for svc in services: [ + "SERVICE_NAME_${svc}", + "SERVICE_NAME_${env}_${svc}" + ]] + ]]) })} phases: build: commands: - set -e - - for env in $(echo $ENVIRONMENTS | jq -c -r '.[].name'); + - for env in $(echo $ENVIRONMENTS | jq -c -r '.[]'); do unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN; - ACCOUNT_ID=$(echo $ENVIRONMENTS | jq -c -r '.[] | select(.name=='\"$env\"').account.id'); + ACCOUNT_ID=$(echo $ENV_CONFIG | jq -c -r .$env.accounts.deploy.id); assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::$ACCOUNT_ID:role/$APPLICATION-$env-codebase-pipeline-deploy-role" --role-session-name "$env-codebase-pipeline-deploy"); export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId); export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey); diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 3bd356d0c..b411308ff 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -136,6 +136,10 @@ resource "aws_codebuild_project" "codebase_deploy_manifests" { image = "aws/codebuild/amazonlinux2-x86_64-standard:5.0" type = "LINUX_CONTAINER" image_pull_credentials_type = "CODEBUILD" + environment_variable { + name = "ENV_CONFIG" + value = jsonencode(local.base_env_config) + } } logs_config { @@ -146,8 +150,12 @@ resource "aws_codebuild_project" "codebase_deploy_manifests" { } source { - type = "CODEPIPELINE" - buildspec = templatefile("${path.module}/buildspec-manifests.yml", { application = var.application, environments = [for env in local.pipeline_environments : upper(env.name)], services = local.service_export_names }) + type = "CODEPIPELINE" + buildspec = templatefile("${path.module}/buildspec-manifests.yml", { + application = var.application, environments = [ + for env in local.pipeline_environments : upper(env.name) + ], services = local.service_export_names + }) } tags = local.tags diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index c1efd9353..b92d9c438 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -57,7 +57,7 @@ resource "aws_codepipeline" "codebase_pipeline" { ProjectName = aws_codebuild_project.codebase_deploy_manifests.name EnvironmentVariables : jsonencode([ { name : "APPLICATION", value : var.application }, - { name : "ENVIRONMENTS", value : jsonencode([for env in each.value.environments : env]) }, + { 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}" } @@ -108,115 +108,104 @@ resource "aws_codepipeline" "codebase_pipeline" { } -# resource "aws_codepipeline" "manual_release_pipeline" { -# name = "${var.application}-${var.codebase}-manual-release-pipeline" -# role_arn = aws_iam_role.codebase_deploy_pipeline.arn -# pipeline_type = "V2" -# execution_mode = "QUEUED" -# -# variable { -# name = "IMAGE_TAG" -# default_value = "NONE" -# description = "Tagged image in ECR to deploy" -# } -# -# variable { -# name = "ENVIRONMENT_NAME" -# default_value = "NONE" -# description = "Envrionment to deploy to" -# } -# -# 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 -# } -# } -# } -# -# 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 : "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" -# } -# role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy-role" -# } -# } -# } -# } -# -# tags = local.tags -# } +resource "aws_codepipeline" "manual_release_pipeline" { + name = "${var.application}-${var.codebase}-manual-release-pipeline" + role_arn = aws_iam_role.codebase_deploy_pipeline.arn + pipeline_type = "V2" + execution_mode = "QUEUED" + + variable { + name = "IMAGE_TAG" + default_value = "NONE" + description = "Tagged image in ECR to deploy" + } + + variable { + name = "ENVIRONMENT" + default_value = "NONE" + description = "Name of the environment to deploy to" + } + + 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 + } + } + } + + 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 = aws_codebuild_project.codebase_deploy_manifests.name + EnvironmentVariables : jsonencode([ + { name : "APPLICATION", value : var.application }, + { name : "ENVIRONMENTS", value : "[\"#{variables.ENVIRONMENT}\"]" }, + { 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}" } + ]) + } + } + } + + stage { + name = "Deploy" + + 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}" + ServiceName = "#{build_manifest.SERVICE_NAME_${upper(replace(action.value.name, "-", "_"))}}" + FileName = "image-definitions-${action.value.name}.json" + } + # role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy-role" + # TODO This is going to need to be a role capable of deploying any environment in the account + } + } + } + + tags = local.tags +} diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 6e88f0a52..fb4ad4d1d 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -132,6 +132,10 @@ run "test_ecr" { condition = jsonencode(aws_ecr_repository.this.tags) == jsonencode(var.expected_ecr_tags) error_message = "Should be: ${jsonencode(var.expected_ecr_tags)}" } + assert { + condition = jsonencode(aws_ecr_repository.this.tags) == false + error_message = "Should be: ${jsonencode(jsonencode(local.base_env_config))}" + } } run "test_artifact_store" { From 49c46938123a6987524901a44ae147cd8a5280b2 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 18 Nov 2024 12:22:57 +0000 Subject: [PATCH 50/71] Formatting --- codebase-pipelines/codepipeline.tf | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index b92d9c438..2fda4e149 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -173,10 +173,7 @@ resource "aws_codepipeline" "manual_release_pipeline" { { name : "APPLICATION", value : var.application }, { name : "ENVIRONMENTS", value : "[\"#{variables.ENVIRONMENT}\"]" }, { 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 : "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}" } ]) } @@ -202,7 +199,7 @@ resource "aws_codepipeline" "manual_release_pipeline" { FileName = "image-definitions-${action.value.name}.json" } # role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy-role" - # TODO This is going to need to be a role capable of deploying any environment in the account + # TODO This is going to need to be a role capable of deploying any environment in the account, how will that work cross-account? } } } From 8a4b2f26fcad68dc59da4e485ae975ee7ad56d0c Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 28 Nov 2024 09:58:31 +0000 Subject: [PATCH 51/71] Fix IAM tests --- codebase-pipelines/codebuild.tf | 6 +- codebase-pipelines/iam.tf | 10 +- codebase-pipelines/tests/unit.tftest.hcl | 340 +++-------------------- 3 files changed, 40 insertions(+), 316 deletions(-) diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index 0c5f5e0a1..42c73994e 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -3,7 +3,7 @@ data "aws_codestarconnections_connection" "github_codestar_connection" { } resource "aws_codebuild_project" "codebase_image_build" { - name = "${var.application}-${var.codebase}-codebase-image-build" + name = "${var.application}-${var.codebase}-codebase-pipeline-image-build" description = "Publish images on push to ${var.repository}" build_timeout = 30 service_role = aws_iam_role.codebase_image_build.arn @@ -117,10 +117,10 @@ 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" + name = "${var.application}-${var.codebase}-${each.value.name}-codebase-pipeline-deploy-manifests" description = "Create image deploy manifests to deploy services" build_timeout = 5 - service_role = aws_iam_role.codebuild_manifests.arn + service_role = aws_iam_role.codebase_deploy_manifests.arn encryption_key = aws_kms_key.artifact_store_kms_key.arn artifacts { diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index df0e583f0..27a3bc994 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -59,9 +59,9 @@ data "aws_iam_policy_document" "log_access_for_codebuild" { "codebuild:BatchPutCodeCoverages" ] resources = [ - "arn:aws:codebuild:${local.account_region}:report-group/${aws_codebuild_project.codebase_image_build.name}-*", + "arn:aws:codebuild:${local.account_region}:report-group/${var.application}-${var.codebase}-codebase-pipeline-image-build-*", "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-*" + "arn:aws:codebuild:${local.account_region}:report-group/${var.application}-${var.codebase}-*-codebase-pipeline-deploy-manifests-*" ] } } @@ -167,15 +167,15 @@ data "aws_iam_policy_document" "codestar_connection_access" { } } -resource "aws_iam_role" "codebuild_manifests" { - name = "${var.application}-${var.codebase}-codebase-codebuild-manifests" +resource "aws_iam_role" "codebase_deploy_manifests" { + name = "${var.application}-${var.codebase}-codebase-pipeline-deploy-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 + role = aws_iam_role.codebase_deploy_manifests.name policy = data.aws_iam_policy_document.access_artifact_store.json } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 5430ac3ac..bb8b6a609 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -175,8 +175,8 @@ run "test_codebuild_images" { command = plan assert { - condition = aws_codebuild_project.codebase_image_build.name == "my-app-my-codebase-codebase-image-build" - error_message = "Should be: my-app-my-codebase-codebase-image-build" + condition = aws_codebuild_project.codebase_image_build.name == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: my-app-my-codebase-codebase-pipeline-image-build" } assert { condition = aws_codebuild_project.codebase_image_build.description == "Publish images on push to my-repository" @@ -271,8 +271,8 @@ run "test_codebuild_images" { # Webhook config: assert { - condition = aws_codebuild_webhook.codebuild_webhook.project_name == "my-app-my-codebase-codebase-image-build" - error_message = "Should be: 'my-app-my-codebase-codebase-image-build'" + condition = aws_codebuild_webhook.codebuild_webhook.project_name == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { condition = aws_codebuild_webhook.codebuild_webhook.build_type == "BUILD" @@ -363,8 +363,8 @@ run "test_iam" { # 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'" + condition = aws_iam_role.codebase_image_build.name == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { condition = aws_iam_role.codebase_image_build.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" @@ -395,8 +395,8 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy.log_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[0].effect == "Allow" @@ -422,19 +422,19 @@ run "test_iam" { } 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/my-app-my-codebase-*-codebase-pipeline-deploy-manifests-*", + "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/my-app-my-codebase-codebase-pipeline-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" + error_message = "Unexpected resources ${jsonencode(data.aws_iam_policy_document.log_access_for_codebuild.statement[1].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'" + condition = aws_iam_role_policy.ecr_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[0].effect == "Allow" @@ -523,8 +523,8 @@ run "test_iam" { error_message = "Should be: 'codestar-connection-policy'" } assert { - 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'" + condition = aws_iam_role_policy.codestar_connection_access.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { condition = data.aws_iam_policy_document.codestar_connection_access.statement[0].effect == "Allow" @@ -538,8 +538,8 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy_attachment.ssm_access.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { condition = aws_iam_role_policy_attachment.ssm_access.policy_arn == "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess" @@ -548,15 +548,15 @@ run "test_iam" { # 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'" + condition = aws_iam_role.codebase_deploy_manifests.name == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" } assert { - condition = aws_iam_role.codebuild_manifests.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" + condition = aws_iam_role.codebase_deploy_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) + condition = jsonencode(aws_iam_role.codebase_deploy_manifests.tags) == jsonencode(var.expected_tags) error_message = "Should be: ${jsonencode(var.expected_tags)}" } assert { @@ -564,8 +564,8 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" } assert { condition = data.aws_iam_policy_document.access_artifact_store.statement[0].effect == "Allow" @@ -612,40 +612,8 @@ run "test_iam" { 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" + condition = aws_iam_role_policy.log_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" } # CodePipeline @@ -701,258 +669,14 @@ run "test_iam" { 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'" + condition = aws_codebuild_project.codebase_deploy_manifests[0].name == "my-app-my-codebase-main-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-main-codebase-pipeline-deploy-manifests'" } assert { condition = aws_codebuild_project.codebase_deploy_manifests[0].description == "Create image deploy manifests to deploy services" @@ -1168,7 +892,7 @@ run "test_main_pipeline" { 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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" } @@ -1287,7 +1011,7 @@ run "test_tagged_pipeline" { 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"staging\\\",\\\"requires_approval\\\":null},{\\\"account\\\":{\\\"id\\\":\\\"123456789000\\\",\\\"name\\\":\\\"prod\\\"},\\\"name\\\":\\\"prod\\\",\\\"requires_approval\\\":true}]\"},{\"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" } @@ -1547,7 +1271,7 @@ run "test_pipeline_single_run_group" { } 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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" } @@ -1628,7 +1352,7 @@ run "test_pipeline_multiple_run_groups" { } 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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" } @@ -1738,7 +1462,7 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { } 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null},{\\\"account\\\":{\\\"id\\\":\\\"123456789000\\\",\\\"name\\\":\\\"prod\\\"},\\\"name\\\":\\\"prod\\\",\\\"requires_approval\\\":true}]\"},{\"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" } From 75caef16f5c47d1ed0b630e07019ce0138a41267 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 28 Nov 2024 10:59:46 +0000 Subject: [PATCH 52/71] Update extensions tests --- extensions/iam.tf | 6 + extensions/tests/unit.tftest.hcl | 196 +++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/extensions/iam.tf b/extensions/iam.tf index e6223f47d..c3bd1acb9 100644 --- a/extensions/iam.tf +++ b/extensions/iam.tf @@ -99,6 +99,7 @@ resource "aws_iam_role_policy" "ecs_deploy_access_for_codebase_pipeline" { data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { statement { + effect = "Allow" actions = [ "ecs:UpdateService", "ecs:DescribeServices", @@ -112,6 +113,7 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { } statement { + effect = "Allow" actions = [ "ecs:DescribeTasks", "ecs:TagResource" @@ -123,6 +125,7 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { } statement { + effect = "Allow" actions = [ "ecs:RunTask", "ecs:TagResource" @@ -133,6 +136,7 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { } statement { + effect = "Allow" actions = [ "ecs:ListTasks" ] @@ -142,6 +146,7 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { } statement { + effect = "Allow" actions = [ "ecs:RegisterTaskDefinition", "ecs:DescribeTaskDefinition" @@ -150,6 +155,7 @@ data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { } statement { + effect = "Allow" actions = [ "iam:PassRole" ] diff --git a/extensions/tests/unit.tftest.hcl b/extensions/tests/unit.tftest.hcl index dee712277..f611ab31b 100644 --- a/extensions/tests/unit.tftest.hcl +++ b/extensions/tests/unit.tftest.hcl @@ -309,6 +309,34 @@ run "codebase_deploy_iam_test" { condition = aws_iam_role.codebase_pipeline_deploy_role.assume_role_policy == "{\"Sid\": \"CodeBaseDeployAssumeRole\"}" error_message = "Should be: {\"Sid\": \"CodeBaseDeployAssumeRole\"}" } + assert { + condition = data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].actions) == "sts:AssumeRole" + error_message = "Should be: sts:AssumeRole" + } + assert { + condition = one(data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].principals).type == "AWS" + error_message = "Should be: AWS" + } + assert { + condition = contains(one(data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].principals).identifiers, "arn:aws:iam::000123456789:root") + error_message = "Should contain: arn:aws:iam::000123456789:root" + } + assert { + condition = [for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.test][0] == "StringLike" + error_message = "Should be: StringLike" + } + assert { + condition = [for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.variable][0] == "aws:PrincipalArn" + error_message = "Should be: aws:PrincipalArn" + } + assert { + condition = flatten([for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.values][0]) == ["arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline", "arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline-deploy-manifests"] + error_message = "Unexpected condition values ${jsonencode([for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.values][0])}" + } assert { condition = jsonencode(aws_iam_role.codebase_pipeline_deploy_role.tags) == jsonencode(var.expected_tags) error_message = "Should be: ${jsonencode(var.expected_tags)}" @@ -321,6 +349,18 @@ run "codebase_deploy_iam_test" { condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" } + 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 = one(data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.statement[0].resources) == "arn:aws:ecr:${data.aws_region.current.name}:000123456789:repository/test-application/*" + error_message = "Unexpected resources" + } assert { condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.name == "test-application-artifact-store-access-for-codebase-pipeline" error_message = "Should be: 'test-application-artifact-store-access-for-codebase-pipeline'" @@ -329,6 +369,54 @@ run "codebase_deploy_iam_test" { condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" } + 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[0].resources == toset(["arn:aws:s3:::test-application-*-codebase-pipeline-artifact-store", "arn:aws:s3:::test-application-*-codebase-pipeline-artifact-store/*"]) + error_message = "Unexpected resources" + } + 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 = one(data.aws_iam_policy_document.access_artifact_store.statement[2].resources) == "arn:aws:kms:${data.aws_region.current.name}:000123456789:key/*" + error_message = "Unexpected resources" + } assert { condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.name == "test-application-ecs-deploy-access-for-codebase-pipeline" error_message = "Should be: 'test-application-ecs-deploy-access-for-codebase-pipeline'" @@ -337,4 +425,112 @@ run "codebase_deploy_iam_test" { condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" } + assert { + condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.policy == "{\"Sid\": \"ECSDeployAccess\"}" + error_message = "Should be: {\"Sid\": \"ECSDeployAccess\"}" + } + 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", + "ecs:ListServices" + ]) + 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/test-application-test-environment", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/test-application-test-environment/*" + ]) + 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:DescribeTasks", + "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/test-application-test-environment", + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task/test-application-test-environment/*" + ]) + 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:RunTask", + "ecs:TagResource" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[2].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task-definition/test-application-test-environment-*:*" + 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 = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[3].actions) == "ecs:ListTasks" + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[3].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:container-instance/test-application-test-environment/*" + 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:DescribeTaskDefinition", + "ecs:RegisterTaskDefinition" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[4].resources) == "*" + 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 = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].actions) == "iam:PassRole" + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].resources) == "*" + error_message = "Unexpected resources" + } + assert { + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].condition : el.test][0] == "StringLike" + error_message = "Should be: StringLike" + } + assert { + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].condition : one(el.values)][0] == "ecs-tasks.amazonaws.com" + error_message = "Should be: ecs-tasks.amazonaws.com" + } + assert { + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].condition : el.variable][0] == "iam:PassedToService" + error_message = "Should be: iam:PassedToService" + } } From b403a054ddf3278767ccc1c4292ba7bd943af66d Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 28 Nov 2024 11:07:45 +0000 Subject: [PATCH 53/71] Fix test error message --- codebase-pipelines/tests/unit.tftest.hcl | 2 +- extensions/tests/unit.tftest.hcl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index bb8b6a609..249363c80 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -426,7 +426,7 @@ run "test_iam" { "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/my-app-my-codebase-codebase-pipeline-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 ${jsonencode(data.aws_iam_policy_document.log_access_for_codebuild.statement[1].resources)}" + 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" diff --git a/extensions/tests/unit.tftest.hcl b/extensions/tests/unit.tftest.hcl index f611ab31b..bda1ea917 100644 --- a/extensions/tests/unit.tftest.hcl +++ b/extensions/tests/unit.tftest.hcl @@ -335,7 +335,7 @@ run "codebase_deploy_iam_test" { } assert { condition = flatten([for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.values][0]) == ["arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline", "arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline-deploy-manifests"] - error_message = "Unexpected condition values ${jsonencode([for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.values][0])}" + error_message = "Unexpected condition values" } assert { condition = jsonencode(aws_iam_role.codebase_pipeline_deploy_role.tags) == jsonencode(var.expected_tags) From b87b8fe139a9d0f821137360895431882ab16e49 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 16 Dec 2024 10:23:11 +0000 Subject: [PATCH 54/71] Refactor environment IAM permissions --- extensions/iam.tf | 38 ++++----- extensions/tests/unit.tftest.hcl | 136 ++++++++++++++++--------------- 2 files changed, 91 insertions(+), 83 deletions(-) diff --git a/extensions/iam.tf b/extensions/iam.tf index c3bd1acb9..54dcac751 100644 --- a/extensions/iam.tf +++ b/extensions/iam.tf @@ -1,13 +1,13 @@ data "aws_caller_identity" "current" {} data "aws_region" "current" {} -resource "aws_iam_role" "codebase_pipeline_deploy_role" { - name = "${var.args.application}-${var.environment}-codebase-pipeline-deploy-role" - assume_role_policy = data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.json +resource "aws_iam_role" "codebase_pipeline_deploy" { + name = "${var.args.application}-${var.environment}-codebase-pipeline-deploy" + assume_role_policy = data.aws_iam_policy_document.assume_codebase_pipeline.json tags = local.tags } -data "aws_iam_policy_document" "codebase_deploy_pipeline_assume_role_policy" { +data "aws_iam_policy_document" "assume_codebase_pipeline" { statement { effect = "Allow" principals { @@ -26,13 +26,13 @@ data "aws_iam_policy_document" "codebase_deploy_pipeline_assume_role_policy" { } } -resource "aws_iam_role_policy" "ecr_access_for_codebase_pipeline" { - name = "${var.args.application}-ecr-access-for-codebase-pipeline" - role = aws_iam_role.codebase_pipeline_deploy_role.name - policy = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.json +resource "aws_iam_role_policy" "ecr_access" { + name = "ecr-access" + role = aws_iam_role.codebase_pipeline_deploy.name + policy = data.aws_iam_policy_document.ecr_access.json } -data "aws_iam_policy_document" "ecr_access_for_codebase_pipeline" { +data "aws_iam_policy_document" "ecr_access" { statement { effect = "Allow" actions = [ @@ -44,13 +44,13 @@ data "aws_iam_policy_document" "ecr_access_for_codebase_pipeline" { } } -resource "aws_iam_role_policy" "artifact_store_access_for_codebase_pipeline" { - name = "${var.args.application}-artifact-store-access-for-codebase-pipeline" - role = aws_iam_role.codebase_pipeline_deploy_role.name - policy = data.aws_iam_policy_document.access_artifact_store.json +resource "aws_iam_role_policy" "artifact_store_access" { + name = "artifact-store-access" + role = aws_iam_role.codebase_pipeline_deploy.name + policy = data.aws_iam_policy_document.artifact_store_access.json } -data "aws_iam_policy_document" "access_artifact_store" { +data "aws_iam_policy_document" "artifact_store_access" { # checkov:skip=CKV_AWS_111:Permissions required to change ACLs on uploaded artifacts # checkov:skip=CKV_AWS_356:Permissions required to upload artifacts statement { @@ -91,13 +91,13 @@ data "aws_iam_policy_document" "access_artifact_store" { } } -resource "aws_iam_role_policy" "ecs_deploy_access_for_codebase_pipeline" { - name = "${var.args.application}-ecs-deploy-access-for-codebase-pipeline" - role = aws_iam_role.codebase_pipeline_deploy_role.name - policy = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.json +resource "aws_iam_role_policy" "ecs_deploy_access" { + name = "ecs-deploy-access" + role = aws_iam_role.codebase_pipeline_deploy.name + policy = data.aws_iam_policy_document.ecs_deploy_access.json } -data "aws_iam_policy_document" "ecs_deploy_access_for_codebase_pipeline" { +data "aws_iam_policy_document" "ecs_deploy_access" { statement { effect = "Allow" actions = [ diff --git a/extensions/tests/unit.tftest.hcl b/extensions/tests/unit.tftest.hcl index bda1ea917..bbc9de8d6 100644 --- a/extensions/tests/unit.tftest.hcl +++ b/extensions/tests/unit.tftest.hcl @@ -261,28 +261,28 @@ run "opensearch_plan_medium_ha_service_test" { } override_data { - target = data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy + target = data.aws_iam_policy_document.assume_codebase_pipeline values = { - json = "{\"Sid\": \"CodeBaseDeployAssumeRole\"}" + json = "{\"Sid\": \"CodeBaseDeployAssume\"}" } } override_data { - target = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline + target = data.aws_iam_policy_document.ecr_access values = { - json = "{\"Sid\": \"ECSDeployAccess\"}" + json = "{\"Sid\": \"ECRAccess\"}" } } override_data { - target = data.aws_iam_policy_document.access_artifact_store + target = data.aws_iam_policy_document.artifact_store_access values = { json = "{\"Sid\": \"ArtifactStoreAccess\"}" } } override_data { - target = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline + target = data.aws_iam_policy_document.ecs_deploy_access values = { json = "{\"Sid\": \"ECSDeployAccess\"}" } @@ -302,79 +302,87 @@ run "codebase_deploy_iam_test" { } assert { - condition = aws_iam_role.codebase_pipeline_deploy_role.name == "test-application-test-environment-codebase-pipeline-deploy-role" - error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" + condition = aws_iam_role.codebase_pipeline_deploy.name == "test-application-test-environment-codebase-pipeline-deploy" + error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy'" } assert { - condition = aws_iam_role.codebase_pipeline_deploy_role.assume_role_policy == "{\"Sid\": \"CodeBaseDeployAssumeRole\"}" - error_message = "Should be: {\"Sid\": \"CodeBaseDeployAssumeRole\"}" + condition = aws_iam_role.codebase_pipeline_deploy.assume_role_policy == "{\"Sid\": \"CodeBaseDeployAssume\"}" + error_message = "Should be: {\"Sid\": \"CodeBaseDeployAssume\"}" } assert { - condition = data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].effect == "Allow" + condition = data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].effect == "Allow" error_message = "Should be: Allow" } assert { - condition = one(data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].actions) == "sts:AssumeRole" + condition = one(data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].actions) == "sts:AssumeRole" error_message = "Should be: sts:AssumeRole" } assert { - condition = one(data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].principals).type == "AWS" + condition = one(data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].principals).type == "AWS" error_message = "Should be: AWS" } assert { - condition = contains(one(data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].principals).identifiers, "arn:aws:iam::000123456789:root") + condition = contains(one(data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].principals).identifiers, "arn:aws:iam::000123456789:root") error_message = "Should contain: arn:aws:iam::000123456789:root" } assert { - condition = [for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.test][0] == "StringLike" + condition = [for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.test][0] == "StringLike" error_message = "Should be: StringLike" } assert { - condition = [for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.variable][0] == "aws:PrincipalArn" + condition = [for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.variable][0] == "aws:PrincipalArn" error_message = "Should be: aws:PrincipalArn" } assert { - condition = flatten([for el in data.aws_iam_policy_document.codebase_deploy_pipeline_assume_role_policy.statement[0].condition : el.values][0]) == ["arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline", "arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline-deploy-manifests"] + condition = flatten([for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.values][0]) == ["arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline", "arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline-deploy-manifests"] error_message = "Unexpected condition values" } assert { - condition = jsonencode(aws_iam_role.codebase_pipeline_deploy_role.tags) == jsonencode(var.expected_tags) + condition = jsonencode(aws_iam_role.codebase_pipeline_deploy.tags) == jsonencode(var.expected_tags) error_message = "Should be: ${jsonencode(var.expected_tags)}" } assert { - condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.name == "test-application-ecr-access-for-codebase-pipeline" - error_message = "Should be: 'test-application-ecr-access-for-codebase-pipeline'" + condition = aws_iam_role_policy.ecr_access.name == "ecr-access" + error_message = "Should be: 'ecr-access'" + } + assert { + condition = aws_iam_role_policy.ecr_access.role == "test-application-test-environment-codebase-pipeline-deploy" + error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy'" } assert { - condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" - error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" + condition = aws_iam_role_policy.ecr_access.policy == "{\"Sid\": \"ECRAccess\"}" + error_message = "Should be: {\"Sid\": \"ECRAccess\"}" } assert { - condition = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.statement[0].effect == "Allow" + condition = data.aws_iam_policy_document.ecr_access.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" + condition = one(data.aws_iam_policy_document.ecr_access.statement[0].actions) == "ecr:DescribeImages" error_message = "Unexpected actions" } assert { - condition = one(data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.statement[0].resources) == "arn:aws:ecr:${data.aws_region.current.name}:000123456789:repository/test-application/*" + condition = one(data.aws_iam_policy_document.ecr_access.statement[0].resources) == "arn:aws:ecr:${data.aws_region.current.name}:000123456789:repository/test-application/*" error_message = "Unexpected resources" } assert { - condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.name == "test-application-artifact-store-access-for-codebase-pipeline" - error_message = "Should be: 'test-application-artifact-store-access-for-codebase-pipeline'" + condition = aws_iam_role_policy.artifact_store_access.name == "artifact-store-access" + error_message = "Should be: 'artifact-store-access'" + } + assert { + condition = aws_iam_role_policy.artifact_store_access.role == "test-application-test-environment-codebase-pipeline-deploy" + error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy'" } assert { - condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" - error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" + condition = aws_iam_role_policy.artifact_store_access.policy == "{\"Sid\": \"ArtifactStoreAccess\"}" + error_message = "Should be: {\"Sid\": \"ArtifactStoreAccess\"}" } assert { - condition = data.aws_iam_policy_document.access_artifact_store.statement[0].effect == "Allow" + condition = data.aws_iam_policy_document.artifact_store_access.statement[0].effect == "Allow" error_message = "Should be: Allow" } assert { - condition = data.aws_iam_policy_document.access_artifact_store.statement[0].actions == toset([ + condition = data.aws_iam_policy_document.artifact_store_access.statement[0].actions == toset([ "s3:GetObject", "s3:GetObjectVersion", "s3:GetBucketVersioning", @@ -384,57 +392,57 @@ run "codebase_deploy_iam_test" { error_message = "Unexpected actions" } assert { - condition = data.aws_iam_policy_document.access_artifact_store.statement[0].resources == toset(["arn:aws:s3:::test-application-*-codebase-pipeline-artifact-store", "arn:aws:s3:::test-application-*-codebase-pipeline-artifact-store/*"]) + condition = data.aws_iam_policy_document.artifact_store_access.statement[0].resources == toset(["arn:aws:s3:::test-application-*-codebase-pipeline-artifact-store", "arn:aws:s3:::test-application-*-codebase-pipeline-artifact-store/*"]) error_message = "Unexpected resources" } assert { - condition = data.aws_iam_policy_document.access_artifact_store.statement[1].effect == "Allow" + condition = data.aws_iam_policy_document.artifact_store_access.statement[1].effect == "Allow" error_message = "Should be: Allow" } assert { - condition = data.aws_iam_policy_document.access_artifact_store.statement[1].actions == toset([ + condition = data.aws_iam_policy_document.artifact_store_access.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) == "*" + condition = one(data.aws_iam_policy_document.artifact_store_access.statement[1].resources) == "*" error_message = "Unexpected resources" } assert { - condition = data.aws_iam_policy_document.access_artifact_store.statement[2].effect == "Allow" + condition = data.aws_iam_policy_document.artifact_store_access.statement[2].effect == "Allow" error_message = "Should be: Allow" } assert { - condition = data.aws_iam_policy_document.access_artifact_store.statement[2].actions == toset([ + condition = data.aws_iam_policy_document.artifact_store_access.statement[2].actions == toset([ "kms:GenerateDataKey", "kms:Decrypt" ]) error_message = "Unexpected actions" } assert { - condition = one(data.aws_iam_policy_document.access_artifact_store.statement[2].resources) == "arn:aws:kms:${data.aws_region.current.name}:000123456789:key/*" + condition = one(data.aws_iam_policy_document.artifact_store_access.statement[2].resources) == "arn:aws:kms:${data.aws_region.current.name}:000123456789:key/*" error_message = "Unexpected resources" } assert { - condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.name == "test-application-ecs-deploy-access-for-codebase-pipeline" - error_message = "Should be: 'test-application-ecs-deploy-access-for-codebase-pipeline'" + condition = aws_iam_role_policy.ecs_deploy_access.name == "ecs-deploy-access" + error_message = "Should be: 'ecs-deploy-access'" } assert { - condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.role == "test-application-test-environment-codebase-pipeline-deploy-role" - error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy-role'" + condition = aws_iam_role_policy.ecs_deploy_access.role == "test-application-test-environment-codebase-pipeline-deploy" + error_message = "Should be: 'test-application-test-environment-codebase-pipeline-deploy'" } assert { - condition = aws_iam_role_policy.ecs_deploy_access_for_codebase_pipeline.policy == "{\"Sid\": \"ECSDeployAccess\"}" + condition = aws_iam_role_policy.ecs_deploy_access.policy == "{\"Sid\": \"ECSDeployAccess\"}" error_message = "Should be: {\"Sid\": \"ECSDeployAccess\"}" } assert { - condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[0].effect == "Allow" + condition = data.aws_iam_policy_document.ecs_deploy_access.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([ + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[0].actions == toset([ "ecs:UpdateService", "ecs:DescribeServices", "ecs:TagResource", @@ -443,94 +451,94 @@ run "codebase_deploy_iam_test" { error_message = "Unexpected actions" } assert { - condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[0].resources == toset([ + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[0].resources == toset([ "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/test-application-test-environment", "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/test-application-test-environment/*" ]) error_message = "Unexpected resources" } assert { - condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[1].effect == "Allow" + condition = data.aws_iam_policy_document.ecs_deploy_access.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([ + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[1].actions == toset([ "ecs:DescribeTasks", "ecs:TagResource" ]) error_message = "Unexpected actions" } assert { - condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[1].resources == toset([ + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[1].resources == toset([ "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/test-application-test-environment", "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task/test-application-test-environment/*" ]) error_message = "Unexpected resources" } assert { - condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[2].effect == "Allow" + condition = data.aws_iam_policy_document.ecs_deploy_access.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([ + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[2].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[2].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task-definition/test-application-test-environment-*:*" + condition = one(data.aws_iam_policy_document.ecs_deploy_access.statement[2].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task-definition/test-application-test-environment-*:*" error_message = "Unexpected resources" } assert { - condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[3].effect == "Allow" + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[3].effect == "Allow" error_message = "Should be: Allow" } assert { - condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[3].actions) == "ecs:ListTasks" + condition = one(data.aws_iam_policy_document.ecs_deploy_access.statement[3].actions) == "ecs:ListTasks" error_message = "Unexpected actions" } assert { - condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[3].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:container-instance/test-application-test-environment/*" + condition = one(data.aws_iam_policy_document.ecs_deploy_access.statement[3].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:container-instance/test-application-test-environment/*" error_message = "Unexpected resources" } assert { - condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[4].effect == "Allow" + condition = data.aws_iam_policy_document.ecs_deploy_access.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([ + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[4].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[4].resources) == "*" + condition = one(data.aws_iam_policy_document.ecs_deploy_access.statement[4].resources) == "*" error_message = "Unexpected resources" } assert { - condition = data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].effect == "Allow" + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[5].effect == "Allow" error_message = "Should be: Allow" } assert { - condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].actions) == "iam:PassRole" + condition = one(data.aws_iam_policy_document.ecs_deploy_access.statement[5].actions) == "iam:PassRole" error_message = "Unexpected actions" } assert { - condition = one(data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].resources) == "*" + condition = one(data.aws_iam_policy_document.ecs_deploy_access.statement[5].resources) == "*" error_message = "Unexpected resources" } assert { - condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].condition : el.test][0] == "StringLike" + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access.statement[5].condition : el.test][0] == "StringLike" error_message = "Should be: StringLike" } assert { - condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].condition : one(el.values)][0] == "ecs-tasks.amazonaws.com" + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access.statement[5].condition : one(el.values)][0] == "ecs-tasks.amazonaws.com" error_message = "Should be: ecs-tasks.amazonaws.com" } assert { - condition = [for el in data.aws_iam_policy_document.ecs_deploy_access_for_codebase_pipeline.statement[5].condition : el.variable][0] == "iam:PassedToService" + condition = [for el in data.aws_iam_policy_document.ecs_deploy_access.statement[5].condition : el.variable][0] == "iam:PassedToService" error_message = "Should be: iam:PassedToService" } } From 2f237956a359d4688d2023a1e4e46167d93ae0b0 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 16 Dec 2024 11:10:22 +0000 Subject: [PATCH 55/71] Role rename --- codebase-pipelines/artifactstore.tf | 2 +- codebase-pipelines/codepipeline.tf | 2 +- codebase-pipelines/tests/unit.tftest.hcl | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/codebase-pipelines/artifactstore.tf b/codebase-pipelines/artifactstore.tf index b2c31ad74..3258fe8a6 100644 --- a/codebase-pipelines/artifactstore.tf +++ b/codebase-pipelines/artifactstore.tf @@ -55,7 +55,7 @@ data "aws_iam_policy_document" "artifact_store_bucket_policy" { type = "AWS" identifiers = [ for env in local.pipeline_environments : - "arn:aws:iam::${env.account.id}:role/${var.application}-${env.name}-codebase-pipeline-deploy-role" + "arn:aws:iam::${env.account.id}:role/${var.application}-${env.name}-codebase-pipeline-deploy" ] } actions = [ diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index ebef13715..2015811ce 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -99,7 +99,7 @@ resource "aws_codepipeline" "codebase_pipeline" { ServiceName = "#{build_manifest.SERVICE_NAME_${upper(stage.value.name)}_${upper(replace(action.value.name, "-", "_"))}}" FileName = "image-definitions-${action.value.name}.json" } - role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy-role" + role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy" } } } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 249363c80..54bcbc449 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -162,7 +162,7 @@ run "test_artifact_store" { error_message = "Should be: AWS" } assert { - condition = flatten([for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[1].principals : el.identifiers]) == ["arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy-role", "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy-role", "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy-role"] + condition = flatten([for el in data.aws_iam_policy_document.artifact_store_bucket_policy.statement[1].principals : el.identifiers]) == ["arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy", "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy", "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy"] error_message = "Bucket policy principals incorrect" } assert { @@ -936,7 +936,7 @@ run "test_main_pipeline" { error_message = "Run order incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy-role" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy" error_message = "Role ARN incorrect" } assert { @@ -962,7 +962,7 @@ run "test_main_pipeline" { error_message = "Run order incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy-role" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy" error_message = "Role ARN incorrect" } assert { @@ -1031,7 +1031,7 @@ run "test_tagged_pipeline" { error_message = "Run order incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].role_arn == "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy-role" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].role_arn == "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy" error_message = "Role ARN incorrect" } assert { @@ -1057,7 +1057,7 @@ run "test_tagged_pipeline" { error_message = "Run order incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].role_arn == "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy-role" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].role_arn == "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy" error_message = "Role ARN incorrect" } assert { @@ -1115,7 +1115,7 @@ run "test_tagged_pipeline" { error_message = "Run order incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy-role" + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy" error_message = "Role ARN incorrect" } assert { @@ -1141,7 +1141,7 @@ run "test_tagged_pipeline" { error_message = "Run order incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy-role" + condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy" error_message = "Role ARN incorrect" } assert { @@ -1285,7 +1285,7 @@ run "test_pipeline_single_run_group" { error_message = "Run order incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy-role" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy" error_message = "Role ARN incorrect" } @@ -1538,7 +1538,7 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { error_message = "Run order incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[1].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy-role" + condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[1].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy" error_message = "Role ARN incorrect" } From 92e47e520caf5409b5e22079150c849f64f79a68 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 16 Dec 2024 11:33:25 +0000 Subject: [PATCH 56/71] Refactor pipeline IAM permissions --- codebase-pipelines/codepipeline.tf | 2 +- codebase-pipelines/eventbridge.tf | 2 +- codebase-pipelines/iam.tf | 22 +++++------ codebase-pipelines/tests/unit.tftest.hcl | 48 ++++++++++++------------ 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 2015811ce..c3a91743f 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -55,7 +55,7 @@ resource "aws_codepipeline" "codebase_pipeline" { namespace = "build_manifest" configuration = { - ProjectName = "${var.application}-${var.codebase}-${each.value.name}-codebase-deploy-manifests" + ProjectName = "${var.application}-${var.codebase}-${each.value.name}-codebase-pipeline-deploy-manifests" EnvironmentVariables : jsonencode([ { name : "APPLICATION", value : var.application }, { name : "ENVIRONMENTS", value : jsonencode([for env in each.value.environments : env]) }, diff --git a/codebase-pipelines/eventbridge.tf b/codebase-pipelines/eventbridge.tf index 404c0e803..63f26a0cd 100644 --- a/codebase-pipelines/eventbridge.tf +++ b/codebase-pipelines/eventbridge.tf @@ -29,7 +29,7 @@ resource "aws_iam_role" "event_bridge_pipeline_trigger" { } resource "aws_iam_role_policy" "event_bridge_pipeline_trigger" { - name = "${var.application}-${var.codebase}-pipeline-trigger-access-for-event-bridge" + name = "event-bridge-access" role = aws_iam_role.event_bridge_pipeline_trigger.name policy = data.aws_iam_policy_document.event_bridge_pipeline_trigger.json } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 27a3bc994..1cf3afb15 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -26,12 +26,12 @@ resource "aws_iam_role_policy_attachment" "ssm_access" { } resource "aws_iam_role_policy" "log_access_for_codebuild_images" { - name = "${var.application}-${var.codebase}-log-access-for-codebuild-images" + name = "log-access" role = aws_iam_role.codebase_image_build.name - policy = data.aws_iam_policy_document.log_access_for_codebuild.json + policy = data.aws_iam_policy_document.log_access.json } -data "aws_iam_policy_document" "log_access_for_codebuild" { +data "aws_iam_policy_document" "log_access" { statement { effect = "Allow" actions = [ @@ -67,7 +67,7 @@ data "aws_iam_policy_document" "log_access_for_codebuild" { } resource "aws_iam_role_policy" "ecr_access_for_codebuild_images" { - name = "${var.application}-${var.codebase}-ecr-access-for-codebuild-images" + name = "ecr-access" role = aws_iam_role.codebase_image_build.name policy = data.aws_iam_policy_document.ecr_access_for_codebuild_images.json } @@ -174,7 +174,7 @@ resource "aws_iam_role" "codebase_deploy_manifests" { } resource "aws_iam_role_policy" "artifact_store_access_for_codebuild_manifests" { - name = "${var.application}-${var.codebase}-artifact-store-access-for-codebuild-manifests" + name = "artifact-store-access" role = aws_iam_role.codebase_deploy_manifests.name policy = data.aws_iam_policy_document.access_artifact_store.json } @@ -223,13 +223,13 @@ data "aws_iam_policy_document" "access_artifact_store" { } resource "aws_iam_role_policy" "log_access_for_codebuild_manifests" { - name = "${var.application}-${var.codebase}-log-access-for-codebuild-manifests" + name = "log-access" role = aws_iam_role.codebase_deploy_manifests.name - policy = data.aws_iam_policy_document.log_access_for_codebuild.json + policy = data.aws_iam_policy_document.log_access.json } resource "aws_iam_role_policy" "codebuild_assume_environment_deploy_role" { - name = "${var.application}-${var.codebase}-environment-deploy-role-access-for-codebuild-manifests" + name = "environment-deploy-role-access" role = aws_iam_role.codebase_deploy_manifests.name policy = data.aws_iam_policy_document.assume_environment_deploy_role.json } @@ -254,7 +254,7 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { } resource "aws_iam_role_policy" "ecr_access_for_codebase_pipeline" { - name = "${var.application}-${var.codebase}-ecr-access-for-codebase-pipeline" + name = "ecr-access" role = aws_iam_role.codebase_deploy_pipeline.name policy = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.json } @@ -272,13 +272,13 @@ data "aws_iam_policy_document" "ecr_access_for_codebase_pipeline" { } resource "aws_iam_role_policy" "artifact_store_access_for_codebase_pipeline" { - name = "${var.application}-${var.codebase}-artifact-store-access-for-codebase-pipeline" + name = "artifact-store-access" role = aws_iam_role.codebase_deploy_pipeline.name policy = data.aws_iam_policy_document.access_artifact_store.json } resource "aws_iam_role_policy" "pipeline_assume_environment_deploy_role" { - name = "${var.application}-${var.codebase}-assume-environment-codebase-pipeline-deploy-role" + name = "environment-deploy-role-access" role = aws_iam_role.codebase_deploy_pipeline.name policy = data.aws_iam_policy_document.assume_environment_deploy_role.json } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 54bcbc449..70a0f0b38 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -8,7 +8,7 @@ override_data { } override_data { - target = data.aws_iam_policy_document.log_access_for_codebuild + target = data.aws_iam_policy_document.log_access values = { json = "{\"Sid\": \"CodeBuildLogs\"}" } @@ -391,27 +391,27 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy.log_access_for_codebuild_images.name == "log-access" + error_message = "Should be: 'log-access'" } assert { condition = aws_iam_role_policy.log_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { - condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[0].effect == "Allow" + condition = data.aws_iam_policy_document.log_access.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"]) + condition = data.aws_iam_policy_document.log_access.statement[0].actions == toset(["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:TagLogGroup"]) error_message = "Unexpected actions" } assert { - condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[1].effect == "Allow" + condition = data.aws_iam_policy_document.log_access.statement[1].effect == "Allow" error_message = "Should be: Allow" } assert { - condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[1].actions == toset([ + condition = data.aws_iam_policy_document.log_access.statement[1].actions == toset([ "codebuild:CreateReportGroup", "codebuild:CreateReport", "codebuild:UpdateReport", @@ -421,7 +421,7 @@ run "test_iam" { error_message = "Unexpected actions" } assert { - condition = data.aws_iam_policy_document.log_access_for_codebuild.statement[1].resources == toset([ + condition = data.aws_iam_policy_document.log_access.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-pipeline-deploy-manifests-*", "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/my-app-my-codebase-codebase-pipeline-image-build-*", "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/pipeline-my-app-*" @@ -429,8 +429,8 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy.ecr_access_for_codebuild_images.name == "ecr-access" + error_message = "Should be: 'ecr-access'" } assert { condition = aws_iam_role_policy.ecr_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" @@ -560,8 +560,8 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.name == "artifact-store-access" + error_message = "Should be: 'artifact-store-access'" } assert { condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" @@ -608,8 +608,8 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy.log_access_for_codebuild_manifests.name == "log-access" + error_message = "Should be: 'log-access'" } assert { condition = aws_iam_role_policy.log_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" @@ -646,8 +646,8 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.name == "ecr-access" + error_message = "Should be: 'ecr-access'" } assert { condition = aws_iam_role_policy.ecr_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" @@ -662,8 +662,8 @@ run "test_iam" { 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'" + condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.name == "artifact-store-access" + error_message = "Should be: 'artifact-store-access'" } assert { condition = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" @@ -888,8 +888,8 @@ run "test_main_pipeline" { 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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-main-codebase-pipeline-deploy-manifests" + error_message = "Should be: my-app-my-codebase-main-codebase-pipeline-deploy-manifests" } assert { condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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}\"}]" @@ -1007,8 +1007,8 @@ run "test_tagged_pipeline" { 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" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-tagged-codebase-pipeline-deploy-manifests" + error_message = "Should be: my-app-my-codebase-tagged-codebase-pipeline-deploy-manifests" } assert { condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"staging\\\",\\\"requires_approval\\\":null},{\\\"account\\\":{\\\"id\\\":\\\"123456789000\\\",\\\"name\\\":\\\"prod\\\"},\\\"name\\\":\\\"prod\\\",\\\"requires_approval\\\":true}]\"},{\"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}\"}]" @@ -1211,8 +1211,8 @@ run "test_event_bridge" { 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'" + condition = aws_iam_role_policy.event_bridge_pipeline_trigger.name == "event-bridge-access" + error_message = "Should be: 'event-bridge-access'" } assert { condition = aws_iam_role_policy.event_bridge_pipeline_trigger.role == "my-app-my-codebase-event-bridge-pipeline-trigger" From e2e50569a062cdc7f3181f1b9cc7648e4fd200a5 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 16 Dec 2024 14:28:07 +0000 Subject: [PATCH 57/71] Remove unnecessary IAM permission; Fix deploy manifest buildspec --- codebase-pipelines/buildspec-manifests.yml | 35 ++++++----- codebase-pipelines/iam.tf | 56 ++++------------- codebase-pipelines/tests/unit.tftest.hcl | 73 +++++++++++----------- 3 files changed, 66 insertions(+), 98 deletions(-) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index 466d041cb..6d5cba003 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -14,27 +14,28 @@ phases: build: commands: - set -e - - for env in $(echo $ENVIRONMENTS | jq -c -r '.[].name'); + - | + for env in $(echo $ENVIRONMENTS | jq -c -r '.[].name'); do - unset AWS_ACCESS_KEY_ID; - unset AWS_SECRET_ACCESS_KEY; - unset AWS_SESSION_TOKEN; - ACCOUNT_ID=$(echo $ENVIRONMENTS | jq -c -r '.[] | select(.name=='\"$env\"').account.id'); - assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::$ACCOUNT_ID:role/$APPLICATION-$env-codebase-pipeline-deploy-role" --role-session-name "$env-codebase-pipeline-deploy"); - export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId); - export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey); - export AWS_SESSION_TOKEN=$(echo $assumed_role | jq -r .Credentials.SessionToken); - EXPORT_ENV=$(echo $env | tr '[:lower:]' '[:upper:]'); - export CLUSTER_NAME_$EXPORT_ENV="$APPLICATION-$env"; + unset AWS_ACCESS_KEY_ID + unset AWS_SECRET_ACCESS_KEY + unset AWS_SESSION_TOKEN + ACCOUNT_ID=$(echo $ENVIRONMENTS | jq -c -r '.[] | select(.name=='\"$env\"').account.id') + assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::$ACCOUNT_ID:role/$APPLICATION-$env-codebase-pipeline-deploy" --role-session-name "$env-codebase-pipeline-deploy") + export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId) + export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey) + export AWS_SESSION_TOKEN=$(echo $assumed_role | jq -r .Credentials.SessionToken) + EXPORT_ENV=$(echo $env | tr '[:lower:]' '[:upper:]') + export CLUSTER_NAME_$EXPORT_ENV="$APPLICATION-$env" for svc in $(echo $SERVICES | jq -c -r '.[]'); do - if [ ! -f image-definitions-$svc.json ]; then - echo '[{"name":"'$svc'","imageUri":"'$REPOSITORY_URL':'$IMAGE_TAG'"}]' > image-definitions-$svc.json; + if [ ! -f "image-definitions-$svc.json" ]; then + echo '[{"name":"'$svc'","imageUri":"'$REPOSITORY_URL':'$IMAGE_TAG'"}]' > image-definitions-$svc.json fi - 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; + 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 diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 1cf3afb15..fdf3bc215 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -41,27 +41,10 @@ data "aws_iam_policy_document" "log_access" { "logs:TagLogGroup" ] 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: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:*" - ] - } - - statement { - effect = "Allow" - actions = [ - "codebuild:CreateReportGroup", - "codebuild:CreateReport", - "codebuild:UpdateReport", - "codebuild:BatchPutTestCases", - "codebuild:BatchPutCodeCoverages" - ] - resources = [ - "arn:aws:codebuild:${local.account_region}:report-group/${var.application}-${var.codebase}-codebase-pipeline-image-build-*", - "arn:aws:codebuild:${local.account_region}:report-group/pipeline-${var.application}-*", - "arn:aws:codebuild:${local.account_region}:report-group/${var.application}-${var.codebase}-*-codebase-pipeline-deploy-manifests-*" + "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group", + "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group:*", + "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-group", + "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-group:*" ] } } @@ -73,16 +56,6 @@ resource "aws_iam_role_policy" "ecr_access_for_codebuild_images" { } data "aws_iam_policy_document" "ecr_access_for_codebuild_images" { - statement { - effect = "Allow" - actions = [ - "ecr:GetAuthorizationToken" - ] - resources = [ - "arn:aws:codebuild:${local.account_region}:report-group/pipeline-${var.application}-*" - ] - } - statement { # checkov:skip=CKV_AWS_107:GetAuthorizationToken required for ci-image-builder effect = "Allow" @@ -284,17 +257,14 @@ resource "aws_iam_role_policy" "pipeline_assume_environment_deploy_role" { } data "aws_iam_policy_document" "assume_environment_deploy_role" { - dynamic "statement" { - for_each = local.pipeline_environments - - content { - effect = "Allow" - actions = [ - "sts:AssumeRole" - ] - resources = [ - "arn:aws:iam::${statement.value.account.id}:role/${var.application}-${statement.value.name}-codebase-pipeline-deploy-role" - ] - } + statement { + effect = "Allow" + actions = [ + "sts:AssumeRole" + ] + resources = [ + for env in local.pipeline_environments : + "arn:aws:iam::${env.account.id}:role/${var.application}-${env.name}-codebase-pipeline-deploy" + ] } } diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index 70a0f0b38..b92c55f85 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -407,26 +407,13 @@ run "test_iam" { error_message = "Unexpected actions" } assert { - condition = data.aws_iam_policy_document.log_access.statement[1].effect == "Allow" - error_message = "Should be: Allow" - } - assert { - condition = data.aws_iam_policy_document.log_access.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.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-pipeline-deploy-manifests-*", - "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/my-app-my-codebase-codebase-pipeline-image-build-*", - "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/pipeline-my-app-*" + condition = data.aws_iam_policy_document.log_access.statement[0].resources == toset([ + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-image-build/log-group", + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-image-build/log-group:*", + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group", + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group:*" ]) - error_message = "Unexpected resources" + error_message = "Unexpected resources ${jsonencode(data.aws_iam_policy_document.log_access.statement[0].resources)}" } assert { condition = aws_iam_role_policy.ecr_access_for_codebuild_images.name == "ecr-access" @@ -441,19 +428,7 @@ run "test_iam" { 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([ + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[0].actions == toset([ "ecr:GetAuthorizationToken", "ecr-public:GetAuthorizationToken", "sts:GetServiceBearerToken" @@ -461,15 +436,15 @@ run "test_iam" { error_message = "Unexpected actions" } assert { - condition = one(data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[1].resources) == "*" + condition = one(data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[0].resources) == "*" error_message = "Unexpected resources" } assert { - condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[2].effect == "Allow" + 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[2].actions == toset([ + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[1].actions == toset([ "ecr-public:DescribeImageScanFindings", "ecr-public:GetLifecyclePolicyPreview", "ecr-public:GetDownloadUrlForLayer", @@ -490,15 +465,15 @@ run "test_iam" { 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/*" + condition = one(data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[1].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" + 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[3].actions == toset([ + condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[2].actions == toset([ "ecr:DescribeImageScanFindings", "ecr:GetLifecyclePolicyPreview", "ecr:GetDownloadUrlForLayer", @@ -669,6 +644,28 @@ run "test_iam" { 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.pipeline_assume_environment_deploy_role.name == "environment-deploy-role-access" + error_message = "Should be: 'environment-deploy-role-access'" + } + assert { + condition = aws_iam_role_policy.pipeline_assume_environment_deploy_role.role == "my-app-my-codebase-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" + } + assert { + condition = data.aws_iam_policy_document.assume_environment_deploy_role.statement[0].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = one(data.aws_iam_policy_document.assume_environment_deploy_role.statement[0].actions) == "sts:AssumeRole" + error_message = "Should be: sts:AssumeRole" + } + assert { + condition = flatten(data.aws_iam_policy_document.assume_environment_deploy_role.statement[0].resources) == ["arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy", + "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy", + "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy"] + error_message = "Unexpected resources" + } } run "test_codebuild_manifests" { From 53a1dd9121843a12ea0b5a38fc3d6767e5baba66 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 16 Dec 2024 14:43:49 +0000 Subject: [PATCH 58/71] Update test error --- codebase-pipelines/tests/unit.tftest.hcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index b92c55f85..aa2c23078 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -413,7 +413,7 @@ run "test_iam" { "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group", "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group:*" ]) - error_message = "Unexpected resources ${jsonencode(data.aws_iam_policy_document.log_access.statement[0].resources)}" + error_message = "Unexpected resources" } assert { condition = aws_iam_role_policy.ecr_access_for_codebuild_images.name == "ecr-access" From 351394e3c910d819cc3190cb3687c15d9feeb649 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 16 Dec 2024 15:15:01 +0000 Subject: [PATCH 59/71] Fix existing tests, codebuild environment variables --- codebase-pipelines/tests/unit.tftest.hcl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index f4dce3113..e808dbd7d 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -728,8 +728,8 @@ run "test_codebuild_manifests" { error_message = "Should be: 'CODEPIPELINE'" } assert { - condition = length(regexall(".*\"exported-variables\":\\[\"CLUSTER_NAME_DEV\".*", aws_codebuild_project.codebase_deploy_manifests.source[0].buildspec)) > 0 - error_message = "Should contain: '\"exported-variables\":[\"CLUSTER_NAME_DEV\"'" + condition = length(regexall(".*\"exported-variables\":\\[\"CLUSTER_NAME\".*", aws_codebuild_project.codebase_deploy_manifests.source[0].buildspec)) > 0 + error_message = "Should contain: '\"exported-variables\":[\"CLUSTER_NAME\"'" } assert { condition = jsonencode(aws_codebuild_project.codebase_deploy_manifests.tags) == jsonencode(var.expected_tags) @@ -889,7 +889,7 @@ run "test_main_pipeline" { error_message = "Should be: my-app-my-codebase-codebase-deploy-manifests" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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}\"}]" + 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" } @@ -1008,7 +1008,7 @@ run "test_tagged_pipeline" { error_message = "Should be: my-app-my-codebase-codebase-deploy-manifests" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"staging\\\",\\\"requires_approval\\\":null},{\\\"account\\\":{\\\"id\\\":\\\"123456789000\\\",\\\"name\\\":\\\"prod\\\"},\\\"name\\\":\\\"prod\\\",\\\"requires_approval\\\":true}]\"},{\"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}\"}]" + 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" } @@ -1268,7 +1268,7 @@ run "test_pipeline_single_run_group" { } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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}\"}]" + 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" } @@ -1349,7 +1349,7 @@ run "test_pipeline_multiple_run_groups" { } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null}]\"},{\"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}\"}]" + 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" } @@ -1459,7 +1459,7 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENTS\",\"value\":\"[{\\\"account\\\":{\\\"id\\\":\\\"000123456789\\\",\\\"name\\\":\\\"sandbox\\\"},\\\"name\\\":\\\"dev\\\",\\\"requires_approval\\\":null},{\\\"account\\\":{\\\"id\\\":\\\"123456789000\\\",\\\"name\\\":\\\"prod\\\"},\\\"name\\\":\\\"prod\\\",\\\"requires_approval\\\":true}]\"},{\"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}\"}]" + 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" } From d12bf4f51dc83e44953330500d7d3f242afc3199 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 16 Dec 2024 16:34:47 +0000 Subject: [PATCH 60/71] Export extra variables for manual release pipeline --- codebase-pipelines/buildspec-manifests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml index 857006d6d..a50884209 100644 --- a/codebase-pipelines/buildspec-manifests.yml +++ b/codebase-pipelines/buildspec-manifests.yml @@ -28,6 +28,7 @@ phases: export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey) export AWS_SESSION_TOKEN=$(echo $assumed_role | jq -r .Credentials.SessionToken) EXPORT_ENV=$(echo $env | tr '[:lower:]' '[:upper:]') + export CLUSTER_NAME="$APPLICATION-$env" export CLUSTER_NAME_$EXPORT_ENV="$APPLICATION-$env" for svc in $(echo $SERVICES | jq -c -r '.[]'); do @@ -35,8 +36,10 @@ phases: echo '[{"name":"'$svc'","imageUri":"'$REPOSITORY_URL':'$IMAGE_TAG'"}]' > image-definitions-$svc.json fi 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_SVC="$(echo $svc | tr - _ | tr '[:lower:]' '[:upper:]')" + EXPORT_ENV_SVC="$(echo $EXPORT_ENV'_'$EXPORT_SVC)" export SERVICE_NAME_$EXPORT_SVC=$(echo $SERVICE_NAME | cut -d '/' -f3) + export SERVICE_NAME_$EXPORT_ENV_SVC=$(echo $SERVICE_NAME | cut -d '/' -f3) cat image-definitions-$svc.json done done From d38bece305a01926e6127b5ed7e382cc1d5c4ec5 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 17 Dec 2024 15:53:45 +0000 Subject: [PATCH 61/71] Change manual pipeline to use codebuild for deployment actions --- codebase-pipelines/buildspec-deploy.yml | 49 ++++++++++++++++ codebase-pipelines/codebuild.tf | 78 ++++++++++++++++++------- codebase-pipelines/codepipeline.tf | 57 ++++++------------ codebase-pipelines/iam.tf | 45 +++++++++++++- codebase-pipelines/locals.tf | 3 +- extensions/iam.tf | 14 ++++- extensions/tests/unit.tftest.hcl | 20 ++++++- 7 files changed, 198 insertions(+), 68 deletions(-) create mode 100644 codebase-pipelines/buildspec-deploy.yml diff --git a/codebase-pipelines/buildspec-deploy.yml b/codebase-pipelines/buildspec-deploy.yml new file mode 100644 index 000000000..cfcea18e8 --- /dev/null +++ b/codebase-pipelines/buildspec-deploy.yml @@ -0,0 +1,49 @@ +version: 0.2 + +phases: + build: + commands: + - set -e + - TASK_FAMILY="${APPLICATION}-${ENVIRONMENT}-${SERVICE}" + - CLUSTER="${APPLICATION}-${ENVIRONMENT}" + - IMAGE_URI="${REPOSITORY_URL}:${IMAGE_TAG}" + # Assume environment role + - ACCOUNT_ID=$(echo ${ENV_CONFIG} | jq -c -r .${ENVIRONMENT}.accounts.deploy.id) + - assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::$ACCOUNT_ID:role/${APPLICATION}-${ENVIRONMENT}-codebase-pipeline-deploy" --role-session-name "${ENVIRONMENT}-codebase-pipeline-deploy") + - export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId) + - export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey) + - export AWS_SESSION_TOKEN=$(echo $assumed_role | jq -r .Credentials.SessionToken) + # Get service name + - SERVICE_NAME=$(aws ecs list-services --cluster ${CLUSTER} | jq -r '.serviceArns[] | select(contains("'${CLUSTER}'-'${SERVICE}'-Service"))' | cut -d '/' -f3) + # Update task definition + - TASK_DEFINITION=$(aws ecs describe-task-definition --task-definition "${TASK_FAMILY}") + - NEW_TASK_DEFINITION=$(echo ${TASK_DEFINITION} | jq --arg IMAGE "$IMAGE_URI" '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') + - NEW_TASK_INFO=$(aws ecs register-task-definition --cli-input-json "$NEW_TASK_DEFINITION") + - NEW_REVISION=$(echo $NEW_TASK_INFO | jq '.taskDefinition.revision') + # Start deployment + - start=$( date +%s ) + - timeout=1800 + - deploy_status="IN_PROGRESS" + - aws ecs update-service --cluster "${CLUSTER}" --service "${SERVICE_NAME}" --task-definition "${TASK_FAMILY}:${NEW_REVISION}" > /dev/null 2>&1 + # Check deployment status + - | + while [[ "$deploy_status" == "IN_PROGRESS" || "$deploy_status" == "PENDING" || "$deploy_status" == "ROLLBACK_IN_PROGRESS" ]]; + do + sleep 10 + now=$( date +%s ) + elapsed=$(( now-start )) + + deploy_status=$(aws ecs list-service-deployments --cluster "${CLUSTER}" --service "${SERVICE_NAME}" --created-at "after=${start}" | jq -r '.serviceDeployments[0].status') + echo "Deployment status after $elapsed seconds: $deploy_status" + + if [[ $elapsed -gt $timeout ]]; then + echo "Error: deployment not completed in $timeout seconds" + exit 1 + fi + done + # Check deployment success + - | + if [ "$deploy_status" != "SUCCEEDED" ]; then + echo "Error: deployment did not succeed" + exit 1 + fi diff --git a/codebase-pipelines/codebuild.tf b/codebase-pipelines/codebuild.tf index f84747324..c0e180b61 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -162,28 +162,6 @@ resource "aws_codebuild_project" "codebase_deploy_manifests" { 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 @@ -195,3 +173,59 @@ 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 } + + +resource "aws_codebuild_project" "codebase_deploy" { + name = "${var.application}-${var.codebase}-codebase-pipeline-deploy" + description = "Deploy specified image tag to specified environment" + build_timeout = 30 + service_role = aws_iam_role.codebase_deploy.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" + + environment_variable { + name = "ENV_CONFIG" + value = jsonencode(local.base_env_config) + } + } + + logs_config { + cloudwatch_logs { + group_name = aws_cloudwatch_log_group.codebase_deploy.name + stream_name = aws_cloudwatch_log_stream.codebase_deploy.name + } + } + + source { + type = "CODEPIPELINE" + buildspec = file("${path.module}/buildspec-deploy.yml") + } + + tags = local.tags +} + +resource "aws_cloudwatch_log_group" "codebase_deploy" { + # 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/log-group" + retention_in_days = 90 +} + +resource "aws_cloudwatch_log_stream" "codebase_deploy" { + name = "codebuild/${var.application}-${var.codebase}-codebase-deploy/log-stream" + log_group_name = aws_cloudwatch_log_group.codebase_deploy.name +} diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index e96d43346..34af60c2e 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -60,7 +60,7 @@ resource "aws_codepipeline" "codebase_pipeline" { { 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 : "REPOSITORY_URL", value : local.repository_url }, { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } ]) } @@ -155,52 +155,31 @@ resource "aws_codepipeline" "manual_release_pipeline" { } } - 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 = aws_codebuild_project.codebase_deploy_manifests.name - EnvironmentVariables : jsonencode([ - { name : "APPLICATION", value : var.application }, - { name : "ENVIRONMENTS", value : "[\"#{variables.ENVIRONMENT}\"]" }, - { 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}" } - ]) - } - } - } - stage { name = "Deploy" 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 + name = action.value.name + category = "Build" + owner = "AWS" + provider = "CodeBuild" + input_artifacts = ["source_output"] + output_artifacts = [] + version = "1" + run_order = action.value.order + 1 + configuration = { - ClusterName = "#{build_manifest.CLUSTER_NAME}" - ServiceName = "#{build_manifest.SERVICE_NAME_${upper(replace(action.value.name, "-", "_"))}}" - FileName = "image-definitions-${action.value.name}.json" + ProjectName = aws_codebuild_project.codebase_deploy.name + EnvironmentVariables : jsonencode([ + { name : "APPLICATION", value : var.application }, + { name : "ENVIRONMENT", value : "#{variables.ENVIRONMENT}" }, + { name : "SERVICE", value : action.value.name }, + { name : "REPOSITORY_URL", value : local.repository_url }, + { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } + ]) } - # role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy-role" - # TODO This is going to need to be a role capable of deploying any environment in the account, how will that work cross-account? } } } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index fdf3bc215..c484d05c9 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -44,7 +44,9 @@ data "aws_iam_policy_document" "log_access" { "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group", "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group:*", "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-group", - "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-group:*" + "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-group:*", + "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy/log-group", + "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy/log-group:*" ] } } @@ -268,3 +270,44 @@ data "aws_iam_policy_document" "assume_environment_deploy_role" { ] } } + + + + +# Manual release pipeline role +resource "aws_iam_role" "codebase_deploy" { + name = "${var.application}-${var.codebase}-codebase-pipeline-deploy" + 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_deploy" { + name = "artifact-store-access" + role = aws_iam_role.codebase_deploy.name + policy = data.aws_iam_policy_document.access_artifact_store.json +} + +resource "aws_iam_role_policy" "log_access_for_codebuild_deploy" { + name = "log-access" + role = aws_iam_role.codebase_deploy.name + policy = data.aws_iam_policy_document.log_access.json +} + +resource "aws_iam_role_policy" "environment_deploy_role_access_for_codebuild_deploy" { + name = "environment-deploy-role-access" + role = aws_iam_role.codebase_deploy.name + policy = data.aws_iam_policy_document.environment_deploy_role_access.json +} + +data "aws_iam_policy_document" "environment_deploy_role_access" { + statement { + effect = "Allow" + actions = [ + "sts:AssumeRole" + ] + resources = [ + for id in local.deploy_account_ids : + "arn:aws:iam::${id}:role/${var.application}-*-codebase-pipeline-deploy" + ] + } +} diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index 28f6e2ace..a6c8f1ac2 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -7,7 +7,8 @@ locals { account_region = "${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}" - ecr_name = "${var.application}/${var.codebase}" + ecr_name = "${var.application}/${var.codebase}" + repository_url = "${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/${local.ecr_name}" pipeline_branches = distinct([ for pipeline in var.pipelines : pipeline.branch if lookup(pipeline, "branch", null) != null diff --git a/extensions/iam.tf b/extensions/iam.tf index 54dcac751..04a7487da 100644 --- a/extensions/iam.tf +++ b/extensions/iam.tf @@ -15,10 +15,10 @@ data "aws_iam_policy_document" "assume_codebase_pipeline" { identifiers = ["arn:aws:iam::${var.args.pipeline_account_id}:root"] } condition { - test = "StringLike" + test = "ArnLike" values = [ "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline", - "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline-deploy-manifests" + "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline-*" ] variable = "aws:PrincipalArn" } @@ -166,4 +166,14 @@ data "aws_iam_policy_document" "ecs_deploy_access" { variable = "iam:PassedToService" } } + + statement { + effect = "Allow" + actions = [ + "ecs:ListServiceDeployments" + ] + resources = [ + "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/${var.args.application}-${var.environment}/*" + ] + } } diff --git a/extensions/tests/unit.tftest.hcl b/extensions/tests/unit.tftest.hcl index bbc9de8d6..6e5e1cb3a 100644 --- a/extensions/tests/unit.tftest.hcl +++ b/extensions/tests/unit.tftest.hcl @@ -326,15 +326,15 @@ run "codebase_deploy_iam_test" { error_message = "Should contain: arn:aws:iam::000123456789:root" } assert { - condition = [for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.test][0] == "StringLike" - error_message = "Should be: StringLike" + condition = [for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.test][0] == "ArnLike" + error_message = "Should be: ArnLike" } assert { condition = [for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.variable][0] == "aws:PrincipalArn" error_message = "Should be: aws:PrincipalArn" } assert { - condition = flatten([for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.values][0]) == ["arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline", "arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline-deploy-manifests"] + condition = flatten([for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.values][0]) == ["arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline", "arn:aws:iam::000123456789:role/test-application-*-codebase-pipeline-*"] error_message = "Unexpected condition values" } assert { @@ -541,4 +541,18 @@ run "codebase_deploy_iam_test" { condition = [for el in data.aws_iam_policy_document.ecs_deploy_access.statement[5].condition : el.variable][0] == "iam:PassedToService" error_message = "Should be: iam:PassedToService" } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[6].effect == "Allow" + error_message = "Should be: Allow" + } + assert { + condition = data.aws_iam_policy_document.ecs_deploy_access.statement[6].actions == toset([ + "ecs:ListServiceDeployments" + ]) + error_message = "Unexpected actions" + } + assert { + condition = one(data.aws_iam_policy_document.ecs_deploy_access.statement[6].resources) == "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/test-application-test-environment/*" + error_message = "Unexpected resources" + } } From 9889c0c8ea4218d6e6aeb8f1e559ee412f2e25f3 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 17 Dec 2024 16:33:11 +0000 Subject: [PATCH 62/71] Add tests for manual release pipeline; Refactor policy documents into separate test group --- codebase-pipelines/iam.tf | 4 - codebase-pipelines/tests/unit.tftest.hcl | 544 ++++++++++++++++++----- 2 files changed, 439 insertions(+), 109 deletions(-) diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index c484d05c9..770c8b9c1 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -271,10 +271,6 @@ data "aws_iam_policy_document" "assume_environment_deploy_role" { } } - - - -# Manual release pipeline role resource "aws_iam_role" "codebase_deploy" { name = "${var.application}-${var.codebase}-codebase-pipeline-deploy" assume_role_policy = data.aws_iam_policy_document.assume_codebuild_role.json diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index e808dbd7d..c30bc9161 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -56,6 +56,13 @@ override_data { } } +override_data { + target = data.aws_iam_policy_document.environment_deploy_role_access + values = { + json = "{\"Sid\": \"EnvironmentDeployAccess\"}" + } +} + variables { env_config = { "*" = { @@ -375,29 +382,149 @@ run "test_iam" { error_message = "Should be: ${jsonencode(var.expected_tags)}" } assert { - condition = data.aws_iam_policy_document.assume_codebuild_role.statement[0].effect == "Allow" - error_message = "Should be: Allow" + condition = aws_iam_role_policy.log_access_for_codebuild_images.name == "log-access" + error_message = "Should be: 'log-access'" } assert { - condition = one(data.aws_iam_policy_document.assume_codebuild_role.statement[0].actions) == "sts:AssumeRole" - error_message = "Should be: sts:AssumeRole" + condition = aws_iam_role_policy.log_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { - condition = one(data.aws_iam_policy_document.assume_codebuild_role.statement[0].principals).type == "Service" - error_message = "Should be: Service" + condition = aws_iam_role_policy.ecr_access_for_codebuild_images.name == "ecr-access" + error_message = "Should be: 'ecr-access'" } 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" + condition = aws_iam_role_policy.ecr_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { - condition = aws_iam_role_policy.log_access_for_codebuild_images.name == "log-access" - error_message = "Should be: 'log-access'" + condition = aws_iam_role_policy.codestar_connection_access.name == "codestar-connection-policy" + error_message = "Should be: 'codestar-connection-policy'" } assert { - condition = aws_iam_role_policy.log_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" + condition = aws_iam_role_policy.codestar_connection_access.role == "my-app-my-codebase-codebase-pipeline-image-build" error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } + assert { + condition = aws_iam_role_policy_attachment.ssm_access.role == "my-app-my-codebase-codebase-pipeline-image-build" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-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.codebase_deploy_manifests.name == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" + } + assert { + condition = aws_iam_role.codebase_deploy_manifests.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" + error_message = "Should be: {\"Sid\": \"AssumeCodebuildRole\"}" + } + assert { + condition = jsonencode(aws_iam_role.codebase_deploy_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 == "artifact-store-access" + error_message = "Should be: 'artifact-store-access'" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" + } + assert { + condition = aws_iam_role_policy.log_access_for_codebuild_manifests.name == "log-access" + error_message = "Should be: 'log-access'" + } + assert { + condition = aws_iam_role_policy.log_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" + } + + # CodeBuild deploy + assert { + condition = aws_iam_role.codebase_deploy.name == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy'" + } + assert { + condition = aws_iam_role.codebase_deploy.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" + error_message = "Should be: {\"Sid\": \"AssumeCodebuildRole\"}" + } + assert { + condition = jsonencode(aws_iam_role.codebase_deploy.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_deploy.name == "artifact-store-access" + error_message = "Should be: 'artifact-store-access'" + } + assert { + condition = aws_iam_role_policy.artifact_store_access_for_codebuild_deploy.role == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy'" + } + assert { + condition = aws_iam_role_policy.log_access_for_codebuild_deploy.name == "log-access" + error_message = "Should be: 'log-access'" + } + assert { + condition = aws_iam_role_policy.log_access_for_codebuild_deploy.role == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy'" + } + assert { + condition = aws_iam_role_policy.environment_deploy_role_access_for_codebuild_deploy.name == "environment-deploy-role-access" + error_message = "Should be: 'environment-deploy-role-access'" + } + assert { + condition = aws_iam_role_policy.environment_deploy_role_access_for_codebuild_deploy.role == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy'" + } + + # 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 = aws_iam_role_policy.ecr_access_for_codebase_pipeline.name == "ecr-access" + error_message = "Should be: 'ecr-access'" + } + 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 = aws_iam_role_policy.artifact_store_access_for_codebase_pipeline.name == "artifact-store-access" + error_message = "Should be: 'artifact-store-access'" + } + 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.pipeline_assume_environment_deploy_role.name == "environment-deploy-role-access" + error_message = "Should be: 'environment-deploy-role-access'" + } + assert { + condition = aws_iam_role_policy.pipeline_assume_environment_deploy_role.role == "my-app-my-codebase-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" + } +} + +run "test_iam_documents" { + command = plan + + # Log access assert { condition = data.aws_iam_policy_document.log_access.statement[0].effect == "Allow" error_message = "Should be: Allow" @@ -411,18 +538,32 @@ run "test_iam" { "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-image-build/log-group", "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-image-build/log-group:*", "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group", - "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group:*" + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group:*", + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy/log-group", + "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy/log-group:*" ]) error_message = "Unexpected resources" } + + # Assume CodeBuild role assert { - condition = aws_iam_role_policy.ecr_access_for_codebuild_images.name == "ecr-access" - error_message = "Should be: 'ecr-access'" + condition = data.aws_iam_policy_document.assume_codebuild_role.statement[0].effect == "Allow" + error_message = "Should be: Allow" } assert { - condition = aws_iam_role_policy.ecr_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" + condition = one(data.aws_iam_policy_document.assume_codebuild_role.statement[0].actions) == "sts:AssumeRole" + error_message = "Should be: sts:AssumeRole" + } + assert { + 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" } + + # ECR access assert { condition = data.aws_iam_policy_document.ecr_access_for_codebuild_images.statement[0].effect == "Allow" error_message = "Should be: Allow" @@ -493,14 +634,8 @@ run "test_iam" { ]) error_message = "Unexpected actions" } - assert { - condition = aws_iam_role_policy.codestar_connection_access.name == "codestar-connection-policy" - error_message = "Should be: 'codestar-connection-policy'" - } - assert { - condition = aws_iam_role_policy.codestar_connection_access.role == "my-app-my-codebase-codebase-pipeline-image-build" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" - } + + # Codestar connection assert { condition = data.aws_iam_policy_document.codestar_connection_access.statement[0].effect == "Allow" error_message = "Should be: Allow" @@ -512,36 +647,8 @@ run "test_iam" { ]) error_message = "Unexpected actions" } - assert { - condition = aws_iam_role_policy_attachment.ssm_access.role == "my-app-my-codebase-codebase-pipeline-image-build" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-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.codebase_deploy_manifests.name == "my-app-my-codebase-codebase-pipeline-deploy-manifests" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" - } - assert { - condition = aws_iam_role.codebase_deploy_manifests.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" - error_message = "Should be: {\"Sid\": \"AssumeCodebuildRole\"}" - } - assert { - condition = jsonencode(aws_iam_role.codebase_deploy_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 == "artifact-store-access" - error_message = "Should be: 'artifact-store-access'" - } - assert { - condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" - } + # Artifact store access assert { condition = data.aws_iam_policy_document.access_artifact_store.statement[0].effect == "Allow" error_message = "Should be: Allow" @@ -582,28 +689,25 @@ run "test_iam" { ]) error_message = "Unexpected actions" } - assert { - condition = aws_iam_role_policy.log_access_for_codebuild_manifests.name == "log-access" - error_message = "Should be: 'log-access'" - } - assert { - condition = aws_iam_role_policy.log_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" - } - # CodePipeline + # Assume environment deploy role 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'" + condition = data.aws_iam_policy_document.environment_deploy_role_access.statement[0].effect == "Allow" + error_message = "Should be: Allow" } assert { - condition = aws_iam_role.codebase_deploy_pipeline.assume_role_policy == "{\"Sid\": \"AssumeCodepipelineRole\"}" - error_message = "Should be: {\"Sid\": \"AssumeCodepipelineRole\"}" + condition = data.aws_iam_policy_document.environment_deploy_role_access.statement[0].actions == toset([ + "sts:AssumeRole" + ]) + error_message = "Unexpected actions" } assert { - condition = jsonencode(aws_iam_role.codebase_deploy_pipeline.tags) == jsonencode(var.expected_tags) - error_message = "Should be: ${jsonencode(var.expected_tags)}" + condition = flatten(data.aws_iam_policy_document.environment_deploy_role_access.statement[0].resources) == ["arn:aws:iam::000123456789:role/my-app-*-codebase-pipeline-deploy", + "arn:aws:iam::123456789000:role/my-app-*-codebase-pipeline-deploy"] + error_message = "Unexpected resources" } + + # Assume CodePipeline role assert { condition = data.aws_iam_policy_document.assume_codepipeline_role.statement[0].effect == "Allow" error_message = "Should be: Allow" @@ -620,14 +724,8 @@ run "test_iam" { 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 == "ecr-access" - error_message = "Should be: 'ecr-access'" - } - 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'" - } + + # Pipeline ECR access assert { condition = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.statement[0].effect == "Allow" error_message = "Should be: Allow" @@ -636,22 +734,8 @@ run "test_iam" { 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 == "artifact-store-access" - error_message = "Should be: 'artifact-store-access'" - } - 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.pipeline_assume_environment_deploy_role.name == "environment-deploy-role-access" - error_message = "Should be: 'environment-deploy-role-access'" - } - assert { - condition = aws_iam_role_policy.pipeline_assume_environment_deploy_role.role == "my-app-my-codebase-codebase-pipeline" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" - } + + # Pipeline assume environment deploy role assert { condition = data.aws_iam_policy_document.assume_environment_deploy_role.statement[0].effect == "Allow" error_message = "Should be: Allow" @@ -735,37 +819,110 @@ run "test_codebuild_manifests" { condition = jsonencode(aws_codebuild_project.codebase_deploy_manifests.tags) == jsonencode(var.expected_tags) error_message = "Should be: ${jsonencode(var.expected_tags)}" } + + # Cloudwatch config: 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" + 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_kms_key.codebuild_kms_key.enable_key_rotation == true - error_message = "Should be: true" + 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_codebuild_deploy" { + command = plan assert { - condition = jsonencode(aws_kms_key.codebuild_kms_key.tags) == jsonencode(var.expected_tags) + condition = aws_codebuild_project.codebase_deploy.name == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: 'my-app-my-codebase-codebase-deploy'" + } + assert { + condition = aws_codebuild_project.codebase_deploy.description == "Deploy specified image tag to specified environment" + error_message = "Should be: 'Deploy specified image tag to specified environment'" + } + assert { + condition = aws_codebuild_project.codebase_deploy.build_timeout == 30 + error_message = "Should be: 5" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy.artifacts).type == "CODEPIPELINE" + error_message = "Should be: 'CODEPIPELINE'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy.cache).type == "S3" + error_message = "Should be: 'S3'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy.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.environment).compute_type == "BUILD_GENERAL1_SMALL" + error_message = "Should be: 'BUILD_GENERAL1_SMALL'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy.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.environment).type == "LINUX_CONTAINER" + error_message = "Should be: 'LINUX_CONTAINER'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy.environment).image_pull_credentials_type == "CODEBUILD" + error_message = "Should be: 'CODEBUILD'" + } + assert { + condition = aws_codebuild_project.codebase_deploy.logs_config[0].cloudwatch_logs[ + 0 + ].group_name == "codebuild/my-app-my-codebase-codebase-deploy/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy/log-group'" + } + assert { + condition = aws_codebuild_project.codebase_deploy.logs_config[0].cloudwatch_logs[ + 0 + ].stream_name == "codebuild/my-app-my-codebase-codebase-deploy/log-stream" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy/log-stream'" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy.source).type == "CODEPIPELINE" + error_message = "Should be: 'CODEPIPELINE'" + } + assert { + condition = length(regexall(".*aws ecs update-service.*", aws_codebuild_project.codebase_deploy.source[0].buildspec)) > 0 + error_message = "Should contain: 'aws ecs update-service'" + } + assert { + condition = jsonencode(aws_codebuild_project.codebase_deploy.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'" + condition = aws_cloudwatch_log_group.codebase_deploy.name == "codebuild/my-app-my-codebase-codebase-deploy/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy/log-group'" } assert { - condition = aws_cloudwatch_log_group.codebase_deploy_manifests.retention_in_days == 90 + condition = aws_cloudwatch_log_group.codebase_deploy.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'" + condition = aws_cloudwatch_log_stream.codebase_deploy.name == "codebuild/my-app-my-codebase-codebase-deploy/log-stream" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy/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'" + condition = aws_cloudwatch_log_stream.codebase_deploy.log_group_name == "codebuild/my-app-my-codebase-codebase-deploy/log-group" + error_message = "Should be: 'codebuild/my-app-my-codebase-codebase-deploy/log-group'" } } @@ -1155,6 +1312,183 @@ run "test_tagged_pipeline" { } } +run "test_manual_release_pipeline" { + command = plan + + assert { + condition = aws_codepipeline.manual_release_pipeline.name == "my-app-my-codebase-manual-release-pipeline" + error_message = "Should be: 'my-app-my-codebase-manual-release-pipeline'" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.variable[0].name == "IMAGE_TAG" + error_message = "Should be: 'IMAGE_TAG'" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.variable[0].default_value == "NONE" + error_message = "Should be: 'NONE'" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.variable[0].description == "Tagged image in ECR to deploy" + error_message = "Should be: 'Tagged image in ECR to deploy'" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.variable[1].name == "ENVIRONMENT" + error_message = "Should be: 'ENVIRONMENT'" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.variable[1].default_value == "NONE" + error_message = "Should be: 'NONE'" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.variable[1].description == "Name of the environment to deploy to" + error_message = "Should be: 'Name of the environment to deploy to'" + } + assert { + condition = tolist(aws_codepipeline.manual_release_pipeline.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.manual_release_pipeline.artifact_store)[0].type == "S3" + error_message = "Should be: 'S3'" + } + assert { + condition = tolist(aws_codepipeline.manual_release_pipeline.artifact_store)[0].encryption_key[0].type == "KMS" + error_message = "Should be: 'KMS'" + } + assert { + condition = jsonencode(aws_codepipeline.manual_release_pipeline.tags) == jsonencode(var.expected_tags) + error_message = "Should be: ${jsonencode(var.expected_tags)}" + } + assert { + condition = length(aws_codepipeline.manual_release_pipeline.stage) == 2 + error_message = "Should be: 2" + } + + # Source stage + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[0].name == "Source" + error_message = "Should be: Source" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].name == "Source" + error_message = "Should be: Source" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].category == "Source" + error_message = "Should be: Source" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].owner == "AWS" + error_message = "Should be: AWS" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].provider == "ECR" + error_message = "Should be: ECR" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].version == "1" + error_message = "Should be: 1" + } + assert { + condition = one(aws_codepipeline.manual_release_pipeline.stage[0].action[0].output_artifacts) == "source_output" + error_message = "Should be: source_output" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].namespace == "source_ecr" + error_message = "Should be: source_ecr" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].configuration.RepositoryName == "my-app/my-codebase" + error_message = "Should be: my-app/my-codebase" + } + + # Deploy stage + + # Deploy service-1 action + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].name == "Deploy" + error_message = "Should be: Deploy" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].name == "service-1" + error_message = "Should be: service-1" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].category == "Build" + error_message = "Should be: Build" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].owner == "AWS" + error_message = "Should be: AWS" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].provider == "CodeBuild" + error_message = "Should be: CodeBuild" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].version == "1" + error_message = "Should be: 1" + } + assert { + condition = one(aws_codepipeline.manual_release_pipeline.stage[1].action[0].input_artifacts) == "source_output" + error_message = "Should be: source_output" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: my-app-my-codebase-codebase-pipeline-deploy" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"#{variables.ENVIRONMENT}\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].run_order == 2 + error_message = "Run order incorrect" + } + + # Deploy service-2 action + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].name == "Deploy" + error_message = "Should be: Deploy" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].name == "service-2" + error_message = "Should be: service-1" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].category == "Build" + error_message = "Should be: Build" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].owner == "AWS" + error_message = "Should be: AWS" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].provider == "CodeBuild" + error_message = "Should be: CodeBuild" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].version == "1" + error_message = "Should be: 1" + } + assert { + condition = one(aws_codepipeline.manual_release_pipeline.stage[1].action[1].input_artifacts) == "source_output" + error_message = "Should be: source_output" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].configuration.ProjectName == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: my-app-my-codebase-codebase-pipeline-deploy" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"#{variables.ENVIRONMENT}\"},{\"name\":\"SERVICE\",\"value\":\"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 ${jsonencode(aws_codepipeline.manual_release_pipeline.stage[1].action[0].configuration.EnvironmentVariables)}" + } + assert { + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].run_order == 3 + error_message = "Run order incorrect" + } +} + run "test_event_bridge" { command = plan From 8fb5d972988d503bc5f0eb9cc1135b8a953e29ad Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 17 Dec 2024 16:38:27 +0000 Subject: [PATCH 63/71] Use arnlike for IAM resource --- extensions/iam.tf | 2 +- extensions/tests/unit.tftest.hcl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/iam.tf b/extensions/iam.tf index 54dcac751..caf52f986 100644 --- a/extensions/iam.tf +++ b/extensions/iam.tf @@ -15,7 +15,7 @@ data "aws_iam_policy_document" "assume_codebase_pipeline" { identifiers = ["arn:aws:iam::${var.args.pipeline_account_id}:root"] } condition { - test = "StringLike" + test = "ArnLike" values = [ "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline", "arn:aws:iam::${var.args.pipeline_account_id}:role/${var.args.application}-*-codebase-pipeline-deploy-manifests" diff --git a/extensions/tests/unit.tftest.hcl b/extensions/tests/unit.tftest.hcl index bbc9de8d6..d3913f091 100644 --- a/extensions/tests/unit.tftest.hcl +++ b/extensions/tests/unit.tftest.hcl @@ -326,8 +326,8 @@ run "codebase_deploy_iam_test" { error_message = "Should contain: arn:aws:iam::000123456789:root" } assert { - condition = [for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.test][0] == "StringLike" - error_message = "Should be: StringLike" + condition = [for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.test][0] == "ArnLike" + error_message = "Should be: ArnLike" } assert { condition = [for el in data.aws_iam_policy_document.assume_codebase_pipeline.statement[0].condition : el.variable][0] == "aws:PrincipalArn" From 69fb2938b4c35fb09ca418e2a8e8a9202f117fff Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Wed, 18 Dec 2024 12:16:01 +0000 Subject: [PATCH 64/71] Change all deployments actions to use codebuild --- codebase-pipelines/buildspec-manifests.yml | 49 -- codebase-pipelines/codebuild.tf | 60 --- codebase-pipelines/codepipeline.tf | 54 +-- codebase-pipelines/iam.tf | 111 ++--- codebase-pipelines/locals.tf | 4 - codebase-pipelines/tests/unit.tftest.hcl | 496 +++++---------------- 6 files changed, 161 insertions(+), 613 deletions(-) delete mode 100644 codebase-pipelines/buildspec-manifests.yml diff --git a/codebase-pipelines/buildspec-manifests.yml b/codebase-pipelines/buildspec-manifests.yml deleted file mode 100644 index a50884209..000000000 --- a/codebase-pipelines/buildspec-manifests.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: 0.2 - -env: - ${jsonencode({ - exported-variables: flatten([ for env in environments : [ - "CLUSTER_NAME", - "CLUSTER_NAME_${env}", - [ for svc in services: [ - "SERVICE_NAME_${svc}", - "SERVICE_NAME_${env}_${svc}" - ]] - ]]) - })} - -phases: - build: - commands: - - set -e - - | - for env in $(echo $ENVIRONMENTS | jq -c -r '.[]'); - do - unset AWS_ACCESS_KEY_ID - unset AWS_SECRET_ACCESS_KEY - unset AWS_SESSION_TOKEN - ACCOUNT_ID=$(echo $ENV_CONFIG | jq -c -r .$env.accounts.deploy.id) - assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::$ACCOUNT_ID:role/$APPLICATION-$env-codebase-pipeline-deploy" --role-session-name "$env-codebase-pipeline-deploy") - export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId) - export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey) - export AWS_SESSION_TOKEN=$(echo $assumed_role | jq -r .Credentials.SessionToken) - EXPORT_ENV=$(echo $env | tr '[:lower:]' '[:upper:]') - export CLUSTER_NAME="$APPLICATION-$env" - export CLUSTER_NAME_$EXPORT_ENV="$APPLICATION-$env" - for svc in $(echo $SERVICES | jq -c -r '.[]'); - do - if [ ! -f "image-definitions-$svc.json" ]; then - echo '[{"name":"'$svc'","imageUri":"'$REPOSITORY_URL':'$IMAGE_TAG'"}]' > image-definitions-$svc.json - fi - SERVICE_NAME=$(aws ecs list-services --cluster $APPLICATION-$env | jq -r '.serviceArns[] | select(contains("'$APPLICATION-$env'-'$svc'-Service"))') - EXPORT_SVC="$(echo $svc | tr - _ | tr '[:lower:]' '[:upper:]')" - EXPORT_ENV_SVC="$(echo $EXPORT_ENV'_'$EXPORT_SVC)" - export SERVICE_NAME_$EXPORT_SVC=$(echo $SERVICE_NAME | cut -d '/' -f3) - export SERVICE_NAME_$EXPORT_ENV_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 c0e180b61..07fd376c5 100644 --- a/codebase-pipelines/codebuild.tf +++ b/codebase-pipelines/codebuild.tf @@ -115,66 +115,6 @@ resource "aws_codebuild_webhook" "codebuild_webhook" { } -resource "aws_codebuild_project" "codebase_deploy_manifests" { - name = "${var.application}-${var.codebase}-codebase-pipeline-deploy-manifests" - description = "Create image deploy manifests to deploy services" - build_timeout = 5 - service_role = aws_iam_role.codebase_deploy_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" - - environment_variable { - name = "ENV_CONFIG" - value = jsonencode(local.base_env_config) - } - } - - 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 local.pipeline_environments : upper(env.name) - ], services = local.service_export_names - }) - } - - 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 -} - - resource "aws_codebuild_project" "codebase_deploy" { name = "${var.application}-${var.codebase}-codebase-pipeline-deploy" description = "Deploy specified image tag to specified environment" diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 34af60c2e..e51e866d1 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -41,32 +41,6 @@ resource "aws_codepipeline" "codebase_pipeline" { } } - 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 = aws_codebuild_project.codebase_deploy_manifests.name - 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 : local.repository_url }, - { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } - ]) - } - } - } - dynamic "stage" { for_each = each.value.environments content { @@ -87,19 +61,25 @@ resource "aws_codepipeline" "codebase_pipeline" { 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 + name = action.value.name + category = "Build" + owner = "AWS" + provider = "CodeBuild" + input_artifacts = ["source_output"] + output_artifacts = [] + version = "1" + 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" + ProjectName = aws_codebuild_project.codebase_deploy.name + EnvironmentVariables : jsonencode([ + { name : "APPLICATION", value : var.application }, + { name : "ENVIRONMENT", value : stage.value.name }, + { name : "SERVICE", value : action.value.name }, + { name : "REPOSITORY_URL", value : local.repository_url }, + { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } + ]) } - role_arn = "arn:aws:iam::${stage.value.account.id}:role/${var.application}-${stage.value.name}-codebase-pipeline-deploy" } } } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 770c8b9c1..0319913b4 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -43,8 +43,6 @@ data "aws_iam_policy_document" "log_access" { resources = [ "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group", "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-image-build/log-group:*", - "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-group", - "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy-manifests/log-group:*", "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy/log-group", "arn:aws:logs:${local.account_region}:log-group:codebuild/${var.application}-${var.codebase}-codebase-deploy/log-group:*" ] @@ -142,73 +140,6 @@ data "aws_iam_policy_document" "codestar_connection_access" { } } -resource "aws_iam_role" "codebase_deploy_manifests" { - name = "${var.application}-${var.codebase}-codebase-pipeline-deploy-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 = "artifact-store-access" - role = aws_iam_role.codebase_deploy_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 = "log-access" - role = aws_iam_role.codebase_deploy_manifests.name - policy = data.aws_iam_policy_document.log_access.json -} - -resource "aws_iam_role_policy" "codebuild_assume_environment_deploy_role" { - name = "environment-deploy-role-access" - role = aws_iam_role.codebase_deploy_manifests.name - policy = data.aws_iam_policy_document.assume_environment_deploy_role.json -} - 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 @@ -252,21 +183,45 @@ resource "aws_iam_role_policy" "artifact_store_access_for_codebase_pipeline" { policy = data.aws_iam_policy_document.access_artifact_store.json } -resource "aws_iam_role_policy" "pipeline_assume_environment_deploy_role" { - name = "environment-deploy-role-access" - role = aws_iam_role.codebase_deploy_pipeline.name - policy = data.aws_iam_policy_document.assume_environment_deploy_role.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}/*" + ] + } -data "aws_iam_policy_document" "assume_environment_deploy_role" { statement { effect = "Allow" + actions = [ - "sts:AssumeRole" + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + ] + + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "kms:GenerateDataKey", + "kms:Decrypt" ] resources = [ - for env in local.pipeline_environments : - "arn:aws:iam::${env.account.id}:role/${var.application}-${env.name}-codebase-pipeline-deploy" + aws_kms_key.artifact_store_kms_key.arn ] } } diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index a6c8f1ac2..e631678cd 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -42,10 +42,6 @@ locals { 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 : [ diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index c30bc9161..be599f043 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -414,36 +414,6 @@ run "test_iam" { error_message = "Should be: 'arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess'" } - # CodeBuild deploy manifests - assert { - condition = aws_iam_role.codebase_deploy_manifests.name == "my-app-my-codebase-codebase-pipeline-deploy-manifests" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" - } - assert { - condition = aws_iam_role.codebase_deploy_manifests.assume_role_policy == "{\"Sid\": \"AssumeCodebuildRole\"}" - error_message = "Should be: {\"Sid\": \"AssumeCodebuildRole\"}" - } - assert { - condition = jsonencode(aws_iam_role.codebase_deploy_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 == "artifact-store-access" - error_message = "Should be: 'artifact-store-access'" - } - assert { - condition = aws_iam_role_policy.artifact_store_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" - } - assert { - condition = aws_iam_role_policy.log_access_for_codebuild_manifests.name == "log-access" - error_message = "Should be: 'log-access'" - } - assert { - condition = aws_iam_role_policy.log_access_for_codebuild_manifests.role == "my-app-my-codebase-codebase-pipeline-deploy-manifests" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy-manifests'" - } - # CodeBuild deploy assert { condition = aws_iam_role.codebase_deploy.name == "my-app-my-codebase-codebase-pipeline-deploy" @@ -511,14 +481,6 @@ run "test_iam" { 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.pipeline_assume_environment_deploy_role.name == "environment-deploy-role-access" - error_message = "Should be: 'environment-deploy-role-access'" - } - assert { - condition = aws_iam_role_policy.pipeline_assume_environment_deploy_role.role == "my-app-my-codebase-codebase-pipeline" - error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" - } } run "test_iam_documents" { @@ -537,8 +499,6 @@ run "test_iam_documents" { condition = data.aws_iam_policy_document.log_access.statement[0].resources == toset([ "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-image-build/log-group", "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-image-build/log-group:*", - "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group", - "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy-manifests/log-group:*", "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy/log-group", "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:codebuild/my-app-my-codebase-codebase-deploy/log-group:*" ]) @@ -734,109 +694,6 @@ run "test_iam_documents" { condition = one(data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.statement[0].actions) == "ecr:DescribeImages" error_message = "Unexpected actions" } - - # Pipeline assume environment deploy role - assert { - condition = data.aws_iam_policy_document.assume_environment_deploy_role.statement[0].effect == "Allow" - error_message = "Should be: Allow" - } - assert { - condition = one(data.aws_iam_policy_document.assume_environment_deploy_role.statement[0].actions) == "sts:AssumeRole" - error_message = "Should be: sts:AssumeRole" - } - assert { - condition = flatten(data.aws_iam_policy_document.assume_environment_deploy_role.statement[0].resources) == ["arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy", - "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy", - "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy"] - error_message = "Unexpected resources" - } -} - -run "test_codebuild_manifests" { - command = plan - - assert { - condition = aws_codebuild_project.codebase_deploy_manifests.name == "my-app-my-codebase-codebase-pipeline-deploy-manifests" - error_message = "Should be: 'my-app-my-codebase-codebase-deploy-manifests'" - } - assert { - condition = aws_codebuild_project.codebase_deploy_manifests.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.build_timeout == 5 - error_message = "Should be: 5" - } - assert { - condition = one(aws_codebuild_project.codebase_deploy_manifests.artifacts).type == "CODEPIPELINE" - error_message = "Should be: 'CODEPIPELINE'" - } - assert { - condition = one(aws_codebuild_project.codebase_deploy_manifests.cache).type == "S3" - error_message = "Should be: 'S3'" - } - assert { - condition = one(aws_codebuild_project.codebase_deploy_manifests.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.environment).compute_type == "BUILD_GENERAL1_SMALL" - error_message = "Should be: 'BUILD_GENERAL1_SMALL'" - } - assert { - condition = one(aws_codebuild_project.codebase_deploy_manifests.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.environment).type == "LINUX_CONTAINER" - error_message = "Should be: 'LINUX_CONTAINER'" - } - assert { - condition = one(aws_codebuild_project.codebase_deploy_manifests.environment).image_pull_credentials_type == "CODEBUILD" - error_message = "Should be: 'CODEBUILD'" - } - assert { - condition = aws_codebuild_project.codebase_deploy_manifests.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.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.source).type == "CODEPIPELINE" - error_message = "Should be: 'CODEPIPELINE'" - } - assert { - condition = length(regexall(".*\"exported-variables\":\\[\"CLUSTER_NAME\".*", aws_codebuild_project.codebase_deploy_manifests.source[0].buildspec)) > 0 - error_message = "Should contain: '\"exported-variables\":[\"CLUSTER_NAME\"'" - } - assert { - condition = jsonencode(aws_codebuild_project.codebase_deploy_manifests.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_codebuild_deploy" { @@ -962,8 +819,8 @@ run "test_main_pipeline" { error_message = "Should be: ${jsonencode(var.expected_tags)}" } assert { - condition = length(aws_codepipeline.codebase_pipeline[0].stage) == 3 - error_message = "Should be: 3" + condition = length(aws_codepipeline.codebase_pipeline[0].stage) == 2 + error_message = "Should be: 2" } # Source stage @@ -1008,14 +865,16 @@ run "test_main_pipeline" { error_message = "Should be: branch-main" } - # Create-Deploy-Manifests stage + # Deploy dev environment stage assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[1].name == "Create-Deploy-Manifests" - error_message = "Should be: Create-Deploy-Manifests" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].name == "Deploy-dev" + error_message = "Should be: Deploy-dev" } + + # Deploy service-1 action assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].name == "CreateManifests" - error_message = "Should be: CreateManifests" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].name == "service-1" + error_message = "Should be: service-1" } assert { condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].category == "Build" @@ -1038,99 +897,55 @@ run "test_main_pipeline" { 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-codebase-pipeline-deploy-manifests" - error_message = "Should be: my-app-my-codebase-codebase-deploy-manifests" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: my-app-my-codebase-codebase-pipeline-deploy" } 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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"dev\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].run_order == 2 + error_message = "Run order incorrect" } - # 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" - } + # Deploy service-2 action assert { - condition = length(aws_codepipeline.codebase_pipeline[0].stage[2].action[0].input_artifacts) == 1 - error_message = "Input artifacts incorrect" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].name == "service-2" + error_message = "Should be: service-1" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].input_artifacts[0] == "manifest_output" - error_message = "Input artifacts incorrect" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].category == "Build" + error_message = "Should be: Build" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].run_order == 2 - error_message = "Run order incorrect" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].owner == "AWS" + error_message = "Should be: AWS" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy" - error_message = "Role ARN incorrect" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].provider == "CodeBuild" + error_message = "Should be: CodeBuild" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_DEV}" - error_message = "Configuration ClusterName incorrect" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].version == "1" + error_message = "Should be: 1" } 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" + condition = one(aws_codepipeline.codebase_pipeline[0].stage[1].action[1].input_artifacts) == "source_output" + error_message = "Should be: source_output" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].configuration.FileName == "image-definitions-service-1.json" - error_message = "Configuration FileName incorrect" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].configuration.ProjectName == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: my-app-my-codebase-codebase-pipeline-deploy" } - - # Deploy service-2 action assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].name == "service-2" - error_message = "Action name incorrect" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"dev\"},{\"name\":\"SERVICE\",\"value\":\"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" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 3 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].run_order == 3 error_message = "Run order incorrect" } - assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy" - error_message = "Role ARN 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" { @@ -1145,8 +960,8 @@ run "test_tagged_pipeline" { error_message = "Should be: 'tag-latest'" } assert { - condition = length(aws_codepipeline.codebase_pipeline[1].stage) == 4 - error_message = "Should be: 4" + condition = length(aws_codepipeline.codebase_pipeline[1].stage) == 3 + error_message = "Should be: 3" } # Source stage @@ -1155,160 +970,98 @@ run "test_tagged_pipeline" { 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-codebase-pipeline-deploy-manifests" - error_message = "Should be: my-app-my-codebase-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" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].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].role_arn == "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy" - error_message = "Role ARN incorrect" - } - assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_STAGING}" - error_message = "Configuration ClusterName incorrect" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].name == "service-1" + error_message = "Should be: service-1" } 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" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"staging\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].configuration.FileName == "image-definitions-service-1.json" - error_message = "Configuration FileName incorrect" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].run_order == 2 + error_message = "Run order 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].role_arn == "arn:aws:iam::000123456789:role/my-app-staging-codebase-pipeline-deploy" - error_message = "Role ARN incorrect" - } - assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_STAGING}" - error_message = "Configuration ClusterName incorrect" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[1].name == "service-2" + error_message = "Should be: service-1" } 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" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"staging\"},{\"name\":\"SERVICE\",\"value\":\"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" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.FileName == "image-definitions-service-2.json" - error_message = "Configuration FileName incorrect" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[1].run_order == 3 + error_message = "Run order incorrect" } # Deploy prod environment stage assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].name == "Deploy-prod" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].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" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].name == "Approve-prod" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].category == "Approval" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].category == "Approval" error_message = "Action category incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].owner == "AWS" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].owner == "AWS" error_message = "Action owner incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].provider == "Manual" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].provider == "Manual" error_message = "Action provider incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].version == "1" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[0].version == "1" error_message = "Action Version incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[0].run_order == 1 + condition = aws_codepipeline.codebase_pipeline[1].stage[2].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].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy" - error_message = "Role ARN incorrect" - } - assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_PROD}" - error_message = "Configuration ClusterName incorrect" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].name == "service-1" + error_message = "Should be: service-1" } 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" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"prod\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[1].configuration.FileName == "image-definitions-service-1.json" - error_message = "Configuration FileName incorrect" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].run_order == 2 + error_message = "Run order 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].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy" - error_message = "Role ARN incorrect" - } - assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].configuration.ClusterName == "#{build_manifest.CLUSTER_NAME_PROD}" - error_message = "Configuration ClusterName incorrect" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[2].name == "service-2" + error_message = "Should be: service-1" } 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" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[2].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"prod\"},{\"name\":\"SERVICE\",\"value\":\"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" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[3].action[2].configuration.FileName == "image-definitions-service-2.json" - error_message = "Configuration FileName incorrect" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[2].run_order == 3 + error_message = "Run order incorrect" } } @@ -1403,12 +1156,12 @@ run "test_manual_release_pipeline" { } # Deploy stage - - # Deploy service-1 action assert { condition = aws_codepipeline.manual_release_pipeline.stage[1].name == "Deploy" error_message = "Should be: Deploy" } + + # Deploy service-1 action assert { condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].name == "service-1" error_message = "Should be: service-1" @@ -1447,10 +1200,6 @@ run "test_manual_release_pipeline" { } # Deploy service-2 action - assert { - condition = aws_codepipeline.manual_release_pipeline.stage[1].name == "Deploy" - error_message = "Should be: Deploy" - } assert { condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].name == "service-2" error_message = "Should be: service-1" @@ -1481,7 +1230,7 @@ run "test_manual_release_pipeline" { } assert { condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"#{variables.ENVIRONMENT}\"},{\"name\":\"SERVICE\",\"value\":\"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 ${jsonencode(aws_codepipeline.manual_release_pipeline.stage[1].action[0].configuration.EnvironmentVariables)}" + error_message = "Configuration environment variables incorrect" } assert { condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].run_order == 3 @@ -1601,52 +1350,43 @@ run "test_pipeline_single_run_group" { ] } - 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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].name == "service-1" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].run_order == 2 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].run_order == 2 error_message = "Run order incorrect" } - assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].role_arn == "arn:aws:iam::000123456789:role/my-app-dev-codebase-pipeline-deploy" - error_message = "Role ARN incorrect" - } # service-2 assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].name == "service-2" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].name == "service-2" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 2 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[2].name == "service-3" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].run_order == 2 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[3].name == "service-4" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].run_order == 2 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[3].run_order == 2 error_message = "Run order incorrect" } } @@ -1682,78 +1422,73 @@ run "test_pipeline_multiple_run_groups" { ] } - 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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].name == "service-1" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].run_order == 2 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].name == "service-2" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 3 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[2].name == "service-3" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].run_order == 3 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[3].name == "service-4" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].run_order == 4 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[4].name == "service-5" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[4].run_order == 5 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[5].name == "service-6" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[5].run_order == 5 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[6].name == "service-7" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[6].run_order == 5 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[6].run_order == 5 error_message = "Run order incorrect" } } @@ -1792,114 +1527,105 @@ run "test_pipeline_multiple_run_groups_multiple_environment_approval" { ] } - 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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].name == "service-1" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].run_order == 2 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].name == "service-2" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 3 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[2].name == "service-3" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].run_order == 3 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[3].name == "service-4" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].run_order == 4 + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[3].run_order == 4 error_message = "Run order incorrect" } # Prod assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].name == "Deploy-prod" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].name == "Deploy-prod" error_message = "Should be: Deploy-prod" } # Approval assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[0].name == "Approve-prod" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[0].name == "Approve-prod" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[0].run_order == 1 + condition = aws_codepipeline.codebase_pipeline[0].stage[2].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].name == "service-1" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[1].run_order == 2 + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[1].run_order == 2 error_message = "Run order incorrect" } - assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[1].role_arn == "arn:aws:iam::123456789000:role/my-app-prod-codebase-pipeline-deploy" - error_message = "Role ARN incorrect" - } # service-2 assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[2].name == "service-2" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[2].name == "service-2" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[2].run_order == 3 + condition = aws_codepipeline.codebase_pipeline[0].stage[2].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[3].name == "service-3" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[3].run_order == 3 + condition = aws_codepipeline.codebase_pipeline[0].stage[2].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" + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[4].name == "service-4" error_message = "Action name incorrect" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[3].action[4].run_order == 4 + condition = aws_codepipeline.codebase_pipeline[0].stage[2].action[4].run_order == 4 error_message = "Run order incorrect" } } From 374630f577186b7062fa3f93fa26e6ce9b9acb5b Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Wed, 18 Dec 2024 12:26:52 +0000 Subject: [PATCH 65/71] Standardise capitalisation --- codebase-pipelines/buildspec-deploy.yml | 39 +++++++++++++------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/codebase-pipelines/buildspec-deploy.yml b/codebase-pipelines/buildspec-deploy.yml index cfcea18e8..3eae652f4 100644 --- a/codebase-pipelines/buildspec-deploy.yml +++ b/codebase-pipelines/buildspec-deploy.yml @@ -1,49 +1,52 @@ version: 0.2 +env: + variables: + DEPLOY_TIMEOUT: 1800 + phases: build: commands: - set -e - - TASK_FAMILY="${APPLICATION}-${ENVIRONMENT}-${SERVICE}" - - CLUSTER="${APPLICATION}-${ENVIRONMENT}" - - IMAGE_URI="${REPOSITORY_URL}:${IMAGE_TAG}" + - task_family="${APPLICATION}-${ENVIRONMENT}-${SERVICE}" + - cluster="${APPLICATION}-${ENVIRONMENT}" + - image_uri="${REPOSITORY_URL}:${IMAGE_TAG}" # Assume environment role - - ACCOUNT_ID=$(echo ${ENV_CONFIG} | jq -c -r .${ENVIRONMENT}.accounts.deploy.id) - - assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::$ACCOUNT_ID:role/${APPLICATION}-${ENVIRONMENT}-codebase-pipeline-deploy" --role-session-name "${ENVIRONMENT}-codebase-pipeline-deploy") + - account_id=$(echo ${ENV_CONFIG} | jq -c -r .${ENVIRONMENT}.accounts.deploy.id) + - assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::${account_id}:role/${APPLICATION}-${ENVIRONMENT}-codebase-pipeline-deploy" --role-session-name "${ENVIRONMENT}-codebase-pipeline-deploy") - export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId) - export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey) - export AWS_SESSION_TOKEN=$(echo $assumed_role | jq -r .Credentials.SessionToken) # Get service name - - SERVICE_NAME=$(aws ecs list-services --cluster ${CLUSTER} | jq -r '.serviceArns[] | select(contains("'${CLUSTER}'-'${SERVICE}'-Service"))' | cut -d '/' -f3) + - service_name=$(aws ecs list-services --cluster ${cluster} | jq -r '.serviceArns[] | select(contains("'${cluster}'-'${SERVICE}'-Service"))' | cut -d '/' -f3) # Update task definition - - TASK_DEFINITION=$(aws ecs describe-task-definition --task-definition "${TASK_FAMILY}") - - NEW_TASK_DEFINITION=$(echo ${TASK_DEFINITION} | jq --arg IMAGE "$IMAGE_URI" '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') - - NEW_TASK_INFO=$(aws ecs register-task-definition --cli-input-json "$NEW_TASK_DEFINITION") - - NEW_REVISION=$(echo $NEW_TASK_INFO | jq '.taskDefinition.revision') + - task_definition=$(aws ecs describe-task-definition --task-definition "${task_family}") + - new_task_definition=$(echo ${task_definition} | jq --arg IMAGE "$image_uri" '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') + - new_task_info=$(aws ecs register-task-definition --cli-input-json "${new_task_definition}") + - new_revision=$(echo ${new_task_info} | jq '.taskDefinition.revision') # Start deployment - start=$( date +%s ) - - timeout=1800 - deploy_status="IN_PROGRESS" - - aws ecs update-service --cluster "${CLUSTER}" --service "${SERVICE_NAME}" --task-definition "${TASK_FAMILY}:${NEW_REVISION}" > /dev/null 2>&1 + - aws ecs update-service --cluster "${cluster}" --service "${service_name}" --task-definition "${task_family}:${new_revision}" > /dev/null 2>&1 # Check deployment status - | - while [[ "$deploy_status" == "IN_PROGRESS" || "$deploy_status" == "PENDING" || "$deploy_status" == "ROLLBACK_IN_PROGRESS" ]]; + while [[ "${deploy_status}" == "IN_PROGRESS" || "${deploy_status}" == "PENDING" || "${deploy_status}" == "ROLLBACK_IN_PROGRESS" ]]; do sleep 10 now=$( date +%s ) elapsed=$(( now-start )) - deploy_status=$(aws ecs list-service-deployments --cluster "${CLUSTER}" --service "${SERVICE_NAME}" --created-at "after=${start}" | jq -r '.serviceDeployments[0].status') - echo "Deployment status after $elapsed seconds: $deploy_status" + deploy_status=$(aws ecs list-service-deployments --cluster "${cluster}" --service "${service_name}" --created-at "after=${start}" | jq -r '.serviceDeployments[0].status') + echo "Deployment status after ${elapsed} seconds: ${deploy_status}" - if [[ $elapsed -gt $timeout ]]; then - echo "Error: deployment not completed in $timeout seconds" + if [[ ${elapsed} -gt ${DEPLOY_TIMEOUT} ]]; then + echo "Error: deployment not completed in ${DEPLOY_TIMEOUT} seconds" exit 1 fi done # Check deployment success - | - if [ "$deploy_status" != "SUCCEEDED" ]; then + if [ "${deploy_status}" != "SUCCESSFUL" ]; then echo "Error: deployment did not succeed" exit 1 fi From fe2ba342b279e133f634148c73daca30eb6a4708 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Wed, 18 Dec 2024 17:46:17 +0000 Subject: [PATCH 66/71] Refactor locals to remove unnecessary config from environment variable --- codebase-pipelines/artifactstore.tf | 2 +- codebase-pipelines/buildspec-deploy.yml | 2 +- codebase-pipelines/locals.tf | 26 +++++++++++------------- codebase-pipelines/tests/unit.tftest.hcl | 8 ++++++++ 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/codebase-pipelines/artifactstore.tf b/codebase-pipelines/artifactstore.tf index 3258fe8a6..6fcb9a89a 100644 --- a/codebase-pipelines/artifactstore.tf +++ b/codebase-pipelines/artifactstore.tf @@ -55,7 +55,7 @@ data "aws_iam_policy_document" "artifact_store_bucket_policy" { type = "AWS" identifiers = [ for env in local.pipeline_environments : - "arn:aws:iam::${env.account.id}:role/${var.application}-${env.name}-codebase-pipeline-deploy" + "arn:aws:iam::${env.account}:role/${var.application}-${env.name}-codebase-pipeline-deploy" ] } actions = [ diff --git a/codebase-pipelines/buildspec-deploy.yml b/codebase-pipelines/buildspec-deploy.yml index 3eae652f4..1346cd8a9 100644 --- a/codebase-pipelines/buildspec-deploy.yml +++ b/codebase-pipelines/buildspec-deploy.yml @@ -12,7 +12,7 @@ phases: - cluster="${APPLICATION}-${ENVIRONMENT}" - image_uri="${REPOSITORY_URL}:${IMAGE_TAG}" # Assume environment role - - account_id=$(echo ${ENV_CONFIG} | jq -c -r .${ENVIRONMENT}.accounts.deploy.id) + - account_id=$(echo ${ENV_CONFIG} | jq -c -r .${ENVIRONMENT}.account) - assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::${account_id}:role/${APPLICATION}-${ENVIRONMENT}-codebase-pipeline-deploy" --role-session-name "${ENVIRONMENT}-codebase-pipeline-deploy") - export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId) - export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey) diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index e631678cd..a03565019 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -17,26 +17,24 @@ locals { tagged_pipeline = length([for pipeline in var.pipelines : true if lookup(pipeline, "tag", null) == true]) > 0 base_env_config = { - for name, config in var.env_config : name => merge(lookup(var.env_config, "*", {}), config) if name != "*" + for name, config in var.env_config : name => { + account : lookup(lookup(lookup(merge(lookup(var.env_config, "*", {}), config), "accounts", {}), "deploy", {}), "id", {}) + } if name != "*" } - deploy_account_ids = distinct([for env in local.base_env_config : env.accounts.deploy.id]) - - pipeline_environment_account_map = { - for id, val in var.pipelines : id => { - "environments" : [ - for name, env in val.environments : merge(env, { - "account" : lookup(local.base_env_config, env.name, {}).accounts.deploy - }) - ] - } - } + deploy_account_ids = distinct([for env in local.base_env_config : env.account]) pipeline_map = { - for id, val in var.pipelines : id => merge(val, local.pipeline_environment_account_map[id]) + for id, val in var.pipelines : id => merge(val, { + environments : [ + for name, env in val.environments : merge(env, lookup(local.base_env_config, env.name, {})) + ] + }) } - pipeline_environments = flatten([for pipeline in local.pipeline_map : [for env in pipeline.environments : env]]) + pipeline_environments = distinct(flatten([ + for pipeline in local.pipeline_map : [for env in pipeline.environments : env] + ])) services = sort(flatten([ for run_group in var.services : [for service in flatten(values(run_group)) : service] diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index be599f043..e8dca4a12 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -739,6 +739,14 @@ run "test_codebuild_deploy" { condition = one(aws_codebuild_project.codebase_deploy.environment).image_pull_credentials_type == "CODEBUILD" error_message = "Should be: 'CODEBUILD'" } + assert { + condition = one(aws_codebuild_project.codebase_deploy.environment).environment_variable[0].name == "ENV_CONFIG" + error_message = "Should be: 'ENV_CONFIG' ${jsonencode(one(aws_codebuild_project.codebase_deploy.environment).environment_variable[0])}" + } + assert { + condition = one(aws_codebuild_project.codebase_deploy.environment).environment_variable[0].value == "{\"dev\":{\"account\":\"000123456789\"},\"prod\":{\"account\":\"123456789000\"},\"staging\":{\"account\":\"000123456789\"}}" + error_message = "Incorrect value" + } assert { condition = aws_codebuild_project.codebase_deploy.logs_config[0].cloudwatch_logs[ 0 From 0d19cfaa1b7076e1a2ee073ab694c4c21454047a Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Thu, 19 Dec 2024 12:29:40 +0000 Subject: [PATCH 67/71] Add checks for environment and image_tag pipeline variables --- codebase-pipelines/buildspec-deploy.yml | 25 ++++++++++++++++++++++-- codebase-pipelines/codepipeline.tf | 2 ++ codebase-pipelines/iam.tf | 6 ++++++ codebase-pipelines/tests/unit.tftest.hcl | 24 +++++++++++++++-------- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/codebase-pipelines/buildspec-deploy.yml b/codebase-pipelines/buildspec-deploy.yml index 1346cd8a9..a0102f21d 100644 --- a/codebase-pipelines/buildspec-deploy.yml +++ b/codebase-pipelines/buildspec-deploy.yml @@ -8,26 +8,46 @@ phases: build: commands: - set -e + + # Check if the specified image tag exists + - | + if ! aws ecr describe-images --repository-name "${REPOSITORY_NAME}" --image-ids "imageTag=${IMAGE_TAG}" > /dev/null 2>&1; then + echo "Error: image tag ${IMAGE_TAG} not found in repository ${REPOSITORY_NAME}" + exit 1 + fi + + # Check environment exists in config + - | + if [ $(echo $ENV_CONFIG | jq 'has('\"${ENVIRONMENT}\"')') == "false" ]; then + echo "Error: environment ${ENVIRONMENT} not listed in environment config" + exit 1 + fi + - task_family="${APPLICATION}-${ENVIRONMENT}-${SERVICE}" - cluster="${APPLICATION}-${ENVIRONMENT}" - image_uri="${REPOSITORY_URL}:${IMAGE_TAG}" + # Assume environment role - account_id=$(echo ${ENV_CONFIG} | jq -c -r .${ENVIRONMENT}.account) - assumed_role=$(aws sts assume-role --role-arn "arn:aws:iam::${account_id}:role/${APPLICATION}-${ENVIRONMENT}-codebase-pipeline-deploy" --role-session-name "${ENVIRONMENT}-codebase-pipeline-deploy") - export AWS_ACCESS_KEY_ID=$(echo $assumed_role | jq -r .Credentials.AccessKeyId) - export AWS_SECRET_ACCESS_KEY=$(echo $assumed_role | jq -r .Credentials.SecretAccessKey) - export AWS_SESSION_TOKEN=$(echo $assumed_role | jq -r .Credentials.SessionToken) + # Get service name - - service_name=$(aws ecs list-services --cluster ${cluster} | jq -r '.serviceArns[] | select(contains("'${cluster}'-'${SERVICE}'-Service"))' | cut -d '/' -f3) + - service_name=$(aws ecs list-services --cluster "${cluster}" | jq -r '.serviceArns[] | select(contains("'${cluster}'-'${SERVICE}'-Service"))' | cut -d '/' -f3) + # Update task definition - task_definition=$(aws ecs describe-task-definition --task-definition "${task_family}") - - new_task_definition=$(echo ${task_definition} | jq --arg IMAGE "$image_uri" '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') + - new_task_definition=$(echo ${task_definition} | jq '.taskDefinition | .containerDefinitions[0].image = '\"${image_uri}\"' | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') - new_task_info=$(aws ecs register-task-definition --cli-input-json "${new_task_definition}") - new_revision=$(echo ${new_task_info} | jq '.taskDefinition.revision') + # Start deployment - start=$( date +%s ) - deploy_status="IN_PROGRESS" - aws ecs update-service --cluster "${cluster}" --service "${service_name}" --task-definition "${task_family}:${new_revision}" > /dev/null 2>&1 + # Check deployment status - | while [[ "${deploy_status}" == "IN_PROGRESS" || "${deploy_status}" == "PENDING" || "${deploy_status}" == "ROLLBACK_IN_PROGRESS" ]]; @@ -44,6 +64,7 @@ phases: exit 1 fi done + # Check deployment success - | if [ "${deploy_status}" != "SUCCESSFUL" ]; then diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index e51e866d1..bf3f5c078 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -77,6 +77,7 @@ resource "aws_codepipeline" "codebase_pipeline" { { name : "ENVIRONMENT", value : stage.value.name }, { name : "SERVICE", value : action.value.name }, { name : "REPOSITORY_URL", value : local.repository_url }, + { name : "REPOSITORY_NAME", value : local.ecr_name }, { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } ]) } @@ -157,6 +158,7 @@ resource "aws_codepipeline" "manual_release_pipeline" { { name : "ENVIRONMENT", value : "#{variables.ENVIRONMENT}" }, { name : "SERVICE", value : action.value.name }, { name : "REPOSITORY_URL", value : local.repository_url }, + { name : "REPOSITORY_NAME", value : local.ecr_name }, { name : "IMAGE_TAG", value : "#{variables.IMAGE_TAG}" } ]) } diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 0319913b4..46e86785c 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -244,6 +244,12 @@ resource "aws_iam_role_policy" "log_access_for_codebuild_deploy" { policy = data.aws_iam_policy_document.log_access.json } +resource "aws_iam_role_policy" "ecr_access_for_codebuild_deploy" { + name = "ecr-access" + role = aws_iam_role.codebase_deploy.name + policy = data.aws_iam_policy_document.ecr_access_for_codebase_pipeline.json +} + resource "aws_iam_role_policy" "environment_deploy_role_access_for_codebuild_deploy" { name = "environment-deploy-role-access" role = aws_iam_role.codebase_deploy.name diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index e8dca4a12..ffb83ac8b 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -443,6 +443,14 @@ run "test_iam" { condition = aws_iam_role_policy.log_access_for_codebuild_deploy.role == "my-app-my-codebase-codebase-pipeline-deploy" error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy'" } + assert { + condition = aws_iam_role_policy.ecr_access_for_codebuild_deploy.name == "ecr-access" + error_message = "Should be: 'ecr-access'" + } + assert { + condition = aws_iam_role_policy.ecr_access_for_codebuild_deploy.role == "my-app-my-codebase-codebase-pipeline-deploy" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-deploy'" + } assert { condition = aws_iam_role_policy.environment_deploy_role_access_for_codebuild_deploy.name == "environment-deploy-role-access" error_message = "Should be: 'environment-deploy-role-access'" @@ -909,7 +917,7 @@ run "test_main_pipeline" { error_message = "Should be: my-app-my-codebase-codebase-pipeline-deploy" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"dev\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"dev\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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\":\"REPOSITORY_NAME\",\"value\":\"my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" error_message = "Configuration environment variables incorrect" } assert { @@ -947,7 +955,7 @@ run "test_main_pipeline" { error_message = "Should be: my-app-my-codebase-codebase-pipeline-deploy" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"dev\"},{\"name\":\"SERVICE\",\"value\":\"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}\"}]" + condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"dev\"},{\"name\":\"SERVICE\",\"value\":\"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\":\"REPOSITORY_NAME\",\"value\":\"my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" error_message = "Configuration environment variables incorrect" } assert { @@ -990,7 +998,7 @@ run "test_tagged_pipeline" { error_message = "Should be: service-1" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"staging\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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}\"}]" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"staging\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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\":\"REPOSITORY_NAME\",\"value\":\"my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" error_message = "Configuration environment variables incorrect" } assert { @@ -1004,7 +1012,7 @@ run "test_tagged_pipeline" { error_message = "Should be: service-1" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"staging\"},{\"name\":\"SERVICE\",\"value\":\"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}\"}]" + condition = aws_codepipeline.codebase_pipeline[1].stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"staging\"},{\"name\":\"SERVICE\",\"value\":\"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\":\"REPOSITORY_NAME\",\"value\":\"my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" error_message = "Configuration environment variables incorrect" } assert { @@ -1050,7 +1058,7 @@ run "test_tagged_pipeline" { error_message = "Should be: service-1" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"prod\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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}\"}]" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"prod\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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\":\"REPOSITORY_NAME\",\"value\":\"my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" error_message = "Configuration environment variables incorrect" } assert { @@ -1064,7 +1072,7 @@ run "test_tagged_pipeline" { error_message = "Should be: service-1" } assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[2].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"prod\"},{\"name\":\"SERVICE\",\"value\":\"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}\"}]" + condition = aws_codepipeline.codebase_pipeline[1].stage[2].action[2].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"prod\"},{\"name\":\"SERVICE\",\"value\":\"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\":\"REPOSITORY_NAME\",\"value\":\"my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" error_message = "Configuration environment variables incorrect" } assert { @@ -1199,7 +1207,7 @@ run "test_manual_release_pipeline" { error_message = "Should be: my-app-my-codebase-codebase-pipeline-deploy" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"#{variables.ENVIRONMENT}\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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}\"}]" + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"#{variables.ENVIRONMENT}\"},{\"name\":\"SERVICE\",\"value\":\"service-1\"},{\"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\":\"REPOSITORY_NAME\",\"value\":\"my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" error_message = "Configuration environment variables incorrect" } assert { @@ -1237,7 +1245,7 @@ run "test_manual_release_pipeline" { error_message = "Should be: my-app-my-codebase-codebase-pipeline-deploy" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"#{variables.ENVIRONMENT}\"},{\"name\":\"SERVICE\",\"value\":\"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}\"}]" + condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].configuration.EnvironmentVariables == "[{\"name\":\"APPLICATION\",\"value\":\"my-app\"},{\"name\":\"ENVIRONMENT\",\"value\":\"#{variables.ENVIRONMENT}\"},{\"name\":\"SERVICE\",\"value\":\"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\":\"REPOSITORY_NAME\",\"value\":\"my-app/my-codebase\"},{\"name\":\"IMAGE_TAG\",\"value\":\"#{variables.IMAGE_TAG}\"}]" error_message = "Configuration environment variables incorrect" } assert { From 4084d0f56bb6e0ed49da5296f4b4effdd4b89cd0 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Fri, 20 Dec 2024 13:09:54 +0000 Subject: [PATCH 68/71] Add custom python script block to set automatic stage rollback --- codebase-pipelines/codepipeline.tf | 21 ++++++++ .../test_update_pipeline.py | 50 +++++++++++++++++++ .../custom_pipeline_update/update_pipeline.py | 33 ++++++++++++ codebase-pipelines/locals.tf | 5 ++ 4 files changed, 109 insertions(+) create mode 100644 codebase-pipelines/custom_pipeline_update/test_update_pipeline.py create mode 100644 codebase-pipelines/custom_pipeline_update/update_pipeline.py diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index bf3f5c078..0e806ba2d 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -168,3 +168,24 @@ resource "aws_codepipeline" "manual_release_pipeline" { tags = local.tags } + +# This is a temporary workaround until automatic stage rollback is implemented in terraform-provider-aws +# https://github.com/hashicorp/terraform-provider-aws/issues/37244 +resource "terraform_data" "update_pipeline" { + provisioner "local-exec" { + command = "python ${path.module}/custom_pipeline_update/update_pipeline.py" + quiet = true + environment = { + PIPELINES = jsonencode(local.pipeline_names) + } + } + triggers_replace = [ + aws_codepipeline.codebase_pipeline, + aws_codepipeline.manual_release_pipeline, + file("${path.module}/custom_pipeline_update/update_pipeline.py") + ] + depends_on = [ + aws_codepipeline.codebase_pipeline, + aws_codepipeline.manual_release_pipeline + ] +} diff --git a/codebase-pipelines/custom_pipeline_update/test_update_pipeline.py b/codebase-pipelines/custom_pipeline_update/test_update_pipeline.py new file mode 100644 index 000000000..c9ab65a17 --- /dev/null +++ b/codebase-pipelines/custom_pipeline_update/test_update_pipeline.py @@ -0,0 +1,50 @@ +import pytest +import unittest +from unittest.mock import MagicMock, patch + +from update_pipeline import update_pipeline_stage_failure + + +class TestUpdatePipeline(unittest.TestCase): + + def test_update_pipeline_stage_failure_sets_rollback(self): + with patch("boto3.client") as mock_boto_client: + mock_client = MagicMock() + mock_boto_client.return_value = mock_client + + mock_client.get_pipeline.return_value = { + "pipeline": { + "name": "test-pipeline", + "stages": [ + { + "name": "Deploy", + } + ] + } + } + + update_pipeline_stage_failure(["test-pipeline"]) + + call_args = mock_client.update_pipeline.call_args[1] + assert call_args["pipeline"]["stages"][0]["onFailure"]["result"] == "ROLLBACK" + + def test_update_pipeline_stage_failure_does_not_set_rollback(self): + with patch("boto3.client") as mock_boto_client: + mock_client = MagicMock() + mock_boto_client.return_value = mock_client + + mock_client.get_pipeline.return_value = { + "pipeline": { + "name": "test-pipeline", + "stages": [ + { + "name": "Source", + } + ] + } + } + + with pytest.raises(ValueError) as error_msg: + update_pipeline_stage_failure(["test-pipeline"]) + + assert "Stage Deploy not found in pipeline test-pipeline" in str(error_msg.value) diff --git a/codebase-pipelines/custom_pipeline_update/update_pipeline.py b/codebase-pipelines/custom_pipeline_update/update_pipeline.py new file mode 100644 index 000000000..a551bce72 --- /dev/null +++ b/codebase-pipelines/custom_pipeline_update/update_pipeline.py @@ -0,0 +1,33 @@ +import os +import json +import boto3 +from typing import List + + +def update_pipeline_stage_failure(pipelines: List[str]): + for pipeline_name in pipelines: + print(f"Updating pipeline {pipeline_name}") + + client = boto3.client("codepipeline") + + response = client.get_pipeline(name=pipeline_name) + pipeline = response["pipeline"] + + stage_found = False + for stage in pipeline["stages"]: + if "Deploy" in stage["name"]: + stage["onFailure"] = { + "result": "ROLLBACK" + } + stage_found = True + + if not stage_found: + raise ValueError(f"Stage Deploy not found in pipeline {pipeline_name}") + + client.update_pipeline(pipeline=pipeline) + print(f"Updated Deploy stage onFailure property for {pipeline_name}") + + +if __name__ == "__main__": + pipelines = json.loads(os.environ['PIPELINES']) + update_pipeline_stage_failure(pipelines) diff --git a/codebase-pipelines/locals.tf b/codebase-pipelines/locals.tf index a03565019..ef8b188a1 100644 --- a/codebase-pipelines/locals.tf +++ b/codebase-pipelines/locals.tf @@ -52,4 +52,9 @@ locals { ] ] ]) + + pipeline_names = flatten(concat([for pipeline in local.pipeline_map : + "${var.application}-${var.codebase}-${pipeline.name}-codebase-pipeline"], + ["${var.application}-${var.codebase}-manual-release-pipeline"] + )) } From a7f63a3a9c909e632e3696ca6f946b45f8818ec3 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Mon, 23 Dec 2024 16:41:46 +0000 Subject: [PATCH 69/71] Ensure the correct containers image is updated; Restore desired taskcount from service manifest --- codebase-pipelines/buildspec-deploy.yml | 34 ++++++++++++- codebase-pipelines/codepipeline.tf | 66 ++++++++++++++++++++----- codebase-pipelines/iam.tf | 8 ++- 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/codebase-pipelines/buildspec-deploy.yml b/codebase-pipelines/buildspec-deploy.yml index a0102f21d..32ecb30a3 100644 --- a/codebase-pipelines/buildspec-deploy.yml +++ b/codebase-pipelines/buildspec-deploy.yml @@ -5,6 +5,10 @@ env: DEPLOY_TIMEOUT: 1800 phases: + install: + commands: + - pip install yq --quiet + build: commands: - set -e @@ -39,14 +43,40 @@ phases: # Update task definition - task_definition=$(aws ecs describe-task-definition --task-definition "${task_family}") - - new_task_definition=$(echo ${task_definition} | jq '.taskDefinition | .containerDefinitions[0].image = '\"${image_uri}\"' | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') + - new_task_definition=$(echo ${task_definition} | jq '.taskDefinition | .containerDefinitions |= map(if .name == '\"${SERVICE}\"' then .image = '\"${image_uri}\"' else . end) | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') - new_task_info=$(aws ecs register-task-definition --cli-input-json "${new_task_definition}") - new_revision=$(echo ${new_task_info} | jq '.taskDefinition.revision') + # Get desired task count + - count=0 + - cd "${CODEBUILD_SRC_DIR}" + + # If count is a range, get the first value otherwise get count + - | + if [ $(yq '.count | type == "object"' copilot/web/manifest.yml) == "true" ]; then + count=$(yq '.count.range' copilot/web/manifest.yml | tr -d '"' | cut -d '-' -f1) + else + count=$(yq '.count' copilot/web/manifest.yml) + fi + + # Check for environment overrides + - | + if [ $(yq '.environments.'${ENVIRONMENT}'.count | type == "object"' copilot/web/manifest.yml) == "true" ]; then + env_count=$(yq '.environments.'${ENVIRONMENT}'.count.range' copilot/web/manifest.yml | tr -d '"' | cut -d '-' -f1) + else + env_count=$(yq '.environments.'${ENVIRONMENT}'.count' copilot/web/manifest.yml) + fi + + - | + if [[ "${env_count}" != "null" ]]; then + count=${env_count} + fi + # Start deployment + - echo "Deploying ${image_uri} to ${service_name} in ${cluster} with task count ${count}" - start=$( date +%s ) - deploy_status="IN_PROGRESS" - - aws ecs update-service --cluster "${cluster}" --service "${service_name}" --task-definition "${task_family}:${new_revision}" > /dev/null 2>&1 + - aws ecs update-service --cluster "${cluster}" --service "${service_name}" --task-definition "${task_family}:${new_revision}" --desired-count ${count} > /dev/null 2>&1 # Check deployment status - | diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 0e806ba2d..28ff84638 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -22,21 +22,41 @@ resource "aws_codepipeline" "codebase_pipeline" { } } +# 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 = "Source" action { - name = "Source" + name = "GitCheckout" category = "Source" owner = "AWS" - provider = "ECR" + provider = "CodeStarSourceConnection" version = "1" - namespace = "source_ecr" - output_artifacts = ["source_output"] + output_artifacts = ["deploy_source"] configuration = { - RepositoryName = local.ecr_name - ImageTag = coalesce(each.value.tag, false) ? "tag-latest" : "branch-${each.value.branch}" + ConnectionArn = data.aws_codestarconnections_connection.github_codestar_connection.arn + FullRepositoryId = "${var.repository}-deploy" + BranchName = "main" + DetectChanges = false } } } @@ -65,7 +85,7 @@ resource "aws_codepipeline" "codebase_pipeline" { category = "Build" owner = "AWS" provider = "CodeBuild" - input_artifacts = ["source_output"] + input_artifacts = ["deploy_source"] output_artifacts = [] version = "1" run_order = action.value.order + 1 @@ -118,20 +138,40 @@ resource "aws_codepipeline" "manual_release_pipeline" { } } +# 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 +# } +# } +# } + stage { name = "Source" action { - name = "Source" + name = "GitCheckout" category = "Source" owner = "AWS" - provider = "ECR" + provider = "CodeStarSourceConnection" version = "1" - namespace = "source_ecr" - output_artifacts = ["source_output"] + output_artifacts = ["deploy_source"] configuration = { - RepositoryName = local.ecr_name + ConnectionArn = data.aws_codestarconnections_connection.github_codestar_connection.arn + FullRepositoryId = "${var.repository}-deploy" + BranchName = "main" + DetectChanges = false } } } @@ -146,7 +186,7 @@ resource "aws_codepipeline" "manual_release_pipeline" { category = "Build" owner = "AWS" provider = "CodeBuild" - input_artifacts = ["source_output"] + input_artifacts = ["deploy_source"] output_artifacts = [] version = "1" run_order = action.value.order + 1 diff --git a/codebase-pipelines/iam.tf b/codebase-pipelines/iam.tf index 46e86785c..819f0b72d 100644 --- a/codebase-pipelines/iam.tf +++ b/codebase-pipelines/iam.tf @@ -121,7 +121,7 @@ data "aws_iam_policy_document" "ecr_access_for_codebuild_images" { } } -resource "aws_iam_role_policy" "codestar_connection_access" { +resource "aws_iam_role_policy" "codestar_connection_access_for_codebuild_images" { name = "codestar-connection-policy" role = aws_iam_role.codebase_image_build.name policy = data.aws_iam_policy_document.codestar_connection_access.json @@ -159,6 +159,12 @@ data "aws_iam_policy_document" "assume_codepipeline_role" { } } +resource "aws_iam_role_policy" "codestar_connection_access_for_codebase_pipeline" { + name = "codestar-connection-policy" + role = aws_iam_role.codebase_deploy_pipeline.name + policy = data.aws_iam_policy_document.codestar_connection_access.json +} + resource "aws_iam_role_policy" "ecr_access_for_codebase_pipeline" { name = "ecr-access" role = aws_iam_role.codebase_deploy_pipeline.name From eea7e9dfaf719377128b7e2083d46ceda5304d1e Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 24 Dec 2024 10:25:43 +0000 Subject: [PATCH 70/71] Update tests for source stage change --- codebase-pipelines/codepipeline.tf | 37 --------- codebase-pipelines/tests/unit.tftest.hcl | 97 ++++++++++++------------ 2 files changed, 48 insertions(+), 86 deletions(-) diff --git a/codebase-pipelines/codepipeline.tf b/codebase-pipelines/codepipeline.tf index 28ff84638..2adb5b91c 100644 --- a/codebase-pipelines/codepipeline.tf +++ b/codebase-pipelines/codepipeline.tf @@ -22,25 +22,6 @@ resource "aws_codepipeline" "codebase_pipeline" { } } -# 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 = "Source" @@ -138,24 +119,6 @@ resource "aws_codepipeline" "manual_release_pipeline" { } } -# 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 -# } -# } -# } - stage { name = "Source" diff --git a/codebase-pipelines/tests/unit.tftest.hcl b/codebase-pipelines/tests/unit.tftest.hcl index ffb83ac8b..4696d9d10 100644 --- a/codebase-pipelines/tests/unit.tftest.hcl +++ b/codebase-pipelines/tests/unit.tftest.hcl @@ -49,13 +49,6 @@ override_data { } } -override_data { - target = data.aws_iam_policy_document.assume_environment_deploy_role - values = { - json = "{\"Sid\": \"AssumeEnvironmentDeployRole\"}" - } -} - override_data { target = data.aws_iam_policy_document.environment_deploy_role_access values = { @@ -398,11 +391,11 @@ run "test_iam" { error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { - condition = aws_iam_role_policy.codestar_connection_access.name == "codestar-connection-policy" + condition = aws_iam_role_policy.codestar_connection_access_for_codebuild_images.name == "codestar-connection-policy" error_message = "Should be: 'codestar-connection-policy'" } assert { - condition = aws_iam_role_policy.codestar_connection_access.role == "my-app-my-codebase-codebase-pipeline-image-build" + condition = aws_iam_role_policy.codestar_connection_access_for_codebuild_images.role == "my-app-my-codebase-codebase-pipeline-image-build" error_message = "Should be: 'my-app-my-codebase-codebase-pipeline-image-build'" } assert { @@ -489,6 +482,14 @@ run "test_iam" { 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.codestar_connection_access_for_codebase_pipeline.name == "codestar-connection-policy" + error_message = "Should be: 'codestar-connection-policy'" + } + assert { + condition = aws_iam_role_policy.codestar_connection_access_for_codebase_pipeline.role == "my-app-my-codebase-codebase-pipeline" + error_message = "Should be: 'my-app-my-codebase-codebase-pipeline'" + } } run "test_iam_documents" { @@ -845,8 +846,8 @@ run "test_main_pipeline" { error_message = "Should be: Source" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].name == "Source" - error_message = "Should be: Source" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].name == "GitCheckout" + error_message = "Should be: GitCheckout" } assert { condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].category == "Source" @@ -857,28 +858,28 @@ run "test_main_pipeline" { error_message = "Should be: AWS" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].provider == "ECR" - error_message = "Should be: ECR" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].provider == "CodeStarSourceConnection" + error_message = "Should be: CodeStarSourceConnection" } 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" + condition = one(aws_codepipeline.codebase_pipeline[0].stage[0].action[0].output_artifacts) == "deploy_source" + error_message = "Should be: deploy_source" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].namespace == "source_ecr" - error_message = "Should be: source_ecr" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.FullRepositoryId == "my-repository-deploy" + error_message = "Should be: my-repository-deploy" } 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" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.BranchName == "main" + error_message = "Should be: main" } assert { - condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.ImageTag == "branch-main" - error_message = "Should be: branch-main" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.DetectChanges == "false" + error_message = "Should be: false" } # Deploy dev environment stage @@ -909,8 +910,8 @@ run "test_main_pipeline" { 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" + condition = one(aws_codepipeline.codebase_pipeline[0].stage[1].action[0].input_artifacts) == "deploy_source" + error_message = "Should be: deploy_source" } assert { condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-codebase-pipeline-deploy" @@ -947,8 +948,8 @@ run "test_main_pipeline" { error_message = "Should be: 1" } assert { - condition = one(aws_codepipeline.codebase_pipeline[0].stage[1].action[1].input_artifacts) == "source_output" - error_message = "Should be: source_output" + condition = one(aws_codepipeline.codebase_pipeline[0].stage[1].action[1].input_artifacts) == "deploy_source" + error_message = "Should be: deploy_source" } assert { condition = aws_codepipeline.codebase_pipeline[0].stage[1].action[1].configuration.ProjectName == "my-app-my-codebase-codebase-pipeline-deploy" @@ -980,12 +981,6 @@ run "test_tagged_pipeline" { error_message = "Should be: 3" } - # Source stage - assert { - condition = aws_codepipeline.codebase_pipeline[1].stage[0].action[0].configuration.ImageTag == "tag-latest" - error_message = "Should be: tag-latest" - } - # Deploy staging environment stage assert { condition = aws_codepipeline.codebase_pipeline[1].stage[1].name == "Deploy-staging" @@ -1135,40 +1130,44 @@ run "test_manual_release_pipeline" { # Source stage assert { - condition = aws_codepipeline.manual_release_pipeline.stage[0].name == "Source" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].name == "Source" error_message = "Should be: Source" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].name == "Source" - error_message = "Should be: Source" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].name == "GitCheckout" + error_message = "Should be: GitCheckout" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].category == "Source" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].category == "Source" error_message = "Should be: Source" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].owner == "AWS" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].owner == "AWS" error_message = "Should be: AWS" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].provider == "ECR" - error_message = "Should be: ECR" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].provider == "CodeStarSourceConnection" + error_message = "Should be: CodeStarSourceConnection" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].version == "1" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].version == "1" error_message = "Should be: 1" } assert { - condition = one(aws_codepipeline.manual_release_pipeline.stage[0].action[0].output_artifacts) == "source_output" - error_message = "Should be: source_output" + condition = one(aws_codepipeline.codebase_pipeline[0].stage[0].action[0].output_artifacts) == "deploy_source" + error_message = "Should be: deploy_source" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].namespace == "source_ecr" - error_message = "Should be: source_ecr" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.FullRepositoryId == "my-repository-deploy" + error_message = "Should be: my-repository-deploy" } assert { - condition = aws_codepipeline.manual_release_pipeline.stage[0].action[0].configuration.RepositoryName == "my-app/my-codebase" - error_message = "Should be: my-app/my-codebase" + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.BranchName == "main" + error_message = "Should be: main" + } + assert { + condition = aws_codepipeline.codebase_pipeline[0].stage[0].action[0].configuration.DetectChanges == "false" + error_message = "Should be: false" } # Deploy stage @@ -1199,8 +1198,8 @@ run "test_manual_release_pipeline" { error_message = "Should be: 1" } assert { - condition = one(aws_codepipeline.manual_release_pipeline.stage[1].action[0].input_artifacts) == "source_output" - error_message = "Should be: source_output" + condition = one(aws_codepipeline.manual_release_pipeline.stage[1].action[0].input_artifacts) == "deploy_source" + error_message = "Should be: deploy_source" } assert { condition = aws_codepipeline.manual_release_pipeline.stage[1].action[0].configuration.ProjectName == "my-app-my-codebase-codebase-pipeline-deploy" @@ -1237,8 +1236,8 @@ run "test_manual_release_pipeline" { error_message = "Should be: 1" } assert { - condition = one(aws_codepipeline.manual_release_pipeline.stage[1].action[1].input_artifacts) == "source_output" - error_message = "Should be: source_output" + condition = one(aws_codepipeline.manual_release_pipeline.stage[1].action[1].input_artifacts) == "deploy_source" + error_message = "Should be: deploy_source" } assert { condition = aws_codepipeline.manual_release_pipeline.stage[1].action[1].configuration.ProjectName == "my-app-my-codebase-codebase-pipeline-deploy" From a79d19e1cd4a67c81715b14d7eedb599d4012352 Mon Sep 17 00:00:00 2001 From: John Stainsby Date: Tue, 24 Dec 2024 10:44:03 +0000 Subject: [PATCH 71/71] Provider update changes to elasticache at_rest_encryption_enabled property --- elasticache-redis/e2e-tests/e2e.tftest.hcl | 2 +- elasticache-redis/tests/unit.tftest.hcl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/elasticache-redis/e2e-tests/e2e.tftest.hcl b/elasticache-redis/e2e-tests/e2e.tftest.hcl index de864d4f2..2a0d4166f 100644 --- a/elasticache-redis/e2e-tests/e2e.tftest.hcl +++ b/elasticache-redis/e2e-tests/e2e.tftest.hcl @@ -57,7 +57,7 @@ run "e2e_test" { } assert { - condition = aws_elasticache_replication_group.redis.at_rest_encryption_enabled == true + condition = aws_elasticache_replication_group.redis.at_rest_encryption_enabled == "true" error_message = "Invalid config for aws_elasticache_replication_group at_rest_encryption_enabled" } diff --git a/elasticache-redis/tests/unit.tftest.hcl b/elasticache-redis/tests/unit.tftest.hcl index 80852e233..ef55a38ed 100644 --- a/elasticache-redis/tests/unit.tftest.hcl +++ b/elasticache-redis/tests/unit.tftest.hcl @@ -94,7 +94,7 @@ run "aws_elasticache_replication_group_unit_test" { } assert { - condition = aws_elasticache_replication_group.redis.at_rest_encryption_enabled == true + condition = aws_elasticache_replication_group.redis.at_rest_encryption_enabled == "true" error_message = "Invalid config for aws_elasticache_replication_group at_rest_encryption_enabled" } @@ -174,7 +174,7 @@ run "aws_elasticache_replication_group_unit_test2" { } assert { - condition = aws_elasticache_replication_group.redis.at_rest_encryption_enabled == true + condition = aws_elasticache_replication_group.redis.at_rest_encryption_enabled == "true" error_message = "Invalid config for aws_elasticache_replication_group at_rest_encryption_enabled" }