Deploy Fargate with VPC Endpoints

Run containers in isolated private subnets using VPC endpoints and AWS CDK.

Introduction

30 Second Summary

When companies run sensitive workloads in the cloud, they often need containers that have zero internet access. Think banking transactions, healthcare records, or internal APIs that should never be publicly reachable. The challenge is that even "private" containers still need to talk to AWS services like image registries and logging.

In this project, you will deploy a containerized web service on AWS Fargate inside completely isolated subnets using AWS CDK. You will experience a real networking failure when your container cannot pull its image, then fix it by adding VPC endpoints that route traffic entirely through the AWS private network.

What You'll Build

By the end, you can query an internal load balancer from within your VPC and get a healthy response from a container running in a subnet with absolutely no internet access, proving that all AWS service communication flows privately through your endpoints.

By the end of this project, you'll have:

  • A running Fargate service you can query through an internal Application Load Balancer, returning a live response from a container that has zero internet routes.
  • First-hand experience of a designed networking failure where your tasks cannot pull images, followed by the fix that brings everything to life using VPC endpoints.
  • A production-style infrastructure-as-code stack in TypeScript that you can tear down and redeploy in minutes, demonstrating private connectivity to ECR, S3, and CloudWatch without a single byte touching the public internet.
  • Secret Mission: Add CPU-based auto-scaling to your Fargate service so it grows and shrinks with demand while keeping all traffic private.

Are there any prerequisites?

You need an AWS account with the CLI configured, Node.js and npm installed, and Docker installed. Basic TypeScript familiarity helps but is not strictly required.

Before We Start

Before you can define any infrastructure in code, you need the tooling that turns your TypeScript into live AWS resources. The AWS CDK CLI is the bridge between your local project and your AWS account.

In this step, you will install the CDK CLI, prepare your AWS account for CDK deployments, and scaffold a fresh TypeScript project that will hold all your infrastructure code.

In this step, get ready to:
  • Install the AWS CDK CLI and verify it works.
  • Bootstrap your AWS account for CDK deployments.
  • Scaffold a new CDK TypeScript project.
Install the AWS CDK CLI

The CDK CLI is a command-line tool that synthesizes your TypeScript code into CloudFormation templates and deploys them to AWS. You install it globally with npm.

  • Install the CDK CLI globally by running this command:
npm install -g aws-cdk
  • Verify the installation by running:
cdk --version

You should see a version number printed, such as 2.261.0.

Not seeing a version number?

If you see "command not found", close your terminal and open a new one. Global npm installs sometimes need a fresh terminal session to appear on your PATH.

If the install itself failed, confirm Node.js is installed by running node --version. You need Node.js available before npm can install packages.

Help me troubleshoot my CDK CLI installation

Bootstrap your AWS account

CDK needs a staging area in your AWS account before it can deploy anything. Bootstrapping creates a CloudFormation stack with an S3 bucket and IAM roles that CDK uses during deployment.

  • Bootstrap your account by running this command:
cdk bootstrap

This takes a minute or two. You will see CloudFormation events scrolling by as it creates the staging resources.

When it finishes, you should see a message containing "Environment bootstrapped" confirming success.

What if my account is already bootstrapped?

If you have used CDK before, the command will confirm the existing bootstrap stack is up to date. Either outcome is fine. You only need the bootstrap stack to exist.

Seeing an access denied error?

Make sure your AWS CLI is configured with credentials that have administrator access. Run aws sts get-caller-identity to confirm your CLI is authenticated.

Help me fix my CDK bootstrap error

Scaffold a new CDK TypeScript project

Now you will create the project directory and scaffold it with CDK's built-in TypeScript template. This generates all the boilerplate files you need to start defining infrastructure.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir fargate-vpc-endpoints
cd fargate-vpc-endpoints
  • Scaffold the CDK TypeScript project by running:
cdk init app --language typescript

What does this command do?

The cdk init command creates a complete CDK project in the current directory. It uses the folder name (fargate-vpc-endpoints) as the project name.

The key files it generates are bin/fargate-vpc-endpoints.ts (the app entry point) and lib/fargate-vpc-endpoints-stack.ts (where you will define your infrastructure). It also installs all npm dependencies automatically.

Before you verify, what files do you expect to see in the bin/ and lib/ directories?

  • Confirm the scaffold created the expected files by running:
ls bin/ lib/

You should see fargate-vpc-endpoints.ts listed under bin/ and fargate-vpc-endpoints-stack.ts listed under lib/. These are the two files you will work in throughout this project.

Not seeing the expected files?

Make sure you ran cdk init from inside the fargate-vpc-endpoints folder. If you ran it in the wrong directory, delete the generated files and re-run from the correct location.

If cdk init reported an error about the directory not being empty, make sure you created a fresh folder with nothing in it before running the command.

Help me troubleshoot my CDK project scaffold

Your tooling is ready and your project is scaffolded. Next up, you will build a container image and push it to ECR so your Fargate tasks have something to run.

Build and Push a Container Image to ECR

Your CDK project is scaffolded and ready to define infrastructure. Before you get into networking and services, your Fargate tasks need a container image to pull.

In this step, you will create a simple nginx-based container image and push it to Amazon ECR (Elastic Container Registry). This establishes the image your Fargate service will reference when you define the infrastructure in later steps.

In this step, get ready to:
  • Create a container directory with a Dockerfile and HTML page.
  • Create an ECR repository and push your container image to it.
Create the container directory and Dockerfile

Your container needs two files: a Dockerfile that defines the image, and an index.html page the web server will serve.

  • Create a container/ directory inside your fargate-vpc-endpoints project folder by running this command:
mkdir container
  • Create the container/Dockerfile with the following content:
FROM public.ecr.aws/nginx/nginx:1-alpine-slim
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80

What does this Dockerfile do?

  • FROM public.ecr.aws/nginx/nginx:1-alpine-slim pulls a lightweight nginx image from the public ECR registry. No authentication is needed for public images.
  • COPY index.html places your custom HTML page into the nginx default serving directory.
  • EXPOSE 80 declares that the container listens on port 80.
  • Create the container/index.html file with this content:
<!DOCTYPE html>
<html>
<head><title>Fargate VPC Endpoints Demo</title></head>
<body>
<h1>Hello from Fargate in an isolated subnet!</h1>
<p>This container was pulled through VPC endpoints with zero internet access.</p>
</body>
</html>
  • Confirm both files exist by running:
ls container/

You should see Dockerfile and index.html listed in the output.

Don't see both files?

  • Make sure you created the files inside the container/ directory, not in the project root.
  • Check that you ran mkdir container first. If the directory does not exist, the file creation would have failed.

Help me debug why my container directory is missing files.

Create the ECR repository and authenticate Docker

ECR is a managed container registry where your Fargate tasks will pull images from. You need to create a repository and authenticate Docker before you can push to it.

  • Create the ECR repository by running:
aws ecr create-repository --repository-name fargate-vpc-demo

You should see a JSON response containing a repositoryUri field. This URI is where your image will live.

What is a repositoryUri?

The repositoryUri follows the format <account-id>.dkr.ecr.<region>.amazonaws.com/fargate-vpc-demo. You will use this URI to tag and push your image. Note your account ID and region from this output.

  • Authenticate Docker to your ECR registry by running:
aws ecr get-login-password --region [[AWS_REGION="your AWS region e.g. us-east-1"]] | docker login --username AWS --password-stdin [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com

You should see Login Succeeded in your terminal output.

Where do I find my account ID and region?

Your account ID is the 12-digit number from the repositoryUri output above. Your region is part of the same URI (e.g. us-east-1).

You can also run aws sts get-caller-identity to see your account ID, and aws configure get region to see your configured region.

Login failed or access denied?

  • Make sure Docker Desktop is running. The docker login command needs the Docker daemon to be active.
  • Double-check that the region in the command matches the region where you created the repository.
  • Verify your AWS CLI credentials are configured correctly by running aws sts get-caller-identity.

Help me fix my Docker ECR authentication.

Build, tag, and push the image

With authentication in place, you can now build your container image locally and push it to ECR.

  • Build the container image by running:
docker build -t fargate-vpc-demo ./container

You should see Docker pull the base nginx image and complete the build with a success message.

  • Tag the image with your ECR repository URI by running:
docker tag fargate-vpc-demo:latest [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com/fargate-vpc-demo:latest
  • Push the image to ECR by running:
docker push [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com/fargate-vpc-demo:latest

You should see each layer being pushed, ending with the latest tag being successfully uploaded.

Push failed with authorization error?

  • ECR login tokens expire after 12 hours. If time has passed, re-run the aws ecr get-login-password command from the previous substep.
  • Make sure the tag command used the exact same account ID and region as your docker login command.

Help me troubleshoot my Docker push to ECR.

Before you check the final result, what do you expect the output to show when you query ECR for your image tags?

  • Verify the image is in ECR by running:
aws ecr describe-images --repository-name fargate-vpc-demo --query 'imageDetails[0].imageTags'

You should see ["latest"] in the output, confirming your image is stored in ECR and ready for Fargate to pull.

Your container image is now safely stored in ECR. Next up, you will define the VPC, ALB, and Fargate service that pulls this image.

Define the VPC, ALB, and Fargate Service (Without VPC Endpoints)

Your container image is sitting in ECR, ready to be pulled. Now you need the infrastructure to run it.

In this step, you will define a VPC with completely isolated private subnets, deploy a Fargate service into them, and watch the deployment fail. The failure is intentional. Isolated subnets have zero internet access, which means your tasks cannot reach ECR to pull the container image. This is the exact problem that VPC endpoints solve, and seeing it break first makes the fix stick.

In this step, get ready to:
  • Define a VPC with public and private isolated subnets across 2 AZs.
  • Wire up an internal ALB, ECS cluster, and Fargate service pointing at your ECR image.
  • Deploy and observe the tasks fail with a container pull error.
Define the VPC with public and private isolated subnets

The VPC is the network boundary for all your resources. You will configure two subnet groups: public subnets (which have a route to the internet) and private isolated subnets (which have no routes anywhere outside the VPC).

  • Open bin/fargate-vpc-endpoints.ts in neovim and replace the entire file content with the following entry point that passes your AWS account and region to the stack:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { FargateVpcEndpointsStack } from '../lib/fargate-vpc-endpoints-stack';

const app = new cdk.App();
new FargateVpcEndpointsStack(app, 'FargateVpcEndpointsStack', {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

What does this change?

The auto-generated entry point creates a stack with no explicit environment. Adding the env property tells CDK which AWS account and region to deploy into, which is required for VPC constructs that reference Availability Zones.

  • Save bin/fargate-vpc-endpoints.ts.
  • Now open lib/fargate-vpc-endpoints-stack.ts and replace its entire content with the imports and VPC definition below:
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import { Construct } from 'constructs';

export class FargateVpcEndpointsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC with public and private isolated subnets, no NAT gateways
    const vpc = new ec2.Vpc(this, 'FargateVpc', {
      maxAzs: 2,
      natGateways: 0,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'PrivateIsolated',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
    });
  }
}

What does this code do?

This creates a VPC spanning 2 Availability Zones with natGateways: 0. That means the private isolated subnets have absolutely no route to the internet. No NAT gateway, no internet gateway route.

The public subnets exist because CDK requires at least one subnet with an internet gateway route for certain validations, but no workloads will run in them.

  • Save lib/fargate-vpc-endpoints-stack.ts.
  • Verify the TypeScript compiles by running:
npx tsc --noEmit

You should see no output, which means zero compile errors.

Seeing compile errors?

Check that you have all five import lines at the top of the file and that the class closing braces match. The file should end with two closing braces: one for the constructor and one for the class.

help me fix TypeScript compile errors in my CDK stack

Add the ECS cluster, task definition, and Fargate service

With the VPC in place, you need the compute layer: a reference to your ECR repository, an ECS cluster, a task definition describing your container, and the Fargate service that runs it.

  • In lib/fargate-vpc-endpoints-stack.ts, find the closing }); of the VPC definition (the line just before the constructor's closing brace). Add the following code below it:
    // Reference the existing ECR repository
    const repository = ecr.Repository.fromRepositoryName(
      this,
      'FargateVpcDemo',
      'fargate-vpc-demo'
    );

    // ECS Cluster
    const cluster = new ecs.Cluster(this, 'Cluster', { vpc });

    // Fargate Task Definition
    const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
      memoryLimitMiB: 512,
      cpu: 256,
    });

    taskDefinition.addContainer('WebContainer', {
      image: ecs.ContainerImage.fromEcrRepository(repository, 'latest'),
      logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'fargate-vpc-demo' }),
      portMappings: [{ containerPort: 80 }],
    });

What does this code do?

Repository.fromRepositoryName creates a reference to the ECR repository you created in Step 2 without trying to manage it. CDK will look up fargate-vpc-demo and use its URI for the task definition.

The task definition allocates 256 CPU units (0.25 vCPU) and 512 MB of memory. The container maps port 80 and sends logs to CloudWatch using the awsLogs driver.

  • Directly below the addContainer block you just added, paste the Fargate service and internal ALB:
    // Fargate Service in private isolated subnets
    const service = new ecs.FargateService(this, 'FargateService', {
      cluster,
      taskDefinition,
      desiredCount: 1,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
      assignPublicIp: false,
    });

    // Internal ALB in private isolated subnets
    const alb = new elbv2.ApplicationLoadBalancer(this, 'InternalAlb', {
      vpc,
      internetFacing: false,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
    });

    const listener = alb.addListener('HttpListener', { port: 80 });

    listener.addTargets('FargateTarget', {
      port: 80,
      targets: [service],
      healthCheck: { path: '/', interval: cdk.Duration.seconds(30) },
    });

What does this code do?

The Fargate service runs one task in the private isolated subnets with no public IP. The Application Load Balancer is internal (internetFacing: false), meaning it is only reachable from within the VPC. The listener on port 80 forwards traffic to the container.

  • Finally, add the stack output right before the constructor's closing brace:
    // Output the ALB DNS name
    new cdk.CfnOutput(this, 'AlbDnsName', {
      value: alb.loadBalancerDnsName,
      description: 'Internal ALB DNS name (only reachable from within the VPC)',
    });
  • Save lib/fargate-vpc-endpoints-stack.ts.
  • Verify the full file compiles cleanly by running:
npx tsc --noEmit

Again, no output means success.

✔️ Awesome, I've got everything!

Great. Make sure you have saved both files before continuing.

ⓧ I'd like to double check the full code

Your lib/fargate-vpc-endpoints-stack.ts should contain these sections in order. Scroll up and confirm each is present:

  • Five import lines (cdk, ec2, ecs, elbv2, ecr) plus the Construct import.
  • Class declaration and constructor opening with super(scope, id, props).
  • VPC definition with maxAzs: 2, natGateways: 0, PUBLIC and PRIVATE_ISOLATED subnet config.
  • ECR repository reference (fromRepositoryName).
  • ECS Cluster creation.
  • FargateTaskDefinition with addContainer referencing the ECR image.
  • FargateService with PRIVATE_ISOLATED subnets and assignPublicIp: false.
  • Internal ALB with listener and target group.
  • CfnOutput for the ALB DNS name.
  • Closing braces for the constructor and class.

Your bin/fargate-vpc-endpoints.ts should look like:

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { FargateVpcEndpointsStack } from '../lib/fargate-vpc-endpoints-stack';

const app = new cdk.App();
new FargateVpcEndpointsStack(app, 'FargateVpcEndpointsStack', {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

Seeing compile errors after adding all the code?

Make sure the CfnOutput block sits inside the constructor (before the last two closing braces). A common mistake is placing code after the class body.

help me fix my CDK stack that won't compile

Deploy and observe the failure

Everything compiles. Time to deploy this stack to AWS and see what happens when Fargate tasks try to pull an image from a subnet with no way out.

Before you run the deploy, think about this: your Fargate tasks are in subnets with zero internet access. They need to pull your container image from ECR. What do you think will happen?

  • Deploy the stack by running:
cdk deploy --require-approval never

How long does this take?

The first deploy creates a VPC, subnets, route tables, security groups, an ECS cluster, an ALB, and a Fargate service. Expect it to take 3-5 minutes. CDK will print progress events to your terminal as each resource is created.

Once the deploy finishes, CDK will print the stack outputs including the ALB DNS name. The stack itself deployed successfully. But the Fargate tasks inside it are a different story.

  • Check the ECS service status by running:
aws ecs list-clusters --query 'clusterArns[0]' --output text
  • Copy the cluster ARN from the output. Then list the services in that cluster:
aws ecs list-services --cluster [[CLUSTER_ARN="your cluster ARN from the previous command"]] --query 'serviceArns[0]' --output text
  • Now describe the service to see its current state:
aws ecs describe-services --cluster [[CLUSTER_ARN="your cluster ARN"]] --services [[SERVICE_ARN="your service ARN from the previous command"]] --query 'services[0].{running: runningCount, desired: desiredCount, events: events[0:2]}' --output json

You will see runningCount: 0 and desiredCount: 1. The events will show tasks failing to start. This is the designed failure.

This failure is expected

Your tasks are stuck because they cannot pull the container image from ECR. The private isolated subnets have no NAT gateway and no VPC endpoint. The tasks have no network path to reach the ECR API or download image layers from S3.

The service events will reference tasks stopping with a container pull failure. This is exactly the gap that VPC endpoints fill: they create a private network path from your isolated subnets to AWS services without needing internet access.

Your infrastructure is deployed but broken by design. In the next step, you will add VPC endpoints that give your isolated subnets a private path to ECR, S3, and CloudWatch Logs, fixing the deployment without adding a single byte of internet access.

Add VPC Endpoints and Fix the Deployment

Your Fargate tasks are stuck in a STOPPED state because isolated subnets have zero internet access. The tasks cannot reach ECR to pull your container image, S3 to download image layers, or CloudWatch Logs to send output.

The fix is not to add internet access. Instead, you will add VPC endpoints that create private network paths from your subnets directly to these AWS services. All traffic stays on the AWS backbone.

In this step, get ready to:
  • Add an S3 Gateway Endpoint for image layer downloads.
  • Add Interface Endpoints for ECR, ECR Docker, and CloudWatch Logs.
  • Redeploy and verify your tasks are running healthy.
Add a Gateway VPC Endpoint for S3

ECR stores container image layers in S3. Your tasks need a path to S3 to download those layers during image pull. A Gateway Endpoint for S3 adds a route to your VPC's route tables at no extra cost.

  • In lib/fargate-vpc-endpoints-stack.ts, find the closing of the VPC definition (the line that ends with }); after the subnet configuration).
  • Add the following code directly below the VPC definition, before the repository lookup:
    // VPC Endpoints for private AWS service access
    vpc.addGatewayEndpoint('S3Endpoint', {
      service: ec2.GatewayVpcEndpointAwsService.S3,
    });

What does this code do?

A Gateway Endpoint is a route-table entry that sends traffic destined for S3 through the AWS private network instead of the internet.

Gateway Endpoints are free. They only work for S3 and DynamoDB. For other services, you need Interface Endpoints.

  • Save fargate-vpc-endpoints-stack.ts.
Add Interface VPC Endpoints for ECR, ECR Docker, and CloudWatch Logs

Your tasks also need to authenticate with ECR, pull the Docker image manifest, and push logs to CloudWatch. Each of these requires its own Interface Endpoint powered by PrivateLink.

  • In lib/fargate-vpc-endpoints-stack.ts, add the following code directly below the S3 Gateway Endpoint you just added:
    vpc.addInterfaceEndpoint('EcrEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.ECR,
    });

    vpc.addInterfaceEndpoint('EcrDockerEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
    });

    vpc.addInterfaceEndpoint('CloudWatchLogsEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
    });

Why three separate endpoints?

ECR uses two distinct APIs. The ECR endpoint handles authentication and image metadata. The ECR_DOCKER endpoint handles the Docker pull protocol (fetching the image manifest and layers).

The CLOUDWATCH_LOGS endpoint lets your container send log output privately. Without it, the awsLogs log driver in your task definition would have nowhere to send data.

What about cost?

Interface Endpoints have per-hour and per-GB data transfer charges. With 3 endpoints across 2 AZs, expect roughly $0.06/hr while the stack is running. Tear down after verifying to minimize cost.

  • Save fargate-vpc-endpoints-stack.ts.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before deploying.

ⓧ I'd like to double check the full code

Your VPC endpoints section (between the VPC definition and the repository lookup) should contain these four calls in order:

  • vpc.addGatewayEndpoint('S3Endpoint', ...)
  • vpc.addInterfaceEndpoint('EcrEndpoint', ...)
  • vpc.addInterfaceEndpoint('EcrDockerEndpoint', ...)
  • vpc.addInterfaceEndpoint('CloudWatchLogsEndpoint', ...)

Scroll up and confirm each endpoint block matches what you pasted. The rest of the file (cluster, task definition, service, ALB) remains unchanged from the previous step.

Redeploy and verify the fix

With the endpoints in place, your isolated subnets now have private paths to every AWS service the Fargate tasks need. Time to redeploy and see the difference.

  • Deploy the updated stack by running:
cdk deploy --require-approval never

What happens during this deployment?

CDK creates the four new VPC endpoint resources in your stack. Once the endpoints are active, ECS automatically retries the failed tasks. This deploy takes 3-5 minutes because Interface Endpoints need ENIs provisioned in each AZ.

Before you check the service status, predict what you expect to see. Do you think the tasks will be running now, or will they still fail?

  • Wait 2-3 minutes after the deployment finishes for ECS to start new tasks.
  • Find your cluster name by running:
aws ecs list-clusters --query 'clusterArns[0]' --output text
  • Copy the cluster ARN from the output. It looks like arn:aws:ecs:<region>:<account>:cluster/FargateVpcEndpointsStack-....
  • Find your service name by running:
aws ecs list-services --cluster [[ECS_CLUSTER="your-cluster-ARN-from-above"]] --query 'serviceArns[0]' --output text
  • Now check the service status by running:
aws ecs describe-services --cluster [[ECS_CLUSTER="your-cluster-ARN"]] --services [[ECS_SERVICE="your-service-ARN"]] --query 'services[0].{runningCount:runningCount,desiredCount:desiredCount,status:status}' --output table

You should see runningCount: 1 and desiredCount: 1. Your task is alive and healthy inside the isolated subnet.

Still showing runningCount 0?

ECS can take up to 5 minutes to retry after the endpoints become active. Wait a minute and re-run the describe-services command. If tasks still fail, check that all four endpoints deployed by running cdk diff (it should show no changes).

  • Confirm logs are flowing through the CloudWatch Logs endpoint by running:
aws logs describe-log-groups --log-group-name-prefix fargate-vpc-demo --query 'logGroups[0].logGroupName' --output text

You should see a log group name containing fargate-vpc-demo printed. This confirms your container is sending logs through the CloudWatch Logs VPC endpoint with zero internet access.

No log group found?

If the command returns nothing, your tasks may not have started logging yet. Wait another minute for the container to initialize, then retry.

If tasks are running but no log group appears, verify the CloudWatchLogsEndpoint is in your stack code and was deployed successfully.

Still stuck? Help me troubleshoot why my Fargate tasks aren't logging to CloudWatch through a VPC endpoint.

Your container is running in a completely isolated subnet with zero internet access. Every AWS service call flows through the private network via your VPC endpoints. Next up, you will add auto-scaling so this service can handle real traffic.

Secret mission

Add Auto-Scaling to the Fargate Service

Your Fargate service handles traffic, but it runs a single task no matter how much demand hits it. In production, that is a recipe for downtime. Can you add CPU-based auto-scaling so your service scales out under load and scales back in when things calm down?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses VPC Interface Endpoints, an internal ALB, and Fargate tasks that accrue charges while running.

Cost warning

Your deployed stack costs approximately $0.09/hr ($2.16/day) while running. The three Interface Endpoints account for most of that cost. If you are done experimenting, tear everything down now to avoid unexpected charges.

Resources you used:

  • FargateVpcEndpointsStack (VPC, subnets, VPC endpoints, ECS cluster, Fargate service, internal ALB).
  • ECR repository (fargate-vpc-demo).
  • Local project files (fargate-vpc-endpoints folder on your Desktop).
  • AWS CDK CLI (globally installed npm package).

Keep everything running

No action needed. Choose this if you are still actively building on this project.

  • Your stack continues running and accruing charges at approximately $0.09/hr.
  • The ECR repository stores your image (free for the first 500 MB).
  • The AWS CDK CLI remains installed globally on your machine.
  • Your local project files remain in the fargate-vpc-endpoints folder on your Desktop.

Pause - I'll come back to this later

Tear down the stack to stop all charges. You can rebuild everything at any time with a single deploy command because your CDK code and container image remain intact.

  • Destroy the CloudFormation stack by running:
  • When prompted, confirm with y. This deletes the VPC, endpoints, ECS cluster, Fargate service, and ALB.
  • Keep your ECR image and local project files. When you are ready to resume, run cdk deploy from the project folder to recreate everything.
cdk destroy

Delete - I don't want to use this again

Remove all project resources completely to start fresh.

  • Destroy the CloudFormation stack by running:
cdk destroy
  • When prompted, confirm with y.
  • Delete the ECR repository by running:
aws ecr delete-repository --repository-name fargate-vpc-demo --force
  • Delete the local project files by running:

macOS

cd ~/Desktop
rm -rf fargate-vpc-endpoints

Windows

cd ~\Desktop
rmdir /s /q fargate-vpc-endpoints
  • Uninstall the AWS CDK CLI by running:
npm uninstall -g aws-cdk

That's a Wrap!

That's a Wrap!

Nice work! You just deployed a fully private Fargate service inside isolated subnets with zero internet access, pulling container images and shipping logs entirely through VPC endpoints.

You've learned how to:

  • Build a custom VPC with public and private isolated subnets using AWS CDK in TypeScript, and deploy an internal Application Load Balancer routing traffic to Fargate tasks.
  • Experience a real networking failure by deploying containers in isolated subnets without VPC endpoints, watching tasks fail with CannotPullContainerError, and understanding exactly why it happened.
  • Fix the failure by adding an S3 Gateway Endpoint and Interface Endpoints for ECR, ECR Docker, and CloudWatch Logs, proving that VPC endpoints enable fully private AWS service access without internet connectivity.
  • Secret Mission: Added CPU-based auto-scaling to the Fargate service so it scales up under load and scales back down when demand drops.

Ready to quiz yourself?