Skip to content

Commit

Permalink
Allow peering connections to be defined for AWS VPCs and add OTEL sid…
Browse files Browse the repository at this point in the history
…ecar. (#7)

* Allow peering connections to be defined for AWS VPCs.
* Define peering ACLs as part of the VPC.
* Allow configuration of fargate endpoint ingress rules.
* Utilize OTEL rather than the CloudWatch agent.
* Added execute command option for fargate.
* Allow an optional subdomain for the fargate service.
* Enable init process on containers.
* Added a security group to allow access to the VPC endpoints.
* Allow ECR tags to be mutated.
* Place the ALB internally by default.
  • Loading branch information
jamesiarmes authored Jun 21, 2024
1 parent 74ecc60 commit d47b9d6
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 86 deletions.
45 changes: 26 additions & 19 deletions aws/fargate_service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,36 @@ 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 |
| 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 |
| internal | Creates an internal ALB instead of a public one. | `bool` | `false` | 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 |
| 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 |
| 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 |

## Outputs

| Name | Description | Type |
|--------------|--------------------------------------------------------------|----------|
| cluster_name | Name of the ECS Fargate cluster. | `string` |
| 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
7 changes: 4 additions & 3 deletions aws/fargate_service/alb.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ module "alb" {
name = local.prefix_short
load_balancer_type = "application"
security_groups = [module.endpoint_security_group.security_group_id]
subnets = var.internal ? var.private_subnets : var.public_subnets
subnets = var.public ? var.public_subnets : var.private_subnets
vpc_id = var.vpc_id
internal = !var.public

# TODO: Support IPv6 and/or dualstack.
ip_address_type = "ipv4"
Expand Down Expand Up @@ -55,7 +56,7 @@ module "alb" {
}

resource "aws_acm_certificate" "endpoint" {
domain_name = var.domain
domain_name = local.fqdn
validation_method = "DNS"

lifecycle {
Expand All @@ -64,7 +65,7 @@ resource "aws_acm_certificate" "endpoint" {
}

resource "aws_route53_record" "endpoint" {
name = var.domain
name = local.fqdn
type = "A"
zone_id = data.aws_route53_zone.domain.zone_id

Expand Down
1 change: 1 addition & 0 deletions aws/fargate_service/local.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
locals {
fqdn = var.subdomain != "" ? "${var.subdomain}.${var.domain}" : var.domain
prefix = "${var.project}-${var.environment}-${var.service}"
prefix_short = "${var.project_short}-${var.environment}-${var.service_short}"
}
36 changes: 21 additions & 15 deletions aws/fargate_service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ module "ecr" {
repository_name = local.prefix
repository_image_scan_on_push = true
repository_encryption_type = "KMS"
repository_kms_key = aws_kms_key.fargate.arn
repository_image_tag_mutability = var.image_tags_mutable ? "MUTABLE" : "IMMUTABLE"
repository_kms_key = aws_kms_key.fargate.arn
repository_lifecycle_policy = jsonencode(yamldecode(templatefile(
"${path.module}/templates/repository-lifecycle.yaml.tftpl", {
untagged_image_retention : var.untagged_image_retention
Expand All @@ -23,8 +24,11 @@ module "endpoint_security_group" {
vpc_id = var.vpc_id

# Ingress for HTTP
ingress_cidr_blocks = [var.internal ? data.aws_vpc.current.cidr_block : "0.0.0.0/0"]
ingress_rules = ["http-80-tcp", "https-443-tcp"]
ingress_cidr_blocks = concat(
[var.public ? "0.0.0.0/0" : data.aws_vpc.current.cidr_block],
var.ingress_cidrs
)
ingress_rules = ["http-80-tcp", "https-443-tcp"]

# Allow all egress
egress_cidr_blocks = [data.aws_vpc.current.cidr_block]
Expand Down Expand Up @@ -69,18 +73,19 @@ module "ecs_service" {
version = "~> 4.2"
depends_on = [module.alb, module.ecs]

name = local.prefix
cluster = module.ecs.arn
container_port = var.container_port
container_name = local.prefix
cpu = 512
memory = 1024
desired_count = 1
vpc_subnets = var.private_subnets
target_group_arn = module.alb.target_groups["endpoint"].arn
security_groups = [module.task_security_group.security_group_id]
iam_daemon_role = aws_iam_role.execution.arn
iam_task_role = aws_iam_role.task.arn
name = local.prefix
cluster = module.ecs.arn
container_port = var.container_port
container_name = local.prefix
cpu = 512
memory = 1024
desired_count = 1
vpc_subnets = var.private_subnets
target_group_arn = module.alb.target_groups["endpoint"].arn
security_groups = [module.task_security_group.security_group_id]
iam_daemon_role = aws_iam_role.execution.arn
iam_task_role = aws_iam_role.task.arn
enable_execute_command = var.enable_execute_command

container_definitions = jsonencode(yamldecode(templatefile(
"${path.module}/templates/container_definitions.yaml.tftpl", {
Expand All @@ -93,6 +98,7 @@ module "ecs_service" {
region = data.aws_region.current.name
namespace = "${var.project}/${var.service}"
env_vars = var.environment_variables
otel_log_level = var.otel_log_level
}
)))
}
2 changes: 1 addition & 1 deletion aws/fargate_service/output.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ output "cluster_name" {
output "docker_push" {
value = <<EOT
aws ecr get-login-password --region ${data.aws_region.current.name} | docker login --username AWS --password-stdin ${module.ecr.repository_registry_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com
docker build -t ${module.ecr.repository_name} .
docker build -t ${module.ecr.repository_name} --platform linux/amd64 .
docker tag ${module.ecr.repository_name}:${var.image_tag} ${module.ecr.repository_url}:latest
docker push ${module.ecr.repository_url}:latest
EOT
Expand Down
23 changes: 8 additions & 15 deletions aws/fargate_service/templates/container_definitions.yaml.tftpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
awslogs-group: "${log_group}"
awslogs-region: "${region}"
awslogs-stream-prefix: "ecs"
linuxParameters:
initProcessEnabled: true
portMappings:
- containerPort: ${container_port}
environment:
%{~ for key, value in env_vars ~}
- name: ${key}
value: "${value}"
%{~ endfor ~}
- name: cwagent
image: public.ecr.aws/cloudwatch-agent/cloudwatch-agent:latest
- name: otel-collector
image: public.ecr.aws/aws-observability/aws-otel-collector:latest
memory: 512
cpu: 256
essential: false
Expand All @@ -27,17 +29,8 @@
options:
awslogs-group: "${log_group}"
awslogs-region: "${region}"
awslogs-stream-prefix: "cwagent"
awslogs-stream-prefix: "otel-collector"
environment:
- name: CW_CONFIG_CONTENT
value: |
{
"metrics": {
"namespace": "${namespace}",
"metrics_collected": {
"statsd": {
"service_address": ":8125"
}
}
}
}
- name: OTEL_LOG_LEVEL
value: ${otel_log_level}
command: ["--config=/etc/ecs/container-insights/otel-task-metrics-config.yaml"]
34 changes: 32 additions & 2 deletions aws/fargate_service/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ variable "domain" {
description = "Domain name for the service."
}

variable "enable_execute_command" {
type = bool
description = "Enable ECS Exec for tasks within the service."
default = false
}

variable "environment" {
type = string
description = "Environment for the deployment."
Expand All @@ -35,12 +41,18 @@ variable "image_tag" {
default = "latest"
}

variable "internal" {
variable "image_tags_mutable" {
type = bool
description = "This is an internal service that should not be exposed to the public Internet."
description = "Whether image tags in the repository can be mutated."
default = false
}

variable "ingress_cidrs" {
type = list(string)
description = "List of CIDR blocks to allow ingress from."
default = []
}

variable "key_recovery_period" {
type = number
default = 30
Expand All @@ -57,6 +69,12 @@ variable "logging_key_id" {
description = "KMS key ID for encrypting logs."
}

variable "otel_log_level" {
type = string
description = "Log level for the OpenTelemetry collector."
default = "info"
}

variable "private_subnets" {
type = list(string)
description = "List of private subnets."
Expand All @@ -67,6 +85,12 @@ variable "project" {
description = "Project that these resources are supporting."
}

variable "public" {
type = bool
description = "This is an internal service that should not be exposed to the public Internet."
default = false
}

variable "project_short" {
type = string
description = "Short name for the project. Used in resource names with character limits."
Expand All @@ -87,6 +111,12 @@ variable "service_short" {
description = "Short name for the service. Used in resource names with character limits."
}

variable "subdomain" {
type = string
description = "Optional subdomain for the service."
default = ""
}

variable "untagged_image_retention" {
type = number
default = 14
Expand Down
20 changes: 20 additions & 0 deletions aws/vpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ To update the source for this module, pass `-upgrade` to `tofu init`:
tofu init -upgrade
```

### VPC Peers

You can create VPC peering connections by passing a named map of VPCs to peer.
This will only initiate the peering connection from the VPC created by this, the
peer must be approved on the other side before it can be used.

For example:

```hcl
peers = {
"aptible": {
account_id: "123456789012",
vpc_id: "vpc-012ab34cde5678fa9",
region: "us-east-1",
cidr: "10.123.0.0/16"
}
}
```

## Inputs

| Name | Description | Type | Default | Required |
Expand All @@ -49,6 +68,7 @@ tofu init -upgrade
| public_subnets | List of public subnet CIDR blocks. | `list` | n/a | yes |
| log_retention_period | Retention period for flow logs, in days. | `string` | 30 | no |
| environment | Environment for the project. | `string` | `"dev"` | no |
| peers | List of VPC peering connections. | `map` | `{}` | no |
| single_nat_gateway | Create a single NAT gateway, rather than 1 in each private subnet. **_Cheaper, but not highly available._** | `bool` | `false` | no |

## Outputs
Expand Down
38 changes: 38 additions & 0 deletions aws/vpc/local.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,42 @@
locals {
azs = data.aws_availability_zones.available.names
prefix = "${var.project}-${var.environment}"

# Define inbound and outbound ACL rules for any peering connections.
peer_inbound_acls = [
for peer in var.peers : {
action = "allow"
cidr_block = peer.cidr
from_port = 0
protocol = -1
rule_number = 200
to_port = 0
}
]
peer_outbound_acls = [
for peer in var.peers : {
action = "allow"
cidr_block = peer.cidr
from_port = 0
protocol = -1
rule_number = 200
to_port = 0
}
]

# Create a set of peering routes based on the provided peers and the created
# private route tables.
peer_cidrs = [
for key, value in var.peers : {
key = key
cidr = value.cidr
}
]
peer_routes = [
for pair in setproduct(local.peer_cidrs, module.vpc.private_route_table_ids) : {
cidr = pair[0].cidr
key = pair[0].key
table_id = pair[1]
}
]
}
Loading

0 comments on commit d47b9d6

Please sign in to comment.