Private Fargate Deployment with VPC Endpoints
VPC Endpoints allow you to have private containers, pulled from ECR repositories, with no external network ingress or egress. These containers can enhance your infrastructure security posture by utilizing AWS PrivateLink, restricting packets from going over the public internet.
Assumptions/Prereqs
- You have a private ECR repository, with a tagged image created in your AWS Region.
- The example code definitions will contain statically coded capacity metrics, as a low-cost deployment. If you are using this in your environment, it is recommended you variablize, or increase the static values.
- The code will not restrict the endpoints, as it is expected the roles on your resources will preform that restriction, however, there will be linked AWS reference documentation, if you require.
- The architecture will have zero public subnets or internet egress. You may require this additional infrastructure, but is outside the scope, and shouldn't impact the defined code.
Architecture
The environment will use a trimmed-down version of a common VPC configuration, with only 2 private subnets, and a Fargate instance.
Process
To enable containers to pull from ECR several steps are needed. Below broken down terraform code will walk through an end to end build, but this is the process that code will perform.
-
Configure a VPC with 2 private subnets and allow DNS Hostname Resolve and Support
-
Configure a VPC Endpoint Security Group to allow your VPC's CIDR as Ingress
- This will be utilized for all internal addresses to communicate with the interfaces, as private DNS will resolve to an internal address.
-
Create an ECR DKR Service Endpoint
- Service types:
com.amazonaws.<region>.ecr.dkr
- Minimal Permission Reference
- Service types:
-
Create a S3 Endpoint, which will connect to the starport bucket
- Service types:
com.amazonaws.<region>.s3
- Note from AWS Documentation:
The gateway endpoint is required because Amazon ECR uses Amazon S3 to store Docker image layers... The gateway endpoint is required because Amazon ECR uses Amazon S3 to store Docker image layers:
arn:aws:s3:::prod-<region>-starport-layer-bucket/*
- Service types:
-
Create Logs endpoint to allow your container to log to CloudWatch
- Service types:
com.amazonaws.<region>.logs
- Service types:
-
Configure your Fargate service with egress to the VPC and S3 private gateway
Terraform
Variable Declaration
Create a variables.tf file for the ECR Image you will pass in.
variable "app_image" {} |
Provider and Account Data
Initialize the config with providers and data.
provider "aws" { | |
region = "us-east-1" | |
} | |
data "aws_region" "current" {} | |
data "aws_caller_identity" "current" {} |
Security Groups
Provision the Security Groups for the VPC Endpoints and Fargate instance.
resource "aws_security_group" "vpce" { | |
name = "vpce" | |
vpc_id = aws_vpc.main.id | |
ingress { | |
from_port = 443 | |
to_port = 443 | |
protocol = "tcp" | |
cidr_blocks = [aws_vpc.main.cidr_block] | |
} | |
tags = { | |
Environment = "dev" | |
} | |
} | |
resource "aws_security_group" "ecs_task" { | |
name = "ecs" | |
vpc_id = aws_vpc.main.id | |
egress { | |
from_port = 443 | |
to_port = 443 | |
protocol = "tcp" | |
cidr_blocks = [aws_vpc.main.cidr_block] | |
} | |
egress { | |
from_port = 443 | |
to_port = 443 | |
protocol = "tcp" | |
prefix_list_ids = [aws_vpc_endpoint.s3.prefix_list_id] | |
} | |
tags = { | |
Environment = "dev" | |
} | |
} |
VPC Endpoints
Configure the VPC Endpoints for S3, DKR, and logging.
resource "aws_vpc_endpoint" "s3" { | |
vpc_id = aws_vpc.main.id | |
service_name = "com.amazonaws.${data.aws_region.current.name}.s3" | |
vpc_endpoint_type = "Gateway" | |
route_table_ids = [aws_route_table.private.id] | |
tags = { | |
Name = "s3-endpoint" | |
Environment = "dev" | |
} | |
} | |
resource "aws_vpc_endpoint" "dkr" { | |
vpc_id = aws_vpc.main.id | |
private_dns_enabled = true | |
service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.dkr" | |
vpc_endpoint_type = "Interface" | |
security_group_ids = [ | |
aws_security_group.vpce.id, | |
] | |
subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id] | |
tags = { | |
Name = "dkr-endpoint" | |
Environment = "dev" | |
} | |
} | |
resource "aws_vpc_endpoint" "logs" { | |
vpc_id = aws_vpc.main.id | |
private_dns_enabled = true | |
service_name = "com.amazonaws.${data.aws_region.current.name}.logs" | |
vpc_endpoint_type = "Interface" | |
security_group_ids = [ | |
aws_security_group.vpce.id, | |
] | |
subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id] | |
tags = { | |
Name = "logs-endpoint" | |
Environment = "dev" | |
} | |
} |
Fargate Service
Configure your IAM Role, task definition, and Fargate service.
data "aws_iam_policy_document" "fargate-role-policy" { | |
statement { | |
actions = ["sts:AssumeRole"] | |
principals { | |
type = "Service" | |
identifiers = ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com"] | |
} | |
} | |
} | |
resource "aws_iam_policy" "fargate_execution" { | |
name = "fargate_execution_policy" | |
policy = <<EOF | |
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ecr:GetDownloadUrlForLayer", | |
"ecr:BatchGetImage", | |
"ecr:BatchCheckLayerAvailability" | |
], | |
"Resource": "arn:aws:ecr:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:repository/${var.app_image}" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ecr:GetAuthorizationToken" | |
], | |
"Resource": "*" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"logs:CreateLogGroup", | |
"logs:CreateLogStream" | |
], | |
"Resource": "*" | |
} | |
] | |
} | |
EOF | |
} | |
resource "aws_iam_policy" "fargate_task" { | |
name = "fargate_task_policy" | |
policy = <<EOF | |
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"logs:CreateLogGroup", | |
"logs:CreateLogStream", | |
"logs:PutLogEvents" | |
], | |
"Resource": "*" | |
} | |
] | |
} | |
EOF | |
} | |
resource "aws_iam_role" "fargate_execution" { | |
name = "fargate_execution_role" | |
assume_role_policy = data.aws_iam_policy_document.fargate-role-policy.json | |
} | |
resource "aws_iam_role" "fargate_task" { | |
name = "fargate_task_role" | |
assume_role_policy = data.aws_iam_policy_document.fargate-role-policy.json | |
} | |
resource "aws_iam_role_policy_attachment" "fargate-execution" { | |
role = aws_iam_role.fargate_execution.name | |
policy_arn = aws_iam_policy.fargate_execution.arn | |
} | |
resource "aws_iam_role_policy_attachment" "fargate-task" { | |
role = aws_iam_role.fargate_task.name | |
policy_arn = aws_iam_policy.fargate_task.arn | |
} | |
# Fargate Container | |
resource "aws_cloudwatch_log_group" "app" { | |
name = "/ecs/app" | |
tags = { | |
Environment = "dev" | |
} | |
} | |
resource "aws_ecs_cluster" "main" { | |
depends_on = [aws_cloudwatch_log_group.app] | |
name = "cluster" | |
} | |
locals { | |
container_defintion = [{ | |
cpu = 1024 | |
image = "${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/${var.app_image}" | |
memory = 2048 | |
name = "app" | |
networkMode = "awsvpc" | |
logConfiguration = { | |
logdriver = "awslogs" | |
options = { | |
"awslogs-group" = "/ecs/app" | |
"awslogs-region" = data.aws_region.current.name | |
"awslogs-stream-prefix" = "stdout" | |
} | |
} | |
}] | |
} | |
resource "aws_ecs_task_definition" "app" { | |
family = "app" | |
network_mode = "awsvpc" | |
cpu = local.container_defintion.0.cpu | |
memory = local.container_defintion.0.memory | |
requires_compatibilities = ["FARGATE"] | |
container_definitions = jsonencode(local.container_defintion) | |
execution_role_arn = aws_iam_role.fargate_execution.arn | |
task_role_arn = aws_iam_role.fargate_task.arn | |
tags = { | |
Name = "app" | |
Environment = "dev" | |
} | |
} | |
resource "aws_ecs_service" "main" { | |
name = "service" | |
cluster = aws_ecs_cluster.main.id | |
task_definition = aws_ecs_task_definition.app.arn | |
desired_count = "1" | |
launch_type = "FARGATE" | |
network_configuration { | |
security_groups = [aws_security_group.ecs_task.id] | |
subnets = [aws_subnet.private_a.id] | |
} | |
} |
Execution
terraform apply -var app_image=<your ECR image name>
References
Amazon ECR Interface VPC Endpoints
Updates
(August 2) This post was written with Fargate 1.3. If having issues pulling from registry, ensure to check out this Stack Overflow solution.