The integration test suite for the team’s API needs to talk to a private RDS instance, an internal microservice with no public endpoint, and an ElastiCache Redis cluster. The current buildspec works around this by SSH-tunneling through a bastion, which fails intermittently and leaks credentials when it does. The right answer is CodeBuild VPC mode — the build runs inside the VPC and can hit private resources directly. But VPC mode is not free, and most CodeBuild projects shouldn’t use it. Here is the decision framework.
What’s happening
By default, CodeBuild runs in AWS-managed infrastructure outside your VPC. The build has full internet access via AWS-managed NAT and no path into your private subnets. This is correct for the common case — pull source, install dependencies, run tests against public services, push artifacts. Most builds need exactly that.
VPC mode attaches an Elastic Network Interface (ENI) from your VPC to the build container at startup. The build then sits inside one of your private subnets, reaches private resources via the VPC’s routing, and uses your VPC’s NAT Gateway for outbound internet (to pull packages from npm, the public Docker Hub, etc.). The build is gone, the ENI is detached when the build ends.
The trade-offs are real and non-obvious:
Cold-start ENI attach. Every VPC-mode build pays a 30-90 second penalty at startup while the ENI is provisioned and attached. CodeBuild has improved this over time (it used to be 5+ minutes), but 30-90 seconds on a 2-minute build is a 25%-75% overhead. On a 20-minute build it’s negligible. The penalty hits the start of every build, including failed builds.
NAT Gateway cost. Outbound traffic from the build (pulling npm packages, Docker base images, S3 sync to a public bucket) goes through your NAT Gateway. NAT charges per GB processed. A build that pulls 500 MB of npm packages and a 1 GB Docker base image processes 1.5 GB through NAT. Multiply by the build frequency. On a high-velocity team, NAT cost from CodeBuild becomes a noticeable line item — sometimes more than the CodeBuild compute itself.
Subnet IP exhaustion. Each running build consumes one IP from the subnet. A team running 30 concurrent builds in a /26 subnet (64 IPs) is fine. A team running 100 concurrent builds in the same subnet will see builds queued with INSUFFICIENT_FREE_ADDRESSES. Size subnets for CodeBuild assuming peak concurrent build count plus headroom.
VPC endpoint requirements. Without VPC endpoints for S3, ECR, Secrets Manager, KMS, and CloudWatch Logs, every interaction the build has with those services goes through the NAT Gateway. Add the endpoints — interface endpoints for the AWS services, a gateway endpoint for S3 — and the NAT bill drops by maybe 70%. The gateway endpoint for S3 is free; the interface endpoints have a fixed per-hour cost but pay for themselves at modest traffic.
The decision rule we apply: use VPC mode only when the build needs to reach private resources. Do not use VPC mode for the convenience of “everything is in the VPC.”
How to verify
Cases where VPC mode is the right answer:
-
Integration tests against a private RDS. The DB is in a private subnet with no public IP. The test needs to run migrations and assert against actual rows. SSH tunneling through a bastion is fragile; VPC mode is direct.
-
Builds that hit an internal API. A multi-service repo where the build of service A runs contract tests against the deployed service B, which is only reachable inside the VPC.
-
Compliance — no public IPs on build infrastructure. Some regulated workloads require all build traffic to traverse private networking. VPC mode plus VPC endpoints means the build never touches the public internet.
-
Pulling from a private package registry. A private CodeArtifact repository configured for VPC-only access, or a self-hosted npm registry behind an ALB with no public listener.
Cases where VPC mode is the wrong answer:
-
“Our infra is in a VPC, so the build should be too.” No. The build doesn’t need to be in the same VPC as the deployed app. Build outside, deploy via APIs that reach in.
-
Pulling Docker images from a public registry. Public Docker Hub, public ECR, public GitHub Container Registry — none need VPC mode. ECR images in your account are reachable from CodeBuild’s default network via the public endpoint with IAM auth.
-
Builds that only read from S3 and push to S3. S3 is reachable from the default network. VPC mode adds NAT cost and ENI overhead for no benefit.
-
Lint, typecheck, unit test. If the build never touches a private resource, VPC mode is pure overhead.
The hybrid pattern that works for many teams: split the build into two CodeBuild projects in a pipeline. The first project runs lint/typecheck/unit-test outside the VPC (fast, cheap). The second project runs integration tests inside the VPC (slow, expensive, only when needed). The second project triggers only on changes to the affected service or on the main branch. Most commits don’t pay the VPC penalty.
Configuring VPC mode at the project level:
aws codebuild update-project \
--name web-api-integration-tests \
--vpc-config '{
"vpcId": "vpc-0abc123def456",
"subnets": ["subnet-0aaa", "subnet-0bbb", "subnet-0ccc"],
"securityGroupIds": ["sg-0codebuild123"]
}'
The security group needs egress to the private resources the build targets, plus egress to 0.0.0.0/0 for the internet via NAT (or restricted egress if all dependencies are mirrored internally).
A common gotcha: the IAM role attached to the CodeBuild project needs ec2:CreateNetworkInterface, ec2:DeleteNetworkInterface, ec2:DescribeSubnets, ec2:DescribeSecurityGroups, and the unusual ec2:CreateNetworkInterfacePermission scoped to a specific service-linked principal. The managed AWSCodeBuildDeveloperAccess policy doesn’t include all of these; you need a custom policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DeleteNetworkInterface",
"ec2:DescribeDhcpOptions",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeVpcs"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "ec2:CreateNetworkInterfacePermission",
"Resource": "arn:aws:ec2:us-east-1:ACCOUNT-ID:network-interface/*",
"Condition": {
"StringEquals": {
"ec2:Subnet": [
"arn:aws:ec2:us-east-1:ACCOUNT-ID:subnet/subnet-0aaa",
"arn:aws:ec2:us-east-1:ACCOUNT-ID:subnet/subnet-0bbb",
"arn:aws:ec2:us-east-1:ACCOUNT-ID:subnet/subnet-0ccc"
],
"ec2:AuthorizedService": "codebuild.amazonaws.com"
}
}
}
]
}
The fix
The decision framework that holds across every CodeBuild VPC-mode call:
-
Default to no VPC. Only switch to VPC mode when the build’s correctness depends on reaching a private resource. The ENI cold start, NAT cost, and IP exhaustion risk are real overheads.
-
Split projects by need. If 80% of your builds don’t need VPC and 20% do, run two projects. Don’t pay the VPC overhead on every build.
-
Add VPC endpoints before turning on VPC mode. Interface endpoints for ECR (api and dkr), Secrets Manager, KMS, STS, CloudWatch Logs, and a gateway endpoint for S3. Without these, your NAT bill from CodeBuild grows linearly with build volume.
-
Multi-AZ subnets. Configure CodeBuild with at least two subnets in different AZs. Single-subnet projects fail when that AZ has a brief network issue and there’s no alternative for ENI placement.
-
Right-size the subnet. Estimate peak concurrent builds and pick subnets with at least 4x that many free IPs. A
/24(251 usable IPs after AWS reserves five) is comfortable for most teams. Don’t shove CodeBuild into a/28shared with other workloads. -
Security group with explicit egress. Don’t use the default VPC security group. The CodeBuild SG should allow egress only to what the build actually needs — RDS port to the DB SG, internal API ALB SG, 443 to 0.0.0.0/0 for package downloads. Tight egress prevents the build from reaching anything it shouldn’t.
-
Watch the cold-start metric. CloudWatch’s
CodeBuild/DownloadSourceDurationandCodeBuild/QueuedDurationshow the time-to-first-command. For VPC projects, this is your ENI attach time. If it’s consistently above 90 seconds, AWS Support can help — sometimes it’s a regional issue.
The simplest test that you’ve made the right call: build a CodeBuild project with VPC mode, run an empty buildspec that just echoes “hello”, and time it. If the wall-clock time is over 90 seconds for “hello”, you’d be paying that overhead on every build. Then ask: does this build actually need a private resource? If not, don’t use VPC mode.
Stack Harbor runs CodeBuild + CodePipeline at production scale as part of Managed AWS onboarding and ongoing Managed Cloud Ops. If your builds tunnel through a bastion or your NAT bill grew faster than your team did, start with an infrastructure consult.