I have a single container that I'd like to run. Instead of spinning up a dedicated EKS cluster, I'd like to use ECS as the underlying AWS service. I describe my deployments with CDK in Python. The container doesn't necessarily need to be exposed via HTTP (ingress) but needs access to the internet (egress). I don't need a HA setup, just running one container and restart it based on a defined health check is fine.
I've been trying out multiple tutorials and arrived at this stack. Unfortunately there seems to be an issue pulling the image.
class MyStack(Stack):
def __init__(self, scope: Construct, id: str, props, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
repository = props["ecr-repository"]
vpc = aws_ec2.Vpc(self, "VPC", vpc_name="vpc", max_azs=2, nat_gateways=0)
vpc.add_interface_endpoint(
"S3Endpoint",
service=aws_ec2.InterfaceVpcEndpointAwsService.S3,
private_dns_enabled=False,
)
vpc.add_interface_endpoint(
"EcrDockerEndpoint",
service=aws_ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
)
vpc.add_interface_endpoint(
"EcrEndpoint", service=aws_ec2.InterfaceVpcEndpointAwsService.ECR
)
vpc.add_interface_endpoint(
"CloudWatchLogsEndpoint",
service=aws_ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
)
cluster = aws_ecs.Cluster(
self, "EcsCluster", cluster_name="ecs-cluster", vpc=vpc
)
execution_role = iam.Role(
self,
"ecs-devops-execution-role",
assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
role_name="ecs-devops-execution-role",
)
execution_role.add_to_policy(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
resources=["*"],
actions=[
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
)
)
task_definition = aws_ecs.FargateTaskDefinition(
self,
"EcsFargateTaskDefinition",
family="EcsFargateFamily",
execution_role=execution_role,
)
image = aws_ecs.ContainerImage.from_ecr_repository(
repository=repository, tag="1.0.0"
)
container = task_definition.add_container(
"app",
image=image,
environment=dict(
WEBSOCKET_CHANNELS="channel-name", WEBSOCKET_TIMEOUT_DURATION="PT15M"
),
)
log_group = aws_logs.LogGroup(
self,
"ecs-devops-service-logs-groups",
log_group_name="ecs-devops-service-logs",
)
container.add_port_mappings(aws_ecs.PortMapping(container_port=8080))
service = aws_ecs_patterns.ApplicationLoadBalancedFargateService(
self,
"EcsFargateService",
service_name="fargate-service",
cluster=cluster,
desired_count=1,
task_definition=task_definition,
)
service.target_group.configure_health_check(path="/actuator/health")
self.output_props = props.copy()
The error I receive is:
CannotPullContainerError: ref pull has been retried 5 time(s): failed to copy: httpReadSeeker: failed open: failed to do request: Get "https://prod-eu-central-1-starport-layer-bucket.s3.eu-central-1.amazonaws.com/...
Thanks to @gshpychka, I could fix the setup with this code:
class ComputeStack(Stack):
def __init__(self, scope: Construct, id: str, props, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
repository = props["ecr-repository"]
secret = props["sm-secret"]
vpc = aws_ec2.Vpc(self, "VPC", vpc_name="vpc", max_azs=2, nat_gateways=0)
cluster = aws_ecs.Cluster(
self, "EcsCluster", cluster_name="ecs-cluster", vpc=vpc
)
task_definition = aws_ecs.FargateTaskDefinition(
self,
"EcsFargateTaskDefinition",
memory_limit_mib=2048,
family="EcsFargateFamily",
)
image = aws_ecs.ContainerImage.from_ecr_repository(
repository=repository, tag="1.0.0"
)
container = task_definition.add_container(
"app",
image=image,
logging=aws_ecs.LogDrivers.aws_logs(stream_prefix="ecs-fargate"),
memory_limit_mib=2048,
environment=dict(
WEBSOCKET_CHANNELS="channel-name",
WEBSOCKET_TIMEOUT_DURATION="PT15M",
),
)
container.add_port_mappings(aws_ecs.PortMapping(container_port=8080))
service = aws_ecs_patterns.ApplicationLoadBalancedFargateService(
self,
"EcsFargateService",
service_name="fargate-service",
cluster=cluster,
desired_count=1,
assign_public_ip=True,
task_definition=task_definition,
)
service.target_group.configure_health_check(path="/actuator/health")
self.output_props = props.copy()
Thank you very much!
If your VPC does not have any NAT gateways, then you will have Public and Isolated subnets created automatically.
Isolated subnets do not have ingress or egress and need VPC endpoints run ECS tasks.
Public subnets have both ingress and egress and do not need any VPC endpoints.
By default, CDK places ECS services in Private/Isolated subnets, so your tasks will not have egress. VPC endpoints would help with running the task but will not provide any internet access.
To launch the tasks in a public subnet, either set the task_subnets
prop to ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC)
or set the assign_public_ip
to True
.
If you do not want ingress from the internet, you'll need a NAT gateway in the VPC.
You will not need any of the VPC endpoints you created.
P.S. You do not need to create the execution role, the default one will have all the necessary permissions added automatically.