Skip to content

Commit

Permalink
Support secrets manager secrets for fargate services. (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesiarmes authored Jun 24, 2024
1 parent bd7b53f commit a1af853
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 30 deletions.
95 changes: 70 additions & 25 deletions aws/fargate_service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Add this module to your `main.tf` (or appropriate) file and configure the inputs
to match your desired configuration. For example:

```hcl
module "cloudfront_waf" {
module "fargate_service" {
source = "github.com/codeforamerica/tofu-modules/aws/fargate_service"
project = "my-project"
Expand All @@ -26,6 +26,10 @@ module "cloudfront_waf" {
public_subnets = module.vpc.public_subnets
logging_key_id = module.logging.kms_key_arn
container_port = 3000
environment_variables = {
RACK_ENV = "development"
}
}
```

Expand All @@ -44,30 +48,68 @@ tofu init -upgrade

## Inputs

| Name | Description | Type | Default | Required |
|--------------------------|---------------------------------------------------------------------------------------|----------|------------|----------|
| domain | Domain name for service. Example: `"staging.service.org"` | `string` | n/a | yes |
| logging_key_id | KMS key to use for log encryption. | `string` | n/a | yes |
| private_subnets | List of private subnet CIDR blocks. | `list` | n/a | yes |
| project | Name of the project. | `string` | n/a | yes |
| project_short | Short name for the project. Used in resource names with character limits. | `string` | n/a | yes |
| public_subnets | List of public subnet CIDR blocks. | `list` | n/a | yes |
| service | Service that these resources are supporting. Example: `"api"`, `"web"`, `"worker"` | `string` | n/a | yes |
| service_short | Short name for the service. Used in resource names with character limits. | `string` | n/a | yes |
| vpc_id | Id of the VPC to deploy into. | `string` | n/a | yes |
| container_port | Port the container listens on. | `number` | `80` | no |
| enable_execute_command | Enable the [ECS ExecuteCommand][ecs-exec] feature. | `bool` | `false` | no |
| environment | Environment for the project. | `string` | `"dev"` | no |
| force_delete | Force deletion of resources. If changing to true, be sure to apply before destroying. | `bool` | `false` | no |
| image_tag | Tag of the container image to be deployed. | `string` | `"latest"` | no |
| image_tags_mutable | Whether the container repository allows tags to be mutated. | `bool` | `false` | no |
| ingress_cidrs | List of additional CIDR blocks to allow traffic from. | `list` | `[]` | no |
| key_recovery_period | Number of days to recover the service KMS key after deletion. | `number` | `30` | no |
| log_retention_period | Retention period for flow logs, in days. | `number` | `30` | no |
| otel_log_level | Log level for the OpenTelemetry collector. | `string` | `"info"` | no |
| public | Whether the service should be exposed to the public Internet. | `bool` | `false` | no |
| subdomain | Optional subdomain for the service, to be appended to the domain for DNS. | `string` | `""` | no |
| untagged_image_retention | Retention period (after push) for untagged images, in days. | `number` | `14` | no |
| Name | Description | Type | Default | Required |
|---------------------------|---------------------------------------------------------------------------------------|---------------|------------|----------|
| domain | Domain name for service. Example: `"staging.service.org"` | `string` | n/a | yes |
| logging_key_id | KMS key to use for log encryption. | `string` | n/a | yes |
| private_subnets | List of private subnet CIDR blocks. | `list` | n/a | yes |
| project | Name of the project. | `string` | n/a | yes |
| project_short | Short name for the project. Used in resource names with character limits. | `string` | n/a | yes |
| public_subnets | List of public subnet CIDR blocks. | `list` | n/a | yes |
| service | Service that these resources are supporting. Example: `"api"`, `"web"`, `"worker"` | `string` | n/a | yes |
| service_short | Short name for the service. Used in resource names with character limits. | `string` | n/a | yes |
| vpc_id | Id of the VPC to deploy into. | `string` | n/a | yes |
| container_port | Port the container listens on. | `number` | `80` | no |
| enable_execute_command | Enable the [ECS ExecuteCommand][ecs-exec] feature. | `bool` | `false` | no |
| environment | Environment for the project. | `string` | `"dev"` | no |
| [environment_secrets] | Secrets to be injected as environment variables into the contrainer. | `map(string)` | `{}` | no |
| environment_variables | Environment variables to be set on the container. | `map(string)` | `{}` | no |
| force_delete | Force deletion of resources. If changing to true, be sure to apply before destroying. | `bool` | `false` | no |
| image_tag | Tag of the container image to be deployed. | `string` | `"latest"` | no |
| image_tags_mutable | Whether the container repository allows tags to be mutated. | `bool` | `false` | no |
| ingress_cidrs | List of additional CIDR blocks to allow traffic from. | `list` | `[]` | no |
| key_recovery_period | Number of days to recover the service KMS key after deletion. | `number` | `30` | no |
| log_retention_period | Retention period for flow logs, in days. | `number` | `30` | no |
| otel_log_level | Log level for the OpenTelemetry collector. | `string` | `"info"` | no |
| public | Whether the service should be exposed to the public Internet. | `bool` | `false` | no |
| [secrets_manager_secrets] | Map of secrets to be created in Secrets Manager. | `map(object)` | `{}` | no |
| subdomain | Optional subdomain for the service, to be appended to the domain for DNS. | `string` | `""` | no |
| untagged_image_retention | Retention period (after push) for untagged images, in days. | `number` | `14` | no |

### secrets_manager_secrets

An optional map of secrets to be created in [AWS Secrets
Manager][secrets-manager]. Once the secret is created, any changes to the value
will be ignored. For example, to create a secret named `example`:

```hcl
secrets_manager_secrets = {
example = {
recovery_window = 7
description = "Example credentials for our application."
}
}
```

| Name | Description | Type | Default | Required |
|------------------------|--------------------------------------------------------------|----------|---------|----------|
| description | Description of the secret. | `string` | n/a | yes |
| recovery_window | Number of days that a secret can be recovered after deltion. | `string` | `30` | no |
| create_random_password | Creates a random password as the staring value. | `bool` | `false` | no |
| start_value | Value to be set into the secret at creation. | `string` | `"{}"` | no |

### environment_secrets

An optional map of secrets to be injected as environment variables into the
container. The key is the name of the environment variable, and the value is the
name and key of a secret defined using [secrets_manager_secrets], seperated by
":". For example, to use the `client_id` key from the `example` secret:

```hcl
environment_secrets = {
EXAMPLE_CLIENT_ID = "client:client_id"
}
```

## Outputs

Expand All @@ -77,3 +119,6 @@ tofu init -upgrade
| docker_push | Commands to push a Docker image to the container repository. | `string` |

[ecs-exec]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
[environment_secrets]: #environment_secrets
[secrets-manager]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html
[secrets_manager_secrets]: #secrets_manager_secrets
11 changes: 11 additions & 0 deletions aws/fargate_service/iam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ resource "aws_iam_policy" "execution" {
})))
}

resource "aws_iam_policy" "secrets" {
name_prefix = "${local.prefix}-secrets-access-"
description = "Allow acceess to ${var.service} secrets for ${var.project} ${var.environment}."

policy = jsonencode(yamldecode(templatefile("${path.module}/templates/secrets-access-policy.yaml.tftpl", {
secrets = module.secrets_manager
})))
}

resource "aws_iam_role" "execution" {
name = "${local.prefix}-execution"
description = "${var.service} task execution role for ${var.project} ${var.environment}."
Expand All @@ -28,6 +37,7 @@ resource "aws_iam_role" "execution" {

managed_policy_arns = [
# aws_iam_policy.execution.arn
aws_iam_policy.secrets.arn,
"arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
"arn:${data.aws_partition.current.partition}:iam::aws:policy/CloudWatchAgentServerPolicy",
]
Expand All @@ -51,6 +61,7 @@ resource "aws_iam_role" "task" {
})

managed_policy_arns = [
aws_iam_policy.secrets.arn,
"arn:${data.aws_partition.current.partition}:iam::aws:policy/CloudWatchFullAccess",
"arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMFullAccess",
"arn:${data.aws_partition.current.partition}:iam::aws:policy/CloudWatchAgentServerPolicy",
Expand Down
12 changes: 9 additions & 3 deletions aws/fargate_service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ module "ecr" {
source = "terraform-aws-modules/ecr/aws"
version = "~> 2.2"

repository_name = local.prefix
repository_image_scan_on_push = true
repository_encryption_type = "KMS"
repository_name = local.prefix
repository_image_scan_on_push = true
repository_encryption_type = "KMS"
repository_image_tag_mutability = var.image_tags_mutable ? "MUTABLE" : "IMMUTABLE"
repository_kms_key = aws_kms_key.fargate.arn
repository_lifecycle_policy = jsonencode(yamldecode(templatefile(
Expand Down Expand Up @@ -99,6 +99,12 @@ module "ecs_service" {
namespace = "${var.project}/${var.service}"
env_vars = var.environment_variables
otel_log_level = var.otel_log_level

# Split defined secrets on ":" and use the name to get the arn.
env_secrets = {
for key, value in var.environment_secrets :
key => "${module.secrets_manager[split(":", value)[0]].secret_arn}:${split(":", value)[1]}::"
}
}
)))
}
15 changes: 15 additions & 0 deletions aws/fargate_service/secrets.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module "secrets_manager" {
source = "terraform-aws-modules/secrets-manager/aws"
version = "~> 1.1"

for_each = var.secrets_manager_secrets

name_prefix = "${var.project}/${var.environment}/${var.service}/${each.key}-"
create_random_password = each.value.create_random_password
description = each.value.description
recovery_window_in_days = each.value.recovery_window
kms_key_id = aws_kms_alias.fargate.id
secret_string = each.value.start_value

ignore_secret_changes = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
- name: ${key}
value: "${value}"
%{~ endfor ~}
secrets:
%{~ for key, value in env_secrets ~}
- name: ${key}
valueFrom: "${value}"
%{~ endfor ~}
- name: otel-collector
image: public.ecr.aws/aws-observability/aws-otel-collector:latest
memory: 512
Expand Down
12 changes: 12 additions & 0 deletions aws/fargate_service/templates/secrets-access-policy.yaml.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
- kms:Decrypt
- ssm:GetParameter
- ssm:GetParameters
Resource:
%{~ for key, value in secrets ~}
- "${value.secret_arn}"
%{~ endfor ~}
20 changes: 19 additions & 1 deletion aws/fargate_service/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ variable "environment" {
default = "dev"
}

variable "environment_secrets" {
type = map(string)
description = "Secrets to be injected as environment variables on the container."
default = {}
}

variable "environment_variables" {
type = map(string)
description = "Environment variables to set on the container."
default = {}

}

variable "force_delete" {
Expand Down Expand Up @@ -101,6 +106,19 @@ variable "public_subnets" {
description = "List of public subnets."
}

# TODO: Support rotation.
variable "secrets_manager_secrets" {
type = map(object({
create_random_password = optional(bool, false)
description = string
recovery_window = optional(number, 30)
start_value = optional(string, "{}")
}))

description = "List of VPC peering connections."
default = {}
}

variable "service" {
type = string
description = "Service that these resources are supporting. Example: 'api', 'web', 'worker'"
Expand Down
2 changes: 1 addition & 1 deletion aws/vpc/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ variable "peers" {
}))

description = "List of VPC peering connections."
default = {}
default = {}
}

variable "private_subnets" {
Expand Down

0 comments on commit a1af853

Please sign in to comment.