VPC: 10.0.0.0/16
├── Public Subnets (one per AZ)
│ ├── 10.0.1.0/24 (us-east-1a)
│ ├── 10.0.2.0/24 (us-east-1b)
│ └── NAT Gateways (for public-subnet resources only)
└── Private Subnets (one per AZ) ├── 10.0.10.0/24 (us-east-1a) └── 10.0.11.0/24 (us-east-1b) — No internet route in route table — VPC endpoints attached
VPC: 10.0.0.0/16
├── Public Subnets (one per AZ)
│ ├── 10.0.1.0/24 (us-east-1a)
│ ├── 10.0.2.0/24 (us-east-1b)
│ └── NAT Gateways (for public-subnet resources only)
└── Private Subnets (one per AZ) ├── 10.0.10.0/24 (us-east-1a) └── 10.0.11.0/24 (us-east-1b) — No internet route in route table — VPC endpoints attached
VPC: 10.0.0.0/16
├── Public Subnets (one per AZ)
│ ├── 10.0.1.0/24 (us-east-1a)
│ ├── 10.0.2.0/24 (us-east-1b)
│ └── NAT Gateways (for public-subnet resources only)
└── Private Subnets (one per AZ) ├── 10.0.10.0/24 (us-east-1a) └── 10.0.11.0/24 (us-east-1b) — No internet route in route table — VPC endpoints attached
# ECR — required for image pulls
aws ec2 create-vpc-endpoint --vpc-id vpc-xxx \ --service-name com.amazonaws.us-east-1.ecr.api \ --vpc-endpoint-type Interface \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints aws ec2 create-vpc-endpoint --vpc-id vpc-xxx \ --service-name com.amazonaws.us-east-1.ecr.dkr \ --vpc-endpoint-type Interface \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints
# ECR — required for image pulls
aws ec2 create-vpc-endpoint --vpc-id vpc-xxx \ --service-name com.amazonaws.us-east-1.ecr.api \ --vpc-endpoint-type Interface \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints aws ec2 create-vpc-endpoint --vpc-id vpc-xxx \ --service-name com.amazonaws.us-east-1.ecr.dkr \ --vpc-endpoint-type Interface \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints
# ECR — required for image pulls
aws ec2 create-vpc-endpoint --vpc-id vpc-xxx \ --service-name com.amazonaws.us-east-1.ecr.api \ --vpc-endpoint-type Interface \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints aws ec2 create-vpc-endpoint --vpc-id vpc-xxx \ --service-name com.amazonaws.us-east-1.ecr.dkr \ --vpc-endpoint-type Interface \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints
eksctl create cluster \ --name fintech-prod \ --region us-east-1 \ --vpc-private-subnets subnet-xxx,subnet-yyy \ --node-private-networking \ --endpoint-private-access true \ --endpoint-public-access false
eksctl create cluster \ --name fintech-prod \ --region us-east-1 \ --vpc-private-subnets subnet-xxx,subnet-yyy \ --node-private-networking \ --endpoint-private-access true \ --endpoint-public-access false
eksctl create cluster \ --name fintech-prod \ --region us-east-1 \ --vpc-private-subnets subnet-xxx,subnet-yyy \ --node-private-networking \ --endpoint-private-access true \ --endpoint-public-access false
# nodegroup.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata: name: fintech-prod region: us-east-1
managedNodeGroups: - name: workers instanceTypes: ["m6i.xlarge", "m6i.2xlarge"] minSize: 2 maxSize: 10 desiredCapacity: 3 privateNetworking: true iam: withAddonPolicies: autoScaler: true cloudWatch: true
# nodegroup.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata: name: fintech-prod region: us-east-1
managedNodeGroups: - name: workers instanceTypes: ["m6i.xlarge", "m6i.2xlarge"] minSize: 2 maxSize: 10 desiredCapacity: 3 privateNetworking: true iam: withAddonPolicies: autoScaler: true cloudWatch: true
# nodegroup.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata: name: fintech-prod region: us-east-1
managedNodeGroups: - name: workers instanceTypes: ["m6i.xlarge", "m6i.2xlarge"] minSize: 2 maxSize: 10 desiredCapacity: 3 privateNetworking: true iam: withAddonPolicies: autoScaler: true cloudWatch: true
# Pull, retag, and push to private ECR
docker pull registry.k8s.io/autoscaling/cluster-autoscaler:v1.29.0
docker tag registry.k8s.io/autoscaling/cluster-autoscaler:v1.29.0 \ 123456789.dkr.ecr.us-east-1.amazonaws.com/cluster-autoscaler:v1.29.0
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/cluster-autoscaler:v1.29.0
# Pull, retag, and push to private ECR
docker pull registry.k8s.io/autoscaling/cluster-autoscaler:v1.29.0
docker tag registry.k8s.io/autoscaling/cluster-autoscaler:v1.29.0 \ 123456789.dkr.ecr.us-east-1.amazonaws.com/cluster-autoscaler:v1.29.0
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/cluster-autoscaler:v1.29.0
# Pull, retag, and push to private ECR
docker pull registry.k8s.io/autoscaling/cluster-autoscaler:v1.29.0
docker tag registry.k8s.io/autoscaling/cluster-autoscaler:v1.29.0 \ 123456789.dkr.ecr.us-east-1.amazonaws.com/cluster-autoscaler:v1.29.0
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/cluster-autoscaler:v1.29.0
Developer pushes code ↓
GitHub Actions (or CodePipeline) ↓
Build image in CI environment (with internet access) ↓
Push image to Private ECR via VPC endpoint ↓
Trigger deployment (CodePipeline or self-hosted runner in VPC) ↓
kubectl/Helm apply via Systems Manager Session Manager ↓
EKS pulls image from private ECR (no internet needed) ↓
ALB routes traffic
Developer pushes code ↓
GitHub Actions (or CodePipeline) ↓
Build image in CI environment (with internet access) ↓
Push image to Private ECR via VPC endpoint ↓
Trigger deployment (CodePipeline or self-hosted runner in VPC) ↓
kubectl/Helm apply via Systems Manager Session Manager ↓
EKS pulls image from private ECR (no internet needed) ↓
ALB routes traffic
Developer pushes code ↓
GitHub Actions (or CodePipeline) ↓
Build image in CI environment (with internet access) ↓
Push image to Private ECR via VPC endpoint ↓
Trigger deployment (CodePipeline or self-hosted runner in VPC) ↓
kubectl/Helm apply via Systems Manager Session Manager ↓
EKS pulls image from private ECR (no internet needed) ↓
ALB routes traffic
# .github/workflows/deploy.yml
jobs: deploy: runs-on: self-hosted # runner inside VPC steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/cicd-deploy-role aws-region: us-east-1 - name: Login to ECR run: | aws ecr get-login-password | docker login \ --username AWS \ --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com - name: Deploy to EKS run: | aws eks update-kubeconfig --name fintech-prod helm upgrade --install my-app ./helm/my-app \ --set image.tag=${{ github.sha }}
# .github/workflows/deploy.yml
jobs: deploy: runs-on: self-hosted # runner inside VPC steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/cicd-deploy-role aws-region: us-east-1 - name: Login to ECR run: | aws ecr get-login-password | docker login \ --username AWS \ --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com - name: Deploy to EKS run: | aws eks update-kubeconfig --name fintech-prod helm upgrade --install my-app ./helm/my-app \ --set image.tag=${{ github.sha }}
# .github/workflows/deploy.yml
jobs: deploy: runs-on: self-hosted # runner inside VPC steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/cicd-deploy-role aws-region: us-east-1 - name: Login to ECR run: | aws ecr get-login-password | docker login \ --username AWS \ --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com - name: Deploy to EKS run: | aws eks update-kubeconfig --name fintech-prod helm upgrade --install my-app ./helm/my-app \ --set image.tag=${{ github.sha }}
# Terraform
resource "aws_db_instance" "postgres" { identifier = "fintech-postgres" engine = "postgres" engine_version = "16.1" instance_class = "db.r6g.large" multi_az = true db_subnet_group_name = aws_db_subnet_group.private.name vpc_security_group_ids = [aws_security_group.rds.id] publicly_accessible = false storage_encrypted = true # Enable pgvector via parameter group parameter_group_name = aws_db_parameter_group.postgres_pgvector.name
}
# Terraform
resource "aws_db_instance" "postgres" { identifier = "fintech-postgres" engine = "postgres" engine_version = "16.1" instance_class = "db.r6g.large" multi_az = true db_subnet_group_name = aws_db_subnet_group.private.name vpc_security_group_ids = [aws_security_group.rds.id] publicly_accessible = false storage_encrypted = true # Enable pgvector via parameter group parameter_group_name = aws_db_parameter_group.postgres_pgvector.name
}
# Terraform
resource "aws_db_instance" "postgres" { identifier = "fintech-postgres" engine = "postgres" engine_version = "16.1" instance_class = "db.r6g.large" multi_az = true db_subnet_group_name = aws_db_subnet_group.private.name vpc_security_group_ids = [aws_security_group.rds.id] publicly_accessible = false storage_encrypted = true # Enable pgvector via parameter group parameter_group_name = aws_db_parameter_group.postgres_pgvector.name
}
CREATE EXTENSION IF NOT EXISTS vector;
CREATE INDEX ON embeddings USING ivfflat (embedding vector_cosine_ops);
CREATE EXTENSION IF NOT EXISTS vector;
CREATE INDEX ON embeddings USING ivfflat (embedding vector_cosine_ops);
CREATE EXTENSION IF NOT EXISTS vector;
CREATE INDEX ON embeddings USING ivfflat (embedding vector_cosine_ops);
# ExternalSecret — pulls from Secrets Manager into a Kubernetes Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: name: db-credentials
spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: db-credentials creationPolicy: Owner data: - secretKey: password remoteRef: key: fintech/prod/db property: password
# ExternalSecret — pulls from Secrets Manager into a Kubernetes Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: name: db-credentials
spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: db-credentials creationPolicy: Owner data: - secretKey: password remoteRef: key: fintech/prod/db property: password
# ExternalSecret — pulls from Secrets Manager into a Kubernetes Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: name: db-credentials
spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: db-credentials creationPolicy: Owner data: - secretKey: password remoteRef: key: fintech/prod/db property: password
# Bedrock VPC endpoint
aws ec2 create-vpc-endpoint \ --service-name com.amazonaws.us-east-1.bedrock-runtime \ --vpc-endpoint-type Interface \ --vpc-id vpc-xxx \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints
# Bedrock VPC endpoint
aws ec2 create-vpc-endpoint \ --service-name com.amazonaws.us-east-1.bedrock-runtime \ --vpc-endpoint-type Interface \ --vpc-id vpc-xxx \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints
# Bedrock VPC endpoint
aws ec2 create-vpc-endpoint \ --service-name com.amazonaws.us-east-1.bedrock-runtime \ --vpc-endpoint-type Interface \ --vpc-id vpc-xxx \ --subnet-ids subnet-xxx subnet-yyy \ --security-group-ids sg-endpoints - Air-gapped EKS requires VPC interface and gateway endpoints for every AWS service your workloads touch — there's no fallback to the public internet
- Your CI/CD pipeline must be redesigned from scratch: images push to private ECR via VPC endpoint, and deployment runs through Systems Manager or a bastion inside the VPC
- Kubernetes External Secrets Operator + AWS Secrets Manager is the cleanest pattern for pod-level secret injection without exposing credentials in manifests
- Every data store (RDS, DynamoDB, ElastiCache) must live in private subnets, accessed via security group rules — no public endpoints - Gateway endpoints — for S3 and DynamoDB only; free, added as route table entries
- Interface endpoints — for all other AWS services via AWS PrivateLink; billed per hour per AZ - Developer access role — scoped to read operations, no production deploy permissions
- CI/CD role — ECR push, EKS kubectl apply, Secrets Manager read
- Node instance role — ECR pull, CloudWatch logging, S3 read for application buckets - Core Rule Set (CRS) — protects against OWASP Top 10
- Known Bad Inputs — blocks common injection payloads