diff --git a/.github/workflows/tf-module-test.yml b/.github/workflows/tf-module-test.yml new file mode 100644 index 00000000..f9ad4b93 --- /dev/null +++ b/.github/workflows/tf-module-test.yml @@ -0,0 +1,41 @@ +name: tf-module-test + +on: + workflow_dispatch: + pull_request: + paths: + - 'terraform/modules/**' + +concurrency: + group: tf-module-test + +env: + TENV_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + apply: + permissions: + contents: read + id-token: write + runs-on: codebuild-cdap-${{github.run_id}}-${{github.run_attempt}} + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + - uses: cmsgov/cdap/actions/setup-tenv@8343fb96563ce4b74c4dececee9b268f42bd4a40 + - uses: cmsgov/cdap/actions/setup-sops@84a6bcee5b70d63c44f8fec4f9b542cb5ec29a54 + - uses: cmsgov/cdap/actions/setup-yq@328406d6e1d435b4e3da598bcdab22e576c3945e + - uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 # v5.1.0 + with: + role-to-assume: arn:aws:iam::${{ secrets.NON_PROD_ACCOUNT }}:role/delegatedadmin/developer/cdap-test-github-actions + aws-region: ${{ vars.AWS_REGION }} + - uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47 + id: changed-dirs + with: + files: | + terraform/modules/** + dir_names: 'true' + - run: scripts/tf-module-test + env: + APP: cdap + ENV: test + CHANGED_DIRS: ${{ steps.changed-dirs.outputs.all_changed_files }} diff --git a/scripts/lib/tofu-destroy.sh b/scripts/lib/tofu-destroy.sh new file mode 100644 index 00000000..3e4f1b7c --- /dev/null +++ b/scripts/lib/tofu-destroy.sh @@ -0,0 +1,24 @@ +# Library to be sourced into scripts for running tofu destroy +# in a GitHub Action. + +echo "::group::$dir tofu destroy" + +export TF_VAR_app="$APP" +export TF_VAR_env="$ENV" +tofu_warning="" +tofu_error="" + +echo "Removing resources for $dir" +if ! tofu destroy -auto-approve; then + job_error=true + tofu_error="Error in tofu apply for $dir" +fi + +echo "::endgroup::" + +if [ -n "$tofu_warning" ]; then + echo "::warning::$tofu_warning" +fi +if [ -n "$tofu_error" ]; then + echo "::error::$tofu_error" +fi diff --git a/scripts/lib/tofu-init-plan-apply.sh b/scripts/lib/tofu-init-plan-apply.sh new file mode 100644 index 00000000..e401995b --- /dev/null +++ b/scripts/lib/tofu-init-plan-apply.sh @@ -0,0 +1,40 @@ +# Library to be sourced into scripts for running tofu init, plan, and apply +# in a GitHub Action. + +echo "::group::$dir tofu" + +if ! tofu init -reconfigure -backend-config="$repo_root/terraform/backends/${APP}-${ENV}.s3.tfbackend"; then + job_error=true + echo "::endgroup::" + echo "::error::Error in tofu init for $dir" + continue +fi + +export TF_VAR_app="$APP" +export TF_VAR_env="$ENV" +tofu_warning="" +tofu_error="" +if tofu plan -detailed-exitcode -out "$temp_plan_out"; then + echo "No changes planned for $dir" +elif [ "$?" -eq "2" ]; then # Detailed exit code is 2, meaning changes are planned + tofu_warning="Changes planned for $dir" +else + job_error=true + tofu_error="Error in tofu plan for $dir" +fi + +if [[ -n "$tofu_warning" && "$APPLY" == "true" ]]; then + echo "Applying plan for $dir" + if ! tofu apply "$temp_plan_out"; then + job_error=true + tofu_error="Error in tofu apply for $dir" + fi +fi +echo "::endgroup::" + +if [ -n "$tofu_warning" ]; then + echo "::warning::$tofu_warning" +fi +if [ -n "$tofu_error" ]; then + echo "::error::$tofu_error" +fi diff --git a/scripts/tf-module-test b/scripts/tf-module-test new file mode 100644 index 00000000..d6c8f773 --- /dev/null +++ b/scripts/tf-module-test @@ -0,0 +1,40 @@ +#!/bin/bash +# Run tests on tf modules. Used in the tf-module-test workflow. +set -e + +repo_root="$(git rev-parse --show-toplevel)" +script_dir="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")" +job_error=false +temp_plan_out=$(mktemp) +APPLY=true + +for dir in $CHANGED_DIRS; do + dir_absolute="$repo_root/$dir" + [ ! -d "$dir_absolute/test" ] && echo "No directory found at $dir. Skipping" && continue + cd "$dir_absolute" + [ ! -f "test/test.sh" ] && echo "No test.sh file found in $dir/test. Skipping" && continue + dir=$dir/test + + source $script_dir/lib/tofu-init-plan-apply.sh + + # The test.sh script must use test_warning, test_error, and job_error + # variables and "continue" as done in tofu lib scripts rather than erroring + # out to allow for looping through dirs. + test_warning="" + test_error="" + echo "::group::$dir test" + source test.sh + echo "::endgroup::" + if [ -n "$test_warning" ]; then + echo "::warning::$test_warning" + fi + if [ -n "$test_error" ]; then + echo "::error::$test_error" + fi + + source $script_dir/lib/tofu-destroy.sh +done + +if [ "$job_error" == "true" ]; then + exit 1 +fi diff --git a/scripts/tofu-plan b/scripts/tofu-plan index 01e61f65..88fe7ac5 100755 --- a/scripts/tofu-plan +++ b/scripts/tofu-plan @@ -2,6 +2,7 @@ # Run tofu plan across all services. Used in the tofu-plan and tofu-apply workflows. repo_root="$(git rev-parse --show-toplevel)" +script_dir="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")" job_error=false temp_plan_out=$(mktemp) @@ -41,43 +42,7 @@ for dir in $(ls "$repo_root/terraform/services"); do ;; esac - echo "::group::$dir tofu" - - if ! tofu init -reconfigure -backend-config="../../backends/${APP}-${ENV}.s3.tfbackend"; then - job_error=true - echo "::endgroup::" - echo "::error::Error in tofu init for $dir" - continue - fi - - export TF_VAR_app="$APP" - export TF_VAR_env="$ENV" - tofu_warning="" - tofu_error="" - if tofu plan -detailed-exitcode -out "$temp_plan_out"; then - echo "No changes planned for $dir" - elif [ "$?" -eq "2" ]; then # Detailed exit code is 2, meaning changes are planned - tofu_warning="Changes planned for $dir" - else - job_error=true - tofu_error="Error in tofu plan for $dir" - fi - - if [[ -n "$tofu_warning" && "$APPLY" == "true" ]]; then - echo "Applying plan for $dir" - if ! tofu apply "$temp_plan_out"; then - job_error=true - tofu_error="Error in tofu apply for $dir" - fi - fi - echo "::endgroup::" - - if [ -n "$tofu_warning" ]; then - echo "::warning::$tofu_warning" - fi - if [ -n "$tofu_error" ]; then - echo "::error::$tofu_error" - fi + source $script_dir/lib/tofu-run.sh done if [ "$job_error" == "true" ]; then diff --git a/terraform/modules/cluster/README.md b/terraform/modules/cluster/README.md index 05133f20..9d6f9575 100644 --- a/terraform/modules/cluster/README.md +++ b/terraform/modules/cluster/README.md @@ -32,7 +32,7 @@ No requirements. | Name | Version | |------|---------| -| [aws](#provider\_aws) | 5.100.0 | +| [aws](#provider\_aws) | n/a | ## Modules @@ -43,13 +43,14 @@ No modules. | Name | Type | |------|------| | [aws_ecs_cluster.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | +| [aws_service_discovery_http_namespace.cluster_service_connect_namespace](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_http_namespace) | resource | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [cluster\_name\_override](#input\_cluster\_name\_override) | Name of the ecs cluster. | `string` | `null` | no | -| [platform](#input\_platform) | Object that describes standardized platform values. | `any` | n/a | yes | +| [platform](#input\_platform) | Object that describes standardized platform values. |
object({
app = string,
env = string,
kms_alias_primary = object({
target_key_arn = string
}),
service = string,
is_ephemeral_env = string
})
| n/a | yes | ## Outputs diff --git a/terraform/modules/cluster/main.tf b/terraform/modules/cluster/main.tf index 39eb32d6..f66886e1 100644 --- a/terraform/modules/cluster/main.tf +++ b/terraform/modules/cluster/main.tf @@ -1,3 +1,13 @@ +locals { + name = var.cluster_name_override != null ? var.cluster_name_override : "${var.platform.app}-${var.platform.env}-${var.platform.service}" +} + +resource "aws_service_discovery_http_namespace" "cluster_service_connect_namespace" { + name = local.name + description = "Service Connect namespace for ${local.name}" +} + + resource "aws_ecs_cluster" "this" { name = var.cluster_name_override != null ? var.cluster_name_override : "${var.platform.app}-${var.platform.env}-${var.platform.service}" @@ -6,10 +16,20 @@ resource "aws_ecs_cluster" "this" { value = var.platform.is_ephemeral_env ? "disabled" : "enabled" } + service_connect_defaults { + namespace = aws_service_discovery_http_namespace.cluster_service_connect_namespace.arn + } + configuration { managed_storage_configuration { fargate_ephemeral_storage_kms_key_id = var.platform.kms_alias_primary.target_key_arn kms_key_id = var.platform.kms_alias_primary.target_key_arn } } + + depends_on = [aws_service_discovery_http_namespace.cluster_service_connect_namespace] } + + + + diff --git a/terraform/modules/platform/variables.tf b/terraform/modules/platform/variables.tf index c1acd7f2..249987fa 100644 --- a/terraform/modules/platform/variables.tf +++ b/terraform/modules/platform/variables.tf @@ -2,8 +2,8 @@ variable "app" { description = "The short name for the delivery team or ADO." type = string validation { - condition = contains(["ab2d", "bcda", "dpc"], var.app) - error_message = "Invalid short var.app (application). Must be one of ab2d, bcda, or dpc." + condition = contains(["ab2d", "bcda", "cdap", "dpc"], var.app) + error_message = "Invalid short var.app (application). Must be one of ab2d, bcda, cdap or dpc." } } diff --git a/terraform/modules/service/README.md b/terraform/modules/service/README.md index 4ce6efcd..a6bd44bc 100644 --- a/terraform/modules/service/README.md +++ b/terraform/modules/service/README.md @@ -78,84 +78,58 @@ module "service" { ``` - +## Requirements + +No requirements. + ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | n/a | - -## Requirements +## Modules -No requirements. +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_ecs_service.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource | +| [aws_ecs_task_definition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | +| [aws_iam_role.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_ecs_cluster.cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_cluster) | data source | +| [aws_iam_policy_document.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_service_discovery_http_namespace.cluster-service_discovery-namespace](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/service_discovery_http_namespace) | data source | - ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [cluster\_arn](#input\_cluster\_arn) | The ecs cluster ARN hosting the service and task. | `string` | n/a | yes | -| [cpu](#input\_cpu) | Number of cpu units used by the task. | `number` | n/a | yes | -| [image](#input\_image) | The image used to start a container. This string is passed directly to the Docker daemon. By default, images in the Docker Hub registry are available. Other repositories are specified with either `repository-url/image:tag` or `repository-url/image@digest` | `string` | n/a | yes | -| [memory](#input\_memory) | Amount (in MiB) of memory used by the task. | `number` | n/a | yes | -| [platform](#input\_platform) | Object representing the CDAP plaform module. |
object({
app = string
env = string
kms_alias_primary = object({ target_key_arn = string })
primary_region = object({ name = string })
private_subnets = map(object({ id = string }))
service = string
})
| n/a | yes | -| [task\_role\_arn](#input\_task\_role\_arn) | ARN of the role that allows the application code in tasks to make calls to AWS services. | `string` | n/a | yes | +| [cluster\_service\_connect\_namespace\_arn](#input\_cluster\_service\_connect\_namespace\_arn) | The Service Connect discovery namespace arn. | `string` | `null` | no | | [container\_environment](#input\_container\_environment) | The environment variables to pass to the container |
list(object({
name = string
value = string
}))
| `null` | no | +| [container\_name\_override](#input\_container\_name\_override) | Desired container name for the ecs task. Defaults to local.service\_name. | `string` | `null` | no | | [container\_secrets](#input\_container\_secrets) | The secrets to pass to the container. For more information, see [Specifying Sensitive Data](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html) in the Amazon Elastic Container Service Developer Guide |
list(object({
name = string
valueFrom = string
}))
| `null` | no | +| [cpu](#input\_cpu) | Number of cpu units used by the task. | `number` | n/a | yes | | [desired\_count](#input\_desired\_count) | Number of instances of the task definition to place and keep running. | `number` | `1` | no | | [execution\_role\_arn](#input\_execution\_role\_arn) | ARN of the role that grants Fargate agents permission to make AWS API calls to pull images for containers, get SSM params in the task definition, etc. Defaults to creation of a new role. | `string` | `null` | no | | [force\_new\_deployment](#input\_force\_new\_deployment) | When *changed* to `true`, trigger a new deployment of the ECS Service even when a deployment wouldn't otherwise be triggered by other changes. **Note**: This has no effect when the value is `false`, changed to `false`, or set to `true` between consecutive applies. | `bool` | `false` | no | | [health\_check](#input\_health\_check) | Health check that monitors the service. |
object({
command = list(string),
interval = optional(number),
retries = optional(number),
startPeriod = optional(number),
timeout = optional(number)
})
| `null` | no | | [health\_check\_grace\_period\_seconds](#input\_health\_check\_grace\_period\_seconds) | Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers. | `number` | `null` | no | +| [image](#input\_image) | The image used to start a container. This string is passed directly to the Docker daemon. By default, images in the Docker Hub registry are available. Other repositories are specified with either `repository-url/image:tag` or `repository-url/image@digest` | `string` | n/a | yes | | [load\_balancers](#input\_load\_balancers) | Load balancer(s) for use by the AWS ECS service. |
list(object({
target_group_arn = string
container_name = string
container_port = number
}))
| `[]` | no | +| [memory](#input\_memory) | Amount (in MiB) of memory used by the task. | `number` | n/a | yes | | [mount\_points](#input\_mount\_points) | The mount points for data volumes in your container |
list(object({
containerPath = optional(string)
readOnly = optional(bool)
sourceVolume = optional(string)
}))
| `null` | no | +| [platform](#input\_platform) | Object representing the CDAP plaform module. |
object({
app = string
env = string
kms_alias_primary = object({ target_key_arn = string })
primary_region = object({ name = string })
private_subnets = map(object({ id = string }))
service = string
})
| n/a | yes | | [port\_mappings](#input\_port\_mappings) | The list of port mappings for the container. Port mappings allow containers to access ports on the host container instance to send or receive traffic. For task definitions that use the awsvpc network mode, only specify the containerPort. The hostPort can be left blank or it must be the same value as the containerPort |
list(object({
appProtocol = optional(string)
containerPort = optional(number)
containerPortRange = optional(string)
hostPort = optional(number)
name = optional(string)
protocol = optional(string)
}))
| `null` | no | | [security\_groups](#input\_security\_groups) | List of security groups to associate with the service. | `list(string)` | `[]` | no | | [service\_name\_override](#input\_service\_name\_override) | Desired service name for the service tag on the aws ecs service. Defaults to var.platform.app-var.platform.env-var.platform.service. | `string` | `null` | no | +| [task\_role\_arn](#input\_task\_role\_arn) | ARN of the role that allows the application code in tasks to make calls to AWS services. | `string` | n/a | yes | | [volumes](#input\_volumes) | Configuration block for volumes that containers in your task may use |
list(object({
configure_at_launch = optional(bool)
efs_volume_configuration = optional(object({
authorization_config = optional(object({
access_point_id = optional(string)
iam = optional(string)
}))
file_system_id = string
root_directory = optional(string)
}))
host_path = optional(string)
name = string
}))
| `null` | no | - -## Modules - -No modules. - - -## Resources - -| Name | Type | -|------|------| -| [aws_ecs_service.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource | -| [aws_ecs_task_definition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | -| [aws_iam_role.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | -| [aws_iam_role_policy.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | -| [aws_iam_policy_document.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | - - ## Outputs | Name | Description | diff --git a/terraform/modules/service/data.tf b/terraform/modules/service/data.tf new file mode 100644 index 00000000..f76a2b74 --- /dev/null +++ b/terraform/modules/service/data.tf @@ -0,0 +1,10 @@ +data "aws_caller_identity" "current" {} + +data "aws_kms_alias" "kms_key" { + name = "alias/cdap-test" +} + +data "aws_ram_resource_share" "pace_ca" { + resource_owner = "OTHER-ACCOUNTS" + name = "pace-ca-g1" +} diff --git a/terraform/modules/service/main.tf b/terraform/modules/service/main.tf index 3dfa603f..c90a1084 100644 --- a/terraform/modules/service/main.tf +++ b/terraform/modules/service/main.tf @@ -1,6 +1,13 @@ locals { service_name = var.service_name_override != null ? var.service_name_override : var.platform.service service_name_full = "${var.platform.app}-${var.platform.env}-${local.service_name}" + container_name = var.container_name_override != null ? var.container_name_override : var.platform.service +} + +resource "random_string" "unique_suffix" { + length = 8 + special = false + upper = false } resource "aws_ecs_task_definition" "this" { @@ -13,13 +20,12 @@ resource "aws_ecs_task_definition" "this" { memory = var.memory container_definitions = nonsensitive(jsonencode([ { - name = local.service_name - image = var.image - readonlyRootFilesystem = true - portMappings = var.port_mappings - mountPoints = var.mount_points - secrets = var.container_secrets - environment = var.container_environment + name = local.service_name_full + image = var.image + portMappings = var.port_mappings + mountPoints = var.mount_points + secrets = var.container_secrets + environment = var.container_environment logConfiguration = { logDriver = "awslogs" options = { @@ -62,6 +68,14 @@ resource "aws_ecs_task_definition" "this" { } } +data "aws_service_discovery_http_namespace" "cluster-service_discovery-namespace" { + name = basename(var.cluster_arn) +} + +data "aws_ecs_cluster" "cluster" { + cluster_name = var.cluster_arn +} + resource "aws_ecs_service" "this" { name = local.service_name_full cluster = var.cluster_arn @@ -72,6 +86,27 @@ resource "aws_ecs_service" "this" { force_new_deployment = var.force_new_deployment propagate_tags = "SERVICE" + service_connect_configuration { + enabled = true + namespace = data.aws_service_discovery_http_namespace.cluster-service_discovery-namespace.arn + service { + discovery_name = "ecs-sc-discovery-${random_string.unique_suffix.result}" + port_name = var.port_mappings[0].name + client_alias { + dns_name = local.service_name_full + port = var.port_mappings[0].containerPort + } + tls { + kms_key = data.aws_kms_alias.kms_key.arn + role_arn = aws_iam_role.service-connect.arn + + issuer_cert_authority { + aws_pca_authority_arn = one(data.aws_ram_resource_share.pace_ca.resource_arns) + } + } + } + } + network_configuration { subnets = keys(var.platform.private_subnets) assign_public_ip = false @@ -139,3 +174,98 @@ resource "aws_iam_role_policy" "execution" { role = aws_iam_role.execution[0].name policy = data.aws_iam_policy_document.execution[0].json } + +data "aws_iam_policy_document" "service_connect_pca" { + statement { + sid = "AllowDescribePCA" + actions = ["acm-pca:DescribeCertificateAuthority"] + resources = [one(data.aws_ram_resource_share.pace_ca.resource_arns)] + } + + statement { + sid = "AllowGetAndIssueCertificate" + actions = ["acm-pca:GetCertificateAuthorityCsr","acm-pca:GetCertificate", "acm-pca:IssueCertificate"] + resources = [one(data.aws_ram_resource_share.pace_ca.resource_arns)] + } +} + +resource "aws_iam_policy" "service_connect_pca" { + name = "${random_string.unique_suffix.result}-service-connect-pca-policy" + description = "Permissions for the ${var.platform.env}-${local.service_name} Service's Service Connect Role to use the PACE Private CA." + policy = data.aws_iam_policy_document.service_connect_pca.json +} + +data "aws_iam_policy_document" "service_connect_secrets_manager" { + statement { + actions = [ + "secretsmanager:CreateSecret", + "secretsmanager:TagResource", + "secretsmanager:DescribeSecret", + "secretsmanager:UpdateSecret", + "secretsmanager:GetSecretValue", + "secretsmanager:PutSecretValue", + "secretsmanager:DeleteSecret", + "secretsmanager:RotateSecret", + "secretsmanager:UpdateSecretVersionStage" + ] + resources = ["arn:aws:secretsmanager:${var.platform.primary_region.name}:${data.aws_caller_identity.current.account_id}:secret:ecs-sc!*"] + } +} + +resource "aws_iam_policy" "service_connect_secrets_manager" { + name = "${random_string.unique_suffix.result}-service-connect-secrets-manager-policy" + description = "Permissions for the ${var.platform.env} ${local.service_name} Service's Service Connect Role to use Secrets Manager for Service Connect related Secrets." + policy = data.aws_iam_policy_document.service_connect_secrets_manager.json +} + +data "aws_iam_policy_document" "service_assume_role" { + for_each = toset(["ecs-tasks", "ecs", "ECSServiceConnectForTLS"]) + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["${each.value}.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "service-connect" { + name = "${local.service_name_full}-service-connect" + assume_role_policy = data.aws_iam_policy_document.service_assume_role["ECSServiceConnectForTLS"].json + force_detach_policies = true +} + +data "aws_iam_policy_document" "kms" { + statement { + sid = "AllowEnvCMKAccess" + actions = [ + "kms:Decrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt", + "kms:DescribeKey", + "kms:CreateGrant", + "kms:ListGrants", + "kms:RevokeGrant", + "kms:GenerateDataKeyPair", + "kms:GenerateDataKeyPairWithoutPlaintext", + ] + resources = ["*"] + } +} + +resource "aws_iam_policy" "service_connect_kms" { + name = "${random_string.unique_suffix.result}-service-connect-kms-policy" + description = "Permissions for the ${var.platform.env} ${local.service_name} Service's Service Connect Role to use the ${var.platform.env} CMK" + policy = data.aws_iam_policy_document.kms.json +} + +resource "aws_iam_role_policy_attachment" "service-connect" { + for_each = { + kms = aws_iam_policy.service_connect_kms.arn + pca = aws_iam_policy.service_connect_pca.arn + secrets_manager = aws_iam_policy.service_connect_secrets_manager.arn + } + + role = aws_iam_role.service-connect.name + policy_arn = each.value +} diff --git a/terraform/modules/service/test/README.md b/terraform/modules/service/test/README.md new file mode 100644 index 00000000..036417d3 --- /dev/null +++ b/terraform/modules/service/test/README.md @@ -0,0 +1,47 @@ +## Requirements + +| Name | Version | +|------|---------| +| [aws](#requirement\_aws) | 5.81.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 5.81.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [backend\_service](#module\_backend\_service) | github.com/CMSgov/cdap//terraform/modules/service | plt-1448_test_service_connect | +| [cluster](#module\_cluster) | github.com/CMSgov/cdap//terraform/modules/cluster | plt-1448_test_service_connect | +| [platform](#module\_platform) | github.com/CMSgov/cdap//terraform/modules/platform | plt-1448_test_service_connect | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_role.ecs_task_execution_role](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/iam_role) | resource | +| [aws_iam_role.ecs_task_role](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.ecs_task_policy](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy_attachment.ecs_task_execution_role_policy](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lb.backend](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/lb) | resource | +| [aws_lb_listener.backend](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/lb_listener) | resource | +| [aws_lb_target_group.backend](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/lb_target_group) | resource | +| [aws_security_group.ecs_tasks](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/security_group) | resource | +| [aws_security_group.load_balancer](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/resources/security_group) | resource | +| [aws_vpc.selected](https://registry.terraform.io/providers/hashicorp/aws/5.81.0/docs/data-sources/vpc) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aws\_region](#input\_aws\_region) | AWS region | `string` | `"us-east-1"` | no | +| [private\_subnet\_ids](#input\_private\_subnet\_ids) | List of private subnet IDs for ECS tasks | `list(string)` | n/a | yes | +| [public\_subnet\_ids](#input\_public\_subnet\_ids) | List of private subnet IDs for ECS tasks | `list(string)` | n/a | yes | +| [vpc\_id](#input\_vpc\_id) | VPC ID where ECS cluster will be deployed | `string` | n/a | yes | + +## Outputs + +No outputs. diff --git a/terraform/modules/service/test/main.tf b/terraform/modules/service/test/main.tf new file mode 100644 index 00000000..565465e8 --- /dev/null +++ b/terraform/modules/service/test/main.tf @@ -0,0 +1,477 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.81.0" + } + } +} +# =========================== +# ECS Service Connect Setup +# =========================== +locals { + default_tags = module.platform.default_tags + service = "service-connect-demo" + api_image_uri = "539247469933.dkr.ecr.us-east-1.amazonaws.com/testing/nginx" + ecs_task_def_cpu_api = 4096 + ecs_task_def_memory_api = 14336 + api_desired_instances = 1 + container_port = 8080 + force_api_deployment = true +} + +module "platform" { + source = "github.com/CMSgov/cdap//terraform/modules/platform?ref=plt-1448_test_service_connect" + providers = { aws = aws, aws.secondary = aws.secondary } + + app = "cdap" + env = "test" + root_module = "https://github.com/CMSgov/cdap/tree/plt-1448_test_service_connect/terraform/services/service-connect-demo" + service = local.service +} + +module "cluster" { + source = "github.com/CMSgov/cdap//terraform/modules/cluster?ref=plt-1448_test_service_connect" + cluster_name_override = "plt-1448-microservices-cluster" + platform = module.platform +} + +# =========================== +# Data Sources +# =========================== +data "aws_vpc" "selected" { + id = var.vpc_id +} + +# =========================== +# IAM Roles and Policies +# =========================== + +resource "aws_iam_role" "ecs_task_execution_role" { + name = "jjr-ecs-task-execution-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" { + role = aws_iam_role.ecs_task_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role" "ecs_task_role" { + name = "jjr-ecs-task-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy" "ecs_task_policy" { + name = "jjr-ecs-task-policy" + role = aws_iam_role.ecs_task_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel" + ] + Resource = "*" + } + ] + }) +} + + +# =========================== +# Security Groups +# =========================== + +resource "aws_security_group" "ecs_tasks" { + name = "jjr-ecs-tasks-sg" + description = "Security group for ECS tasks" + vpc_id = var.vpc_id + + ingress { + description = "Allow all traffic from within VPC" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = [data.aws_vpc.selected.cidr_block] + } + + egress { + description = "Allow all outbound traffic" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "ecs-tasks-sg" + } +} + +resource "aws_security_group" "load_balancer" { + name = "jjr-load-balancer-sg" + description = "Security group for load balancer" + vpc_id = var.vpc_id + + ingress { + description = "HTTP from internet" + from_port = 80 + to_port = 8080 + protocol = "tcp" + cidr_blocks = [data.aws_vpc.selected.cidr_block] + } + + egress { + description = "All outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "load-balancer-sg" + } +} + +# =========================== +# CloudWatch Log Groups +# =========================== + +resource "aws_cloudwatch_log_group" "backend_service" { + name = "/ecs/jjr-backend-service" + retention_in_days = 7 + + tags = { + Name = "backend-service-logs" + } +} + +resource "aws_cloudwatch_log_group" "frontend_service" { + name = "/ecs/jjr-frontend-service" + retention_in_days = 7 + + tags = { + Name = "frontend-service-logs" + } +} + +# =========================== +# Load Balancer for Backend Service +# =========================== + +resource "aws_lb" "backend" { + name = "sc-backend-alb" + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.load_balancer.id] + subnets = var.public_subnet_ids # Use public subnets for internet-facing ALB + + enable_deletion_protection = false + + tags = { + Name = "backend-alb" + } +} + +resource "aws_lb_listener" "backend" { + load_balancer_arn = aws_lb.backend.arn + port = "8080" + protocol = "HTTP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.backend.arn + } +} + +resource "aws_lb_target_group" "backend" { + name = "cdap-backend-tg" + port = local.container_port + protocol = "HTTP" + target_type = "ip" + vpc_id = var.vpc_id + + health_check { + enabled = true + healthy_threshold = 2 + unhealthy_threshold = 2 + timeout = 5 + interval = 30 + path = "/" + protocol = "HTTP" + } + + tags = { + Name = "cdap-backend-target-group" + } +} + +module "backend_service" { + source = "github.com/CMSgov/cdap//terraform/modules/service?ref=plt-1448_test_service_connect" + service_name_override = "backend-service" + platform = module.platform + cluster_arn = module.cluster.this.arn + cpu = local.ecs_task_def_cpu_api + memory = local.ecs_task_def_memory_api + image = local.api_image_uri + desired_count = local.api_desired_instances + security_groups = [aws_security_group.ecs_tasks.id, aws_security_group.load_balancer.id] + task_role_arn = aws_iam_role.ecs_task_role.arn + port_mappings = [ + { + name = "backend-port" + containerPort = 8080 + protocol = "tcp" + appProtocol = "http" + } + ] + force_new_deployment = local.force_api_deployment + + load_balancers = [{ + target_group_arn = aws_lb_target_group.backend.arn + container_name = "backend" + container_port = local.container_port + }] + + mount_points = [ + { + "containerPath" = "/var/log/*", + "readOnly" = false, + "sourceVolume" = "var_log", + }, + { + "sourceVolume" : "nginx-cache", + "readOnly" = false, + "containerPath" : "/var/cache/nginx/*" + }, + ] + + volumes = [ + { + name = "nginx-cache" + readOnly = false + }, + { + name = "var_log" + readOnly = false + }, + ] + +} + +# # =========================== +# # Frontend Service Task Definition +# # =========================== +# +# resource "aws_ecs_task_definition" "frontend" { +# family = "frontend-service" +# network_mode = "awsvpc" +# requires_compatibilities = ["FARGATE"] +# cpu = "256" +# memory = "512" +# execution_role_arn = aws_iam_role.ecs_task_execution_role.arn +# task_role_arn = aws_iam_role.ecs_task_role.arn +# +# container_definitions = jsonencode([ +# { +# name = "frontend" +# image = local.api_image_uri +# +# mount_points = [ +# { +# "containerPath" = "/var/log", +# "sourceVolume" = "var_log", +# }, +# { +# "sourceVolume" : "nginx-cache", +# "containerPath" : "/var/cache/nginx" +# }, +# ] +# +# volumes = [ +# { +# name = "nginx-cache" +# }, +# { +# name = "var_log" +# }, +# ] +# +# portMappings = [ +# { +# name = "frontend-port" +# containerPort = 8080 +# protocol = "tcp" +# appProtocol = "http" +# } +# ] +# +# environment = [ +# { +# name = "SERVICE_NAME" +# value = "frontend" +# }, +# { +# name = "FRONTEND_URL" +# value = "http://frontend:8080" +# } +# ] +# +# logConfiguration = { +# logDriver = "awslogs" +# options = { +# "awslogs-group" = aws_cloudwatch_log_group.frontend_service.name +# "awslogs-region" = var.aws_region +# "awslogs-stream-prefix" = "frontend" +# } +# } +# +# healthCheck = { +# command = ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"] +# interval = 30 +# timeout = 5 +# retries = 3 +# startPeriod = 60 +# } +# } +# ]) +# +# tags = { +# Name = "frontend-service-task" +# } +# } + +# =========================== +# Application Load Balancer for Frontend +# =========================== + +resource "aws_lb" "frontend" { + name = "sc-frontend-alb" + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.load_balancer.id] + subnets = var.public_subnet_ids # Use public subnets for internet-facing ALB + + enable_deletion_protection = false + + tags = { + Name = "frontend-alb" + } +} + +resource "aws_lb_target_group" "frontend" { + name = "sc-frontend-tg" + port = 8080 + protocol = "HTTP" + target_type = "ip" + vpc_id = var.vpc_id + + health_check { + enabled = true + healthy_threshold = 2 + unhealthy_threshold = 2 + timeout = 5 + interval = 30 + path = "/" + protocol = "HTTP" + } + + tags = { + Name = "frontend-target-group" + } +} + +resource "aws_lb_listener" "frontend" { + load_balancer_arn = aws_lb.frontend.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.frontend.arn + } +} + +module "frontend_service" { + source = "github.com/CMSgov/cdap//terraform/modules/service?ref=plt-1448_test_service_connect" + service_name_override = "frontend-service" + platform = module.platform + cluster_arn = module.cluster.this.arn + cpu = local.ecs_task_def_cpu_api + memory = local.ecs_task_def_memory_api + image = local.api_image_uri + desired_count = local.api_desired_instances + security_groups = [aws_security_group.ecs_tasks.id, aws_security_group.load_balancer.id] + task_role_arn = aws_iam_role.ecs_task_role.arn + port_mappings = [ + { + name = "frontend-port" + containerPort = 8080 + protocol = "tcp" + appProtocol = "http" + } + ] + force_new_deployment = local.force_api_deployment + + load_balancers = [{ + target_group_arn = aws_lb_target_group.frontend.arn + container_port = local.container_port + container_name = "frontend-service" + }] + + mount_points = [ + { + "containerPath" = "/var/log/*", + "readOnly" = false, + "sourceVolume" = "var_log", + }, + { + "sourceVolume" : "nginx-cache", + "readOnly" = false, + "containerPath" : "/var/cache/nginx/*" + }, + ] + + volumes = [ + { + name = "nginx-cache" + readOnly = false + }, + { + name = "var_log" + readOnly = false + }, + ] + +} diff --git a/terraform/modules/service/test/test.sh b/terraform/modules/service/test/test.sh new file mode 100644 index 00000000..9292abb1 --- /dev/null +++ b/terraform/modules/service/test/test.sh @@ -0,0 +1,38 @@ +#Verify Service Connect ConfigurationĀ  +aws ecs describe-services --cluster jjr-microservices-cluster --services frontend-service backend-service + +#Test Connectivity Using ECS Exec + +#Enable ECS Exec on both services +aws ecs update-service --cluster jjr-microservices-cluster --service frontend-service --enable-execute-command --force-new-deployment +aws ecs update-service --cluster jjr-microservices-cluster --service backend-service --enable-execute-command --force-new-deployment + +#Install session-manager plugin +brew install session-manager-plugin + +#Open a shell +aws ecs execute-command --cluster plt-1448-microservices-cluster --task arn:aws:ecs:us-east-1:539247469933:cluster/plt-1448-microservices-cluster --container backend --interactive --command "/bin/bash" + +#Run a command +curl http://backend-service.backend:80/health + +#Check for envoy in the header +curl -I http://backend-service.backend:80/health + +#Monitor Logs and Metrics + +#- **View Logs**: Check container and application logs in Amazon CloudWatch Logs for connectivity or runtime errors. +#- **Review Metrics**: Use the CloudWatch console to monitor Service Connect metrics like`RequestCount` and `NewConnectionCount`under the`ECS`namespace. +#These metrics provide detailed telemetry and can be used for setting alarms and configuring auto scaling. +# Get a shell in one of your Service Connect-enabled containers +aws ecs execute-command --cluster your-cluster \ + --task your-task-id \ + --container your-container \ + --interactive --command "/bin/sh" + +# Test DNS resolution of your service +nslookup backend-service +dig backend-service + +# Test actual connectivity +wget -O- http://backend-service:8080 diff --git a/terraform/modules/service/test/test.tfvars b/terraform/modules/service/test/test.tfvars new file mode 100644 index 00000000..b9aded3f --- /dev/null +++ b/terraform/modules/service/test/test.tfvars @@ -0,0 +1,36 @@ +# AWS Region +# Specify the AWS region where resources will be deployed +aws_region = "us-east-1" + +# VPC Configuration +# Replace with your actual VPC ID +vpc_id = "vpc-07cac3327db239c92" + +# Private Subnet IDs for ECS Tasks +# Replace with your actual private subnet IDs +private_subnet_ids = [ + "subnet-0c46ebc2dad32d964", + "subnet-0f26c81d2b603e918", + "subnet-0c9276af7df0a20eb" +] + +# Public Subnet IDs for Load Balancers +# Replace with your actual public subnet IDs +public_subnet_ids = [ + "subnet-0626ed98a921efee0", + "subnet-07f988f48aa18c6c8", + "subnet-03948d4372e37165d" +] + +# Port Mappings for Container +# Example configuration - adjust based on your application needs +port_mappings = [ + { + name = "app-port" + containerPort = 8080 + hostPort = 8080 + protocol = "tcp" + appProtocol = "http" + containerPortRange = null + } +] diff --git a/terraform/modules/service/test/tofu.tf b/terraform/modules/service/test/tofu.tf new file mode 100644 index 00000000..cd154a4e --- /dev/null +++ b/terraform/modules/service/test/tofu.tf @@ -0,0 +1,20 @@ +provider "aws" { + region = "us-east-1" + default_tags { + tags = local.default_tags + } +} + +provider "aws" { + alias = "secondary" + region = "us-west-2" + default_tags { + tags = local.default_tags + } +} + +terraform { + backend "s3" { + key = "service-connect-demo/terraform.tfstate" + } +} diff --git a/terraform/modules/service/test/variables.tf b/terraform/modules/service/test/variables.tf new file mode 100644 index 00000000..f0360d69 --- /dev/null +++ b/terraform/modules/service/test/variables.tf @@ -0,0 +1,20 @@ +variable "aws_region" { +description = "AWS region" +type = string +default = "us-east-1" +} + +variable "private_subnet_ids" { +description = "List of private subnet IDs for ECS tasks" +type = list(string) +} + +variable "public_subnet_ids" { +description = "List of private subnet IDs for ECS tasks" +type = list(string) +} + +variable "vpc_id" { +description = "VPC ID where ECS cluster will be deployed" +type = string +} diff --git a/terraform/modules/service/variables.tf b/terraform/modules/service/variables.tf index 1a38d61a..127e4739 100644 --- a/terraform/modules/service/variables.tf +++ b/terraform/modules/service/variables.tf @@ -3,6 +3,12 @@ variable "cluster_arn" { type = string } +variable "cluster_service_connect_namespace_arn" { + description = "The Service Connect discovery namespace arn." + type = string + default = null +} + variable "container_environment" { description = "The environment variables to pass to the container" type = list(object({ @@ -26,10 +32,10 @@ variable "cpu" { type = number } -variable "health_check_grace_period_seconds" { +variable "container_name_override" { + description = "Desired container name for the ecs task. Defaults to local.service_name." + type = string default = null - description = "Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers." - type = number } variable "desired_count" { @@ -50,6 +56,24 @@ variable "force_new_deployment" { default = false } +variable "health_check" { + description = "Health check that monitors the service." + type = object({ + command = list(string), + interval = optional(number), + retries = optional(number), + startPeriod = optional(number), + timeout = optional(number) + }) + default = null +} + +variable "health_check_grace_period_seconds" { + default = null + description = "Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers." + type = number +} + variable "image" { description = "The image used to start a container. This string is passed directly to the Docker daemon. By default, images in the Docker Hub registry are available. Other repositories are specified with either `repository-url/image:tag` or `repository-url/image@digest`" type = string @@ -106,18 +130,6 @@ variable "port_mappings" { default = null } -variable "health_check" { - description = "Health check that monitors the service." - type = object({ - command = list(string), - interval = optional(number), - retries = optional(number), - startPeriod = optional(number), - timeout = optional(number) - }) - default = null -} - variable "security_groups" { description = "List of security groups to associate with the service." type = list(string) diff --git a/terraform/services/github-actions-role/README.md b/terraform/services/github-actions-role/README.md index 9419f115..76a9ead9 100644 --- a/terraform/services/github-actions-role/README.md +++ b/terraform/services/github-actions-role/README.md @@ -10,3 +10,61 @@ Pass in a backend file when running tofu init. Example: tofu init -reconfigure -backend-config=../../backends/ab2d-dev.s3.tfbackend tofu plan ``` + + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | +| [aws.secondary](#provider\_aws.secondary) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [standards](#module\_standards) | github.com/CMSgov/cdap//terraform/modules/standards | 0bd3eeae6b03cc8883b7dbdee5f04deb33468260 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_instance_profile.github_actions_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.github_actions_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_openid_connect_provider.github](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_openid_connect_provider) | data source | +| [aws_iam_policy.poweruser_boundary](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | +| [aws_iam_policy_document.github_actions_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.github_actions_role_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_role.admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_role) | data source | +| [aws_kms_alias.ab2d_ecr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.ab2d_tfstate_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.account_env](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.account_env_old](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.account_env_old_secondary](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.account_env_secondary](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.bcda_aco_creds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.bcda_app_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.bcda_insights_data_sampler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.dpc_app_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.dpc_cloudwatch_keys](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.dpc_ecr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_kms_alias.environment_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [app](#input\_app) | The application name (ab2d, bcda, dpc, cdap) | `string` | n/a | yes | +| [env](#input\_env) | The application environment (dev, test, mgmt, sbx, sandbox, prod) | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | ARN for the GitHub Actions role | + \ No newline at end of file diff --git a/terraform/services/github-actions-role/main.tf b/terraform/services/github-actions-role/main.tf index b0989bc3..aeec4887 100644 --- a/terraform/services/github-actions-role/main.tf +++ b/terraform/services/github-actions-role/main.tf @@ -204,6 +204,7 @@ data "aws_iam_policy_document" "github_actions_policy" { "ecs:DescribeServices", "ecs:DescribeTaskDefinition", "ecs:DescribeTasks", + "ecs:ListClusters", "ecs:ListTaskDefinitions", "ecs:ListTasks", "ecs:RegisterTaskDefinition", diff --git a/terraform/services/service-connect-cluster-namespaces/README.md b/terraform/services/service-connect-cluster-namespaces/README.md new file mode 100644 index 00000000..b8a20a81 --- /dev/null +++ b/terraform/services/service-connect-cluster-namespaces/README.md @@ -0,0 +1,47 @@ +# OpenTofu for Service Connect namespaces for each ECS Cluster + +This OpenTofu code creates aws_service_discovery_http_namespace for each ECS Cluster. + +## Instructions + +Pass in a backend file when running tofu init. Example: + +```bash +tofu init -reconfigure -backend-config=../../backends/cdap-test.s3.tfbackend +tofu plan +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | ~> 1.10.5 | +| [aws](#requirement\_aws) | ~> 5.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | ~> 5.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_service_discovery_http_namespace.ecs_namespaces](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_http_namespace) | resource | +| [aws_ecs_cluster.clusters](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_cluster) | data source | +| [aws_ecs_clusters.all](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_clusters) | data source | + +## Inputs + +No inputs. + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/terraform/services/service-connect-cluster-namespaces/conf.sh b/terraform/services/service-connect-cluster-namespaces/conf.sh new file mode 100644 index 00000000..9d570530 --- /dev/null +++ b/terraform/services/service-connect-cluster-namespaces/conf.sh @@ -0,0 +1 @@ +TARGET_ENVS=all diff --git a/terraform/services/service-connect-cluster-namespaces/main.tf b/terraform/services/service-connect-cluster-namespaces/main.tf new file mode 100644 index 00000000..b136ea21 --- /dev/null +++ b/terraform/services/service-connect-cluster-namespaces/main.tf @@ -0,0 +1,38 @@ +terraform { + required_version = "~> 1.10.5" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +# Data source to fetch all ECS cluster ARNs +data "aws_ecs_clusters" "all" {} + +# Data source to fetch details for each cluster +data "aws_ecs_cluster" "clusters" { + for_each = toset([ + for arn in data.aws_ecs_clusters.all.cluster_arns : + element(split("/", arn), 1) + ]) + + cluster_name = each.key +} + +# Create Service Discovery HTTP Namespace for each ECS Cluster +resource "aws_service_discovery_http_namespace" "ecs_namespaces" { + for_each = data.aws_ecs_cluster.clusters + + name = each.value.cluster_name + description = "Service Connect namespace for ${each.value.cluster_name}" + + tags = { + Name = "Service Connect - ${each.value.cluster_name}" + Cluster = each.value.cluster_name + Environment = try(each.value.tags["Environment"], "unknown") + ManagedBy = "Terraform" + } +} diff --git a/terraform/services/service-connect-cluster-namespaces/tofu.tf b/terraform/services/service-connect-cluster-namespaces/tofu.tf new file mode 100644 index 00000000..33e8bd3a --- /dev/null +++ b/terraform/services/service-connect-cluster-namespaces/tofu.tf @@ -0,0 +1,14 @@ +provider "aws" { + region = "us-east-1" +} + +provider "aws" { + alias = "secondary" + region = "us-west-2" +} + +terraform { + backend "s3" { + key = "service-connect-cluster-namespaces/terraform.tfstate" + } +}