Everything for AWS in one place — pick a section below. 36 reviewed items across 4 content types, plus a hands-on tutorial.
Quick Answer
You design for AZ failure by spreading stateless capacity, data replicas, routing, and dependencies across at least two or three Availability Zones, then testing that each tier can continue when one zone is impaired. The hard part is removing hidden single-zone dependencies such as NAT gateways, subnets, load balancer targets, caches, and database writers.
Detailed Answer
Think of a hospital system with several buildings on the same campus. If one building loses power, patients should not lose all care because the pharmacy, oxygen supply, and emergency desk were only in that building. AWS Availability Zones are similar failure compartments inside a Region. Multi-AZ design is not just placing servers in two zones; every dependency the service needs must survive the loss of one zone.
AWS reliability guidance emphasizes strong foundations, resilient architecture, change management, and failure recovery. For a web service, this usually means an Application Load Balancer spanning subnets in multiple AZs, Auto Scaling groups balanced across those subnets, independent NAT gateways per AZ for private egress, and data stores configured for Multi-AZ durability or replication. Stateless services are easiest because replacement capacity can start elsewhere. Stateful services need replication, failover semantics, and recovery objectives.
The internal request path matters. A client resolves DNS, reaches a regional load balancer, the load balancer chooses healthy targets, app instances call databases, caches, queues, and external services, and responses return through the same dependency chain. If the app instances are balanced but all private traffic exits through one NAT gateway in the impaired AZ, healthy instances in other zones can still fail. If a cache cluster has all primary shards in one zone, the app may be technically running but unusable.
In production, engineers watch per-AZ target health, load balancer zonal metrics, database failover events, queue age, retry rates, and dependency saturation. They also run game days that intentionally remove an AZ from service, disable a subnet route, or shift traffic away from a zone. Good systems use timeouts, retries with jitter, idempotent writes, queue buffering, and autoscaling headroom so the surviving zones can absorb load without cascading.
The non-obvious gotcha is that multi-AZ can still fail as one unit when deployments, credentials, or shared control-plane assumptions are regional. A bad launch template, expired secret, or globally shared config can break all zones at once. Senior engineers separate failure domains, but they also test rollback, configuration isolation, and dependency degradation. Multi-AZ is a reliability foundation, not a substitute for disciplined operations.
Code Example
aws elbv2 describe-target-health --target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/payments-api/abc123 # Checks whether targets are healthy in every enabled Availability Zone. aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names payments-api-prod # Verifies the Auto Scaling group spans multiple subnets and zones. aws ec2 describe-nat-gateways --filter Name=tag:Service,Values=payments-api # Confirms private subnets are not all depending on one single-zone NAT gateway. aws rds describe-db-instances --db-instance-identifier payments-prod # Confirms whether the production database is configured for Multi-AZ failover. aws route53 get-health-check-status --health-check-id abcd1234 # Reviews external health check status before failover automation changes routing.
Interview Tip
A junior engineer typically answers that you run instances in multiple Availability Zones, but for a senior/architect role, the interviewer is actually looking for dependency analysis. Mention load balancers, autoscaling, NAT gateways, databases, queues, caches, DNS, retries, and capacity headroom. Then explain how you prove the design with game days and zonal metrics. A strong answer also admits that Multi-AZ does not protect against bad deployments, bad config, or regional service dependencies unless operational controls are designed too.
◈ Architecture Diagram
┌──────────┐
│ Client │
└────┬─────┘
↓
┌──────────┐
│ ALB │
└─┬─────┬──┘
↓ ↓
┌────┐ ┌────┐
│ AZA│ │ AZB│
└─┬──┘ └─┬──┘
↓ ↓
┌──────────┐
│ Multi AZ │
└──────────┘💬 Comments
Quick Answer
Start with a monolithic serverless stack (API Gateway, Lambda, DynamoDB) for the first 100-10K users, then progressively decompose into microservices behind an ALB with ECS Fargate, introduce ElastiCache and read replicas at 100K users, and finally adopt a multi-region active-active architecture with Aurora Global Database, CloudFront, and reserved/Savings Plans pricing at 1M+ users.
Detailed Answer
Designing for startup scale is fundamentally about avoiding premature optimization while building in the right inflection points. Think of it like building a restaurant: you start with a food truck (serverless), move to a small storefront (single-region containers), then franchise (multi-region). At each stage, you only pay for what you need.
Phase 1 (0-10K users): Deploy everything serverless. API Gateway handles HTTP routing, Lambda runs your business logic, and DynamoDB provides single-digit millisecond reads. Your monthly bill stays under $50. The key insight is that Lambda's per-request pricing means zero cost at zero traffic. Use S3 for static assets behind CloudFront. Deploy with SAM or CDK for reproducibility. At this stage, a single DynamoDB table with a well-designed partition key (e.g., USER#12345) handles all access patterns through single-table design.
Phase 2 (10K-100K users): Lambda concurrency limits and cold starts begin hurting user experience. Migrate compute-heavy paths to ECS Fargate behind an Application Load Balancer. Keep event-driven workloads (image processing, notifications) on Lambda. Introduce ElastiCache Redis for session management and hot-path caching, reducing DynamoDB read costs by 60-70%. Add an SQS queue between your API tier and async processors to absorb traffic spikes. Your bill is now $500-2000/month, but you have headroom.
Phase 3 (100K-1M users): Migrate from DynamoDB to Aurora PostgreSQL with read replicas for complex query patterns. DynamoDB excels at key-value lookups but becomes expensive for analytics queries. Aurora Serverless v2 scales capacity automatically between 0.5 and 128 ACUs. Implement write-through caching: writes go to Aurora, which triggers a Lambda via DynamoDB Streams-style change capture to update ElastiCache. Deploy a NAT Gateway in each AZ (not one shared, which creates a single point of failure and cross-AZ data transfer charges).
Phase 4 (1M-10M users): Move to multi-region active-active with Aurora Global Database (under 1 second replication lag). Use Route 53 latency-based routing to direct users to the nearest region. Adopt CloudFront with Lambda@Edge for personalization at the edge. Purchase Compute Savings Plans (not EC2 Instance Savings Plans) because they cover Lambda, Fargate, and EC2, giving you flexibility as your compute mix evolves. Implement S3 Intelligent-Tiering for your data lake since access patterns are unpredictable at this scale. Use AWS Cost Explorer daily with anomaly detection alerts via SNS.
Production gotchas: Always set billing alarms at each phase boundary. NAT Gateway data processing charges are the number one surprise cost, often exceeding the compute bill. Use VPC endpoints for S3 and DynamoDB to eliminate NAT Gateway charges for AWS service traffic. Tag every resource with cost-allocation tags (team, environment, service) from day one. You cannot retroactively tag resources for cost attribution.
Code Example
# Phase 1: Serverless foundation - SAM template for startup MVP
# Save as template.yaml and deploy with 'sam deploy --guided'
# Define the CloudFormation template version
AWSTemplateFormatVersion: '2010-09-09'
# Use the SAM transform for serverless resources
Transform: AWS::Serverless-2016-10-31
# Human-readable description of the stack purpose
Description: Startup MVP - Cost-optimized serverless backend for user-facing API
# Global configuration applied to all Lambda functions in the stack
Globals:
Function:
# 256MB balances cold start speed vs cost for Node.js APIs
MemorySize: 256
# 10-second timeout prevents runaway Lambda costs
Timeout: 10
# ARM64 (Graviton2) is 20% cheaper and 15% faster than x86
Architectures:
- arm64
# Environment variables shared across all functions
Environment:
Variables:
# Reference the DynamoDB table created below
TABLE_NAME: !Ref StartupUsersTable
# Set to 'production' for error reporting integration
STAGE: production
Resources:
# API Gateway - handles routing, throttling, and CORS for the frontend
StartupApi:
Type: AWS::Serverless::Api
Properties:
# Stage name appears in the URL: /prod/users
StageName: prod
# Throttle to 1000 RPS to prevent DDoS cost spikes
MethodSettings:
- HttpMethod: '*'
ResourcePath: '/*'
ThrottlingRateLimit: 1000
ThrottlingBurstLimit: 500
# Lambda function - core business logic for user management
UserServiceFunction:
Type: AWS::Serverless::Function
Properties:
# Path to the handler code in the project directory
CodeUri: src/handlers/users/
# Entry point: index.js exports a 'handler' function
Handler: index.handler
# Node.js 20.x has the fastest cold starts for API workloads
Runtime: nodejs20.x
# IAM policies granting DynamoDB access to this function
Policies:
# Allow read/write to the users table only (least privilege)
- DynamoDBCrudPolicy:
TableName: !Ref StartupUsersTable
# API Gateway event triggers for this function
Events:
# GET /users/{userId} - retrieve a single user profile
GetUser:
Type: Api
Properties:
RestApiId: !Ref StartupApi
Path: /users/{userId}
Method: GET
# POST /users - create a new user account
CreateUser:
Type: Api
Properties:
RestApiId: !Ref StartupApi
Path: /users
Method: POST
# DynamoDB table - single-table design for all entities
StartupUsersTable:
Type: AWS::DynamoDB::Table
Properties:
# Descriptive table name with environment prefix
TableName: prod-startup-users
# On-demand billing: pay per request, $0 at zero traffic
BillingMode: PAY_PER_REQUEST
# Partition key uses entity prefix pattern: USER#12345, ORDER#67890
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: GSI1PK
AttributeType: S
- AttributeName: GSI1SK
AttributeType: S
# Composite primary key enables single-table design queries
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
# GSI for access patterns like 'all orders for a date range'
GlobalSecondaryIndexes:
- IndexName: GSI1
KeySchema:
- AttributeName: GSI1PK
KeyType: HASH
- AttributeName: GSI1SK
KeyType: RANGE
Projection:
ProjectionType: ALL
# Enable point-in-time recovery - costs pennies, saves careers
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
# CloudFront distribution - serves static frontend assets globally
FrontendCDN:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
# Enable the distribution immediately after creation
Enabled: true
# Use PriceClass_100 (US/EU only) to minimize CDN costs early on
PriceClass: PriceClass_100
Origins:
- Id: S3FrontendOrigin
# Reference the S3 bucket hosting React/Vue build output
DomainName: !GetAtt FrontendBucket.RegionalDomainName
S3OriginConfig:
OriginAccessIdentity: ''
DefaultCacheBehavior:
TargetOriginId: S3FrontendOrigin
ViewerProtocolPolicy: redirect-to-https
# Cache static assets for 24 hours to reduce S3 GET costs
DefaultTTL: 86400
ForwardedValues:
QueryString: false
# S3 bucket - hosts the compiled frontend SPA files
FrontendBucket:
Type: AWS::S3::Bucket
Properties:
# Globally unique bucket name with account-specific prefix
BucketName: prod-startup-frontend-assets-123456789012
# Block all public access; CloudFront uses OAI to read objects
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: trueInterview Tip
A junior engineer typically jumps straight to describing a complex microservices architecture with Kubernetes, multiple databases, and multi-region failover, even when the question starts at 100 users. This signals a lack of practical experience with real startup constraints: limited budget, small team, and the need to ship fast. The strongest answers walk through each scaling phase with specific inflection points (e.g., 'at 50K concurrent connections, Lambda concurrency limits force a migration to containers'). Mention actual dollar amounts at each phase to show cost awareness. Interviewers want to see that you understand the trade-offs: serverless is cheap at low scale but expensive at high scale due to per-request pricing, while containers have a fixed baseline cost but better unit economics at scale. Always mention Savings Plans as the primary cost lever at scale, and explain why Compute Savings Plans are more flexible than EC2 Instance Savings Plans for evolving architectures.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────┐ │ Phase 1: 0-10K Users (Serverless) │ │ Monthly Cost: ~$50 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌─────────────┐ │ │ │ Route 53 │───→│ CloudFront │───→│ S3 Static │ │ Cognito │ │ │ │ DNS │ │ CDN │ │ Frontend │ │ User Pool │ │ │ └──────────┘ └──────────────┘ └────────────┘ └──────┬──────┘ │ │ │ │ │ │ ↓ ↓ │ │ ┌──────────────┐ ┌───────────────┐ ┌──────────────────────────┐ │ │ │ API Gateway │───→│ Lambda (ARM64)│───→│ DynamoDB (On-Demand) │ │ │ │ REST API │ │ Node.js 20.x │ │ Single-Table Design │ │ │ │ 1000 RPS cap │ │ 256MB Memory │ │ PK: USER#id / ORDER#id │ │ │ └──────────────┘ └───────────────┘ └──────────────────────────┘ │ │ │ ├─────────────────────────────────────────────────────────────────────────┤ │ Phase 2: 10K-100K Users (Hybrid) │ │ Monthly Cost: ~$500-2000 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌───────────────────┐ ┌─────────────────────┐ │ │ │ ALB │───→│ ECS Fargate │───→│ ElastiCache Redis │ │ │ │ Path-based │ │ 2 tasks (ARM64) │ │ cache.t4g.medium │ │ │ │ routing │ │ 0.5 vCPU / 1GB │ │ Session + hot data │ │ │ └──────────────┘ └────────┬──────────┘ └─────────────────────┘ │ │ │ │ │ ↓ │ │ ┌──────────────┐ ┌───────────────────┐ ┌─────────────────────┐ │ │ │ SQS Queue │───→│ Lambda Workers │ │ DynamoDB (still) │ │ │ │ Async tasks │ │ Image processing │ │ With DAX caching │ │ │ └──────────────┘ └───────────────────┘ └─────────────────────┘ │ │ │ ├─────────────────────────────────────────────────────────────────────────┤ │ Phase 3-4: 100K-10M Users (Multi-Region) │ │ Monthly Cost: $5K-50K+ │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Route 53 Latency-Based Routing │ │ │ └────────────────────────┬──────────────────┬───────────────────────┘ │ │ │ │ │ │ ┌────────────↓────────┐ ┌──────↓──────────────┐ │ │ │ us-east-1 Region │ │ eu-west-1 Region │ │ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │ │ │ ECS Fargate │ │ │ │ ECS Fargate │ │ │ │ │ │ Auto Scaling │ │ │ │ Auto Scaling │ │ │ │ │ └──────┬────────┘ │ │ └──────┬────────┘ │ │ │ │ ↓ │ │ ↓ │ │ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │ │ │ Aurora Writer │←─┼──┼─→│ Aurora Reader │ │ │ │ │ │ Global DB │ │ │ │ Global DB │ │ │ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │ └─────────────────────┘ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Zero-trust in AWS means eliminating implicit trust at every layer: use VPC Lattice or PrivateLink for service-to-service communication, enforce mutual TLS with ACM Private CA, implement microsegmentation with security groups referencing other security groups, use IAM Roles Anywhere for on-premises workloads, deploy AWS Verified Access for application-level access without VPNs, and continuously verify identity and device posture before granting access.
Detailed Answer
Zero-trust networking replaces the traditional castle-and-moat model with a 'never trust, always verify' approach. Think of the old model like an office building: once you badge in at the front door, you can walk into any room. Zero-trust is like a hospital where every room has its own lock, every person wears an ID badge that's checked at every door, and the security system logs every entry.
The foundation starts with identity. Every workload, service, and user must have a cryptographically verifiable identity. In AWS, this means IAM roles for compute resources (never long-lived access keys), OIDC federation for external services, and IAM Identity Center (successor to AWS SSO) for human access. Critical production accounts should enforce hardware MFA tokens, not virtual MFA.
Network microsegmentation is the next layer. Traditional VPC security uses NACLs and security groups at the subnet and instance level. Zero-trust goes further: security groups should reference other security groups instead of CIDR blocks. For example, your payments-api security group allows inbound on port 8443 only from the checkout-service security group. This means if an attacker compromises a machine in the same subnet, they cannot reach the payments API because their security group is not authorized. VPC Lattice, launched as a service mesh alternative, handles service-to-service authentication and authorization at the network layer without sidecars.
Encryption in transit is non-negotiable. Deploy ACM Private CA to issue short-lived certificates (24-hour validity) to every service. Mutual TLS (mTLS) ensures both client and server verify each other's identity. AWS App Mesh or Envoy sidecars terminate and originate mTLS connections, so application code does not need to handle certificate management. For east-west traffic between VPCs, use PrivateLink endpoints instead of VPC peering because PrivateLink restricts access to specific services rather than exposing entire VPC CIDR ranges.
AWS Verified Access replaces traditional VPNs for application access. Instead of granting network-level access to a VPC (where a compromised laptop can scan the entire network), Verified Access evaluates trust policies based on user identity (from IAM Identity Center or third-party IdP), device posture (from CrowdStrike, Jamf, or AWS device trust), and application-specific conditions. Each application gets its own Verified Access endpoint, so compromising access to one application does not grant access to others.
Continuous monitoring completes the zero-trust model. VPC Flow Logs feed into a SIEM (Security Lake or third-party) for anomaly detection. GuardDuty monitors for credential exfiltration, cryptocurrency mining, and unusual API patterns. CloudTrail logs every API call across all accounts. AWS Config rules verify that security groups do not have 0.0.0.0/0 ingress rules and that all ALBs enforce HTTPS.
Production gotchas: mTLS certificate rotation must be automated; manual rotation causes outages. VPC Lattice has a 500 services-per-service-network limit that bites at scale. Security group rules referencing other security groups do not work across VPC peering connections in different regions; use PrivateLink instead. Verified Access adds 5-15ms latency per request, which matters for latency-sensitive APIs. Always deploy in 'monitor only' mode first to identify legitimate traffic patterns before enforcing deny rules.
Code Example
# Zero-trust network configuration for a payments processing service
# Implements microsegmentation, mTLS, and Verified Access
# Step 1: Create security groups that reference each other (microsegmentation)
# The payments API only accepts traffic from the checkout service, not by IP range
aws ec2 create-security-group \
--group-name payments-api-sg \
--description "Payments API - accepts traffic only from checkout service SG" \
--vpc-id vpc-0a1b2c3d4e5f67890 \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Service,Value=payments-api},{Key=Environment,Value=production},{Key=ZeroTrust,Value=microsegmented}]'
# Store the security group ID for use in subsequent commands
PAYMENTS_SG_ID=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=payments-api-sg" \
--query 'SecurityGroups[0].GroupId' \
--output text)
# Allow inbound HTTPS only from the checkout service security group (not a CIDR block)
# This is the core of microsegmentation: identity-based network access
aws ec2 authorize-security-group-ingress \
--group-id $PAYMENTS_SG_ID \
--protocol tcp \
--port 8443 \
--source-group sg-0checkout9service8id
# Deny all other inbound traffic by default (security groups are deny-all by default)
# No additional rule needed; the absence of a rule is the deny
# Step 2: Issue short-lived mTLS certificates from ACM Private CA
# Create a subordinate CA specifically for service mesh certificates
aws acm-pca create-certificate-authority \
--certificate-authority-configuration '{"KeyAlgorithm":"EC_prime256v1","SigningAlgorithm":"SHA256WITHECDSA","Subject":{"Organization":"FinTech Corp","OrganizationalUnit":"Platform Engineering","CommonName":"prod-service-mesh-ca"}}' \
--certificate-authority-type SUBORDINATE \
--tags Key=Purpose,Value=zero-trust-mtls Key=Team,Value=platform-security
# Issue a 24-hour certificate for the payments service (short-lived reduces blast radius)
# If compromised, the certificate expires before an attacker can use it broadly
aws acm-pca issue-certificate \
--certificate-authority-arn arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
--csr fileb://payments-api-csr.pem \
--signing-algorithm SHA256WITHECDSA \
--validity Value=24,Type=HOURS \
--template-arn arn:aws:acm-pca:::template/EndEntityCertificate/V1
# Step 3: Configure VPC Lattice service network for authenticated service-to-service calls
# VPC Lattice replaces traditional service mesh sidecars with AWS-managed auth
aws vpc-lattice create-service-network \
--name prod-payments-network \
--auth-type AWS_IAM \
--tags Key=ZeroTrust,Value=enabled Key=DataClassification,Value=pci-scope
# Create a Lattice service for the payments API
aws vpc-lattice create-service \
--name payments-api-service \
--auth-type AWS_IAM \
--tags Key=Service,Value=payments-api
# Attach an auth policy requiring IAM Sig V4 signing from specific roles only
# Only the checkout-service role in account 123456789012 can call this service
aws vpc-lattice put-auth-policy \
--resource-identifier svc-0a1b2c3d4e5f67890 \
--policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:role/prod-checkout-service-role"},"Action":"vpc-lattice-svcs:Invoke","Resource":"arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-0a1b2c3d4e5f67890/*","Condition":{"StringEquals":{"vpc-lattice-svcs:ServiceNetworkArn":"arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-0a1b2c3d4e5f67890"}}}]}'
# Step 4: Deploy Verified Access for human access to internal tools (replaces VPN)
# Create a trust provider linked to the corporate identity provider
aws ec2 create-verified-access-trust-provider \
--trust-provider-type user \
--user-trust-provider-type iam-identity-center \
--policy-reference-name "CorpIdentity" \
--description "Corporate IAM Identity Center trust for zero-trust app access" \
--tags Key=Purpose,Value=zero-trust-human-access
# Step 5: Enable VPC Flow Logs for continuous monitoring and anomaly detection
# Send to CloudWatch Logs for real-time GuardDuty analysis
aws ec2 create-flow-log \
--resource-type VPC \
--resource-ids vpc-0a1b2c3d4e5f67890 \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /vpc/prod-payments-vpc/flow-logs \
--deliver-logs-permission-arn arn:aws:iam::123456789012:role/prod-vpc-flow-logs-role \
--max-aggregation-interval 60 \
--tag-specifications 'ResourceType=vpc-flow-log,Tags=[{Key=ZeroTrust,Value=monitoring},{Key=Retention,Value=90-days}]'Interview Tip
A junior engineer typically describes zero-trust as 'just using private subnets and security groups,' which is actually traditional perimeter security, not zero-trust. The critical distinction is that zero-trust assumes the network is already compromised and verifies identity at every interaction. Strong answers reference specific AWS services: VPC Lattice for service-to-service auth, Verified Access for replacing VPNs, ACM Private CA for mTLS certificates, and Security Lake for centralized log analysis. Mention the NIST SP 800-207 zero-trust architecture framework to show you understand the theoretical foundation. Interviewers are especially impressed when you discuss the operational overhead: certificate rotation automation, the risk of locking yourself out during mTLS rollout, and the importance of deploying in audit mode before enforcing deny policies.
◈ Architecture Diagram
┌────────────────────────────────────────────────────────────────────────────┐ │ Zero-Trust Architecture - AWS │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Identity Layer (Always Verify) │ │ │ │ ┌──────────────┐ ┌───────────────┐ ┌──────────────────────────┐ │ │ │ │ │ IAM Identity │ │ IAM Roles │ │ ACM Private CA │ │ │ │ │ │ Center (SSO) │ │ Everywhere │ │ 24-hour mTLS certs │ │ │ │ │ │ + Hardware │ │ (No long-lived│ │ EC_prime256v1 signing │ │ │ │ │ │ MFA │ │ access keys) │ │ Auto-rotation via Lambda │ │ │ │ │ └──────┬───────┘ └──────┬────────┘ └────────────┬─────────────┘ │ │ │ └─────────┼─────────────────┼─────────────────────────┼──────────────┘ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Network Layer (Microsegmentation) │ │ │ │ │ │ │ │ ┌──────────────────┐ ┌──────────────────────────────────────┐ │ │ │ │ │ Verified Access │ │ VPC Lattice Service Network │ │ │ │ │ │ (Replaces VPN) │ │ ┌──────────────┐ ┌────────────────┐ │ │ │ │ │ │ │ │ │ checkout-svc │→│ payments-api │ │ │ │ │ │ │ User Identity │ │ │ IAM Role Auth │ │ IAM Role Auth │ │ │ │ │ │ │ + Device Posture │ │ │ SG: sg-chkout │ │ SG: sg-pay-api │ │ │ │ │ │ │ + App Policy │ │ └──────────────┘ └────────────────┘ │ │ │ │ │ │ │ │ ↓ (mTLS + SigV4) │ │ │ │ │ │ ┌──────────────┐ │ │ ┌──────────────┐ ┌────────────────┐ │ │ │ │ │ │ │ Admin Portal │ │ │ │ order-svc │→│ inventory-svc │ │ │ │ │ │ │ │ Internal Tool│ │ │ │ IAM Role Auth │ │ IAM Role Auth │ │ │ │ │ │ │ └──────────────┘ │ │ └──────────────┘ └────────────────┘ │ │ │ │ │ └──────────────────┘ └──────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ ↓ ↓ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Monitoring Layer (Continuous) │ │ │ │ ┌──────────────┐ ┌────────────────┐ ┌────────────────────────┐ │ │ │ │ │ VPC Flow Logs│→ │ Security Lake │→ │ GuardDuty + Detective │ │ │ │ │ │ ALL traffic │ │ Centralized │ │ Anomaly detection │ │ │ │ │ │ 60s interval │ │ OCSF format │ │ Automated remediation │ │ │ │ │ └──────────────┘ └────────────────┘ └────────────────────────┘ │ │ │ │ ┌──────────────┐ ┌────────────────┐ ┌────────────────────────┐ │ │ │ │ │ CloudTrail │→ │ AWS Config │→ │ SNS Alerts to SecOps │ │ │ │ │ │ All API calls│ │ SG open check │ │ PagerDuty integration │ │ │ │ │ └──────────────┘ └────────────────┘ └────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Cross-account access uses two mechanisms: IAM role assumption (account B creates a role with a trust policy allowing account A's principal, and account A's principal has sts:AssumeRole permission) or resource-based policies (the resource in account B directly grants access to account A's principal). Role assumption requires two policies to align, while resource-based policies enable direct access without assuming a role, and the two approaches have different implications for permission boundaries, SCPs, and session policies.
Detailed Answer
Cross-account access is one of the most misunderstood topics in AWS because there are two fundamentally different mechanisms, and the permissions evaluation logic differs between them. Think of it like two buildings: with role assumption, you walk into building B, get a visitor badge (temporary credentials), and operate under building B's rules. With resource-based policies, building B sends a package to you in building A; you never enter building B, so your building A badge is what matters.
Mechanism 1: IAM Role Assumption (sts:AssumeRole). Account B (the trusting account, e.g., 987654321098) creates an IAM role with a trust policy that specifies which principals in account A (the trusted account, e.g., 123456789012) can assume it. Account A's principal must also have an IAM policy granting sts:AssumeRole on the role ARN in account B. Both policies must align; either one missing means access denied. When the principal in account A calls sts:AssumeRole, it receives temporary credentials (access key, secret key, session token) that operate with the permissions of the role in account B. Critically, the original permissions from account A are dropped entirely. The principal now operates as if it lives in account B, subject to account B's SCPs, permission boundaries, and session policies.
Mechanism 2: Resource-Based Policies. Some AWS resources (S3 buckets, SQS queues, SNS topics, Lambda functions, KMS keys) support resource-based policies that directly grant cross-account access. When account B's S3 bucket policy grants s3:GetObject to arn:aws:iam::123456789012:role/prod-analytics-etl-role, the principal in account A can access the bucket directly without assuming a role. The critical difference: the principal retains its own identity and permissions. Both the identity-based policy in account A and the resource-based policy in account B are evaluated, and if either grants access (in the absence of an explicit deny), the request succeeds. This is called the union of permissions for cross-account resource-based policies.
SCP interactions create the most confusion. When using role assumption, SCPs in account B apply to the assumed role session. If account B's SCP denies s3:DeleteObject, the assumed role cannot delete objects even if the role's IAM policy allows it. SCPs in account A do not apply because the principal is now operating in account B's context. For resource-based policies, SCPs in both accounts apply: account A's SCPs affect the principal making the request, and account B's SCPs affect the resource being accessed.
Permission boundaries interact differently too. If principal A has a permission boundary that does not include s3:GetObject, it cannot access account B's S3 bucket via resource-based policy (because its effective permissions are the intersection of IAM policy and permission boundary). However, if principal A assumes a role in account B, the permission boundary on principal A is irrelevant; only the permission boundary on the role in account B matters.
The confused deputy problem arises when a service in account B assumes a role on behalf of an external entity. Always use the ExternalId condition in trust policies for third-party access. The ExternalId acts like a shared secret that prevents an attacker from tricking your role into granting them access through a different AWS account they control.
Production gotchas: S3 bucket policies have a 20KB size limit, which you hit quickly when granting access to many individual accounts; use aws:PrincipalOrgID instead to grant access to all accounts in your AWS Organization. KMS key policies are unique: the key policy must explicitly grant access even for same-account principals (unlike S3 where the IAM policy alone is sufficient). When using cross-account role assumption in CI/CD pipelines, implement session tags to pass metadata (environment, pipeline-id, commit-sha) that can be used in condition keys for fine-grained access control.
Code Example
# Cross-account access: Account A (123456789012) analytics team needs
# read access to Account B (987654321098) production data lake
# ============================================================
# STEP 1: In Account B (987654321098) - Create the cross-account role
# ============================================================
# Create the trust policy JSON that specifies who can assume this role
# Only the specific ETL role in Account A can assume it, not the entire account
cat > /tmp/prod-data-lake-trust-policy.json << 'TRUSTPOLICY'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/prod-analytics-etl-role"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "fintech-analytics-2024-xK9mP2",
"aws:PrincipalTag/Department": "analytics"
},
"IpAddress": {
"aws:SourceIp": "10.0.0.0/8"
}
}
}
]
}
TRUSTPOLICY
# Create the IAM role in Account B using the trust policy above
# This role exists in the production account and has read-only data lake access
aws iam create-role \
--role-name prod-cross-account-analytics-reader \
--assume-role-policy-document file:///tmp/prod-data-lake-trust-policy.json \
--description "Read-only access to prod data lake for analytics team in account 123456789012" \
--max-session-duration 3600 \
--tags Key=CrossAccount,Value=true Key=SourceAccount,Value=123456789012 Key=DataClassification,Value=confidential
# Create the permissions policy for what the role can actually do
# Grants read-only access to specific S3 prefixes and Athena query execution
cat > /tmp/prod-data-lake-permissions.json << 'PERMISSIONS'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadProductionDataLakeBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::prod-analytics-data-lake-987654321098",
"arn:aws:s3:::prod-analytics-data-lake-987654321098/cleaned/*",
"arn:aws:s3:::prod-analytics-data-lake-987654321098/aggregated/*"
],
"Condition": {
"StringEquals": {
"s3:ExistingObjectTag/Classification": ["public", "internal"]
}
}
},
{
"Sid": "RunAthenaQueriesOnDataLake",
"Effect": "Allow",
"Action": [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults"
],
"Resource": "arn:aws:athena:us-east-1:987654321098:workgroup/analytics-readonly-workgroup"
},
{
"Sid": "WriteAthenaQueryResultsToDesignatedBucket",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::prod-athena-query-results-987654321098/cross-account-analytics/*"
},
{
"Sid": "DecryptDataLakeObjectsWithKMS",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:us-east-1:987654321098:key/mrk-1234abcd5678efgh9012ijkl"
}
]
}
PERMISSIONS
# Attach the permissions policy to the cross-account role
aws iam put-role-policy \
--role-name prod-cross-account-analytics-reader \
--policy-name DataLakeReadOnlyAccess \
--policy-document file:///tmp/prod-data-lake-permissions.json
# ============================================================
# STEP 2: In Account A (123456789012) - Grant permission to assume the role
# ============================================================
# Create a policy that allows the ETL role to assume the cross-account role
# This is the "other half" - both accounts must agree for access to work
cat > /tmp/analytics-assume-role-policy.json << 'ASSUMEPOLICY'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeProductionDataLakeRole",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::987654321098:role/prod-cross-account-analytics-reader",
"Condition": {
"StringEquals": {
"sts:ExternalId": "fintech-analytics-2024-xK9mP2"
}
}
}
]
}
ASSUMEPOLICY
# Attach the assume-role policy to the analytics ETL role in Account A
aws iam put-role-policy \
--role-name prod-analytics-etl-role \
--policy-name CrossAccountDataLakeAccess \
--policy-document file:///tmp/analytics-assume-role-policy.json
# ============================================================
# STEP 3: Test the cross-account access by assuming the role
# ============================================================
# Assume the role and capture temporary credentials (valid for 1 hour)
# The ExternalId must match what's in the trust policy
CREDENTIALS=$(aws sts assume-role \
--role-arn arn:aws:iam::987654321098:role/prod-cross-account-analytics-reader \
--role-session-name analytics-etl-daily-run-20240115 \
--external-id fintech-analytics-2024-xK9mP2 \
--duration-seconds 3600 \
--query 'Credentials' \
--output json)
# Extract the temporary access key from the assume-role response
export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.AccessKeyId')
# Extract the temporary secret key from the assume-role response
export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')
# Extract the session token required for temporary credential authentication
export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.SessionToken')
# Verify the assumed identity shows the cross-account role, not the original principal
aws sts get-caller-identity
# Expected output: Account 987654321098, Role prod-cross-account-analytics-reader
# List objects in the production data lake (now accessible via cross-account role)
aws s3 ls s3://prod-analytics-data-lake-987654321098/cleaned/ --recursive --human-readableInterview Tip
A junior engineer typically explains cross-account access as simply 'create a role and assume it,' missing the critical nuance of how permission evaluation differs between role assumption and resource-based policies. The strongest answers explain the double-policy requirement for role assumption (trust policy AND identity policy), contrast it with the union behavior of resource-based policies, and discuss SCP implications. Always mention the confused deputy problem and ExternalId for third-party integrations. Mention that KMS key policies are unique because they require explicit key policy grants even for same-account access. If you can explain that permission boundaries on the calling principal are irrelevant after role assumption (only the boundary on the assumed role matters), you demonstrate genuine mastery of IAM evaluation logic.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────────┐ │ Cross-Account Access Patterns │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Pattern 1: IAM Role Assumption (sts:AssumeRole) │ │ ───────────────────────────────────────────────── │ │ │ │ Account A (123456789012) Account B (987654321098) │ │ Analytics Account Production Account │ │ ┌──────────────────────┐ ┌───────────────────────────┐ │ │ │ prod-analytics-etl │ │ prod-cross-account-reader │ │ │ │ IAM Role │ │ IAM Role │ │ │ │ │ │ │ │ │ │ Policy: │ STS │ Trust Policy: │ │ │ │ Allow sts:AssumeRole │────────→│ Allow 123456789012 │ │ │ │ on role in Acct B │ Assume │ with ExternalId match │ │ │ │ │ Role │ │ │ │ │ (Original perms │ │ Permissions Policy: │ │ │ │ are DROPPED) │ │ s3:GetObject on data lake │ │ │ └──────────────────────┘ │ athena:StartQuery │ │ │ │ kms:Decrypt │ │ │ Temporary Credentials └─────────┬─────────────────┘ │ │ ←─────────────────────────────────── │ │ │ (AccessKeyId + SecretKey │ │ │ + SessionToken, 1hr TTL) ↓ │ │ ┌───────────────────────────┐ │ │ │ prod-analytics-data-lake │ │ │ │ S3 Bucket │ │ │ │ (accessed with Acct B │ │ │ │ role permissions) │ │ │ └───────────────────────────┘ │ │ │ │ Pattern 2: Resource-Based Policy (Direct Access) │ │ ──────────────────────────────────────────────── │ │ │ │ Account A (123456789012) Account B (987654321098) │ │ ┌──────────────────────┐ ┌───────────────────────────┐ │ │ │ prod-analytics-etl │ │ prod-analytics-data-lake │ │ │ │ IAM Role │ │ S3 Bucket │ │ │ │ │ Direct │ │ │ │ │ Policy: │ Access │ Bucket Policy: │ │ │ │ Allow s3:GetObject │────────→│ Allow s3:GetObject │ │ │ │ on bucket in Acct B │ (No role│ Principal: 123456789012 │ │ │ │ │ assumed)│ :role/analytics-etl │ │ │ │ (Original perms │ │ │ │ │ │ are RETAINED) │ │ Evaluation: UNION of │ │ │ └──────────────────────┘ │ identity + resource policy│ │ │ └───────────────────────────┘ │ │ │ │ Permission Evaluation Summary: │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Role Assumption: Acct B SCP → Acct B Permission Boundary → │ │ │ │ Role Policy (Acct A perms irrelevant) │ │ │ │ │ │ │ │ Resource Policy: (Acct A SCP → Acct A IAM) UNION (Acct B policy) │ │ │ │ Both account SCPs apply │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Ingest raw data into an S3 landing zone, trigger AWS Glue crawlers to catalog schemas in the Glue Data Catalog, run Glue ETL jobs (PySpark or Python shell) to transform and partition data into Parquet/Iceberg format in a curated S3 zone, register tables in Athena, and query with Athena using partition pruning and columnar scan optimization. Use Glue workflows or Step Functions to orchestrate the pipeline end-to-end.
Detailed Answer
A well-designed data pipeline on AWS follows the medallion architecture pattern (bronze/silver/gold zones), with S3 as the storage layer, Glue as the ETL engine and metadata catalog, and Athena as the serverless query engine. Think of it like a water treatment plant: raw water (data) enters the intake (landing zone), goes through filtration stages (ETL transformations), and emerges as clean drinking water (analytics-ready tables).
The S3 layer is organized into three zones. The landing zone (s3://prod-analytics-data-lake-123456789012/landing/) receives raw data in its original format: CSV exports from RDS, JSON events from Kinesis Firehose, or Parquet files from third-party vendors. This zone has a lifecycle policy: move to Glacier Deep Archive after 90 days, delete after 365 days. The processed zone (s3://prod-analytics-data-lake-123456789012/processed/) contains transformed data in Apache Parquet format with Snappy compression, partitioned by date. Parquet is critical because Athena charges per byte scanned, and Parquet's columnar format means querying 5 columns of a 200-column table scans only 2.5% of the data. The curated zone (s3://prod-analytics-data-lake-123456789012/curated/) contains business-specific aggregations ready for dashboarding: daily revenue summaries, user cohort metrics, funnel conversion rates.
Glue Crawlers automatically infer schemas from files in S3 and register them in the Glue Data Catalog. However, in production you should avoid running crawlers on every file change. Instead, use the Glue API to manually register schemas (create_table) because crawlers can infer incorrect data types (e.g., treating ZIP codes as integers, stripping leading zeros). Crawlers are useful for initial discovery but should be replaced with explicit schema definitions for production tables.
Glue ETL jobs run Apache Spark under the hood but with AWS-managed infrastructure. For small datasets (under 1GB), use Python shell jobs (0.0625 DPU, costs roughly $0.44/hour) instead of Spark jobs (minimum 2 DPUs at $0.44/DPU/hour). For larger datasets, use Glue 4.0 with auto-scaling enabled, which adjusts DPU count based on actual workload. A critical optimization: enable job bookmarks, which track which data has already been processed. Without bookmarks, re-running a job reprocesses all historical data, inflating both cost and execution time.
Partitioning strategy determines Athena query performance. Partition by the most common query filter. For event data, partition by year/month/day (s3://bucket/events/year=2024/month=01/day=15/). For multi-tenant data, partition by tenant_id first, then date. Use Hive-style partitions (key=value in the path) so Athena automatically recognizes them. Each partition creates a separate metadata entry in the Glue Data Catalog, so avoid over-partitioning: 100,000+ partitions causes slow query planning. If you need hourly granularity, use Apache Iceberg hidden partitioning instead, which partitions data without polluting the S3 path structure.
Athena pricing is $5 per TB scanned, so query optimization is directly financial optimization. Always use columnar formats (Parquet or ORC), compress with Snappy or ZSTD, partition by the most filtered column, and use LIMIT clauses during development. Create Athena workgroups to enforce per-team query limits: the analytics team gets a 10GB per-query scan limit to prevent runaway queries that scan the entire data lake. Enable Athena query result reuse for repeated queries to avoid re-scanning data.
Orchestration ties everything together. Use Glue Workflows for simple linear pipelines (crawl, transform, validate) or AWS Step Functions for complex DAGs with conditional branching, error handling, and human approval steps. EventBridge rules trigger the pipeline when new files land in the S3 landing zone via s3:ObjectCreated events.
Production gotchas: Glue crawlers do not detect schema evolution (added/removed columns) well; use schema registry with explicit versioning. Athena has a 30-query concurrency limit per account per region (can be increased). S3 strongly consistent reads (since December 2020) eliminated eventual consistency bugs in pipelines, but older documentation still warns about them. Never use CSV for analytical workloads; the format lacks schema enforcement and columnar scan benefits.
Code Example
# End-to-end data pipeline: S3 landing zone → Glue ETL → Athena query
# This pipeline processes daily e-commerce transaction logs
# Step 1: Create the S3 bucket structure for the three-zone data lake
# Landing zone receives raw data, processed zone has Parquet, curated zone has aggregations
aws s3api create-bucket \
--bucket prod-ecommerce-data-lake-123456789012 \
--region us-east-1 \
--object-lock-enabled-for-bucket
# Create the three zone prefixes that organize the data lake tiers
aws s3api put-object --bucket prod-ecommerce-data-lake-123456789012 --key landing/
aws s3api put-object --bucket prod-ecommerce-data-lake-123456789012 --key processed/
aws s3api put-object --bucket prod-ecommerce-data-lake-123456789012 --key curated/
# Apply lifecycle rules to age out raw data and reduce storage costs
# Raw data moves to Glacier after 90 days, deletes after 1 year
aws s3api put-bucket-lifecycle-configuration \
--bucket prod-ecommerce-data-lake-123456789012 \
--lifecycle-configuration '{
"Rules": [
{
"ID": "ArchiveRawDataToGlacier",
"Filter": {"Prefix": "landing/"},
"Status": "Enabled",
"Transitions": [{"Days": 90, "StorageClass": "GLACIER_IR"}],
"Expiration": {"Days": 365}
},
{
"ID": "TransitionProcessedToIA",
"Filter": {"Prefix": "processed/"},
"Status": "Enabled",
"Transitions": [{"Days": 180, "StorageClass": "STANDARD_IA"}]
}
]
}'
# Step 2: Create the Glue Data Catalog database for this pipeline
# This database holds all table definitions for the e-commerce data lake
aws glue create-database \
--database-input '{
"Name": "prod_ecommerce_analytics",
"Description": "E-commerce transaction analytics - Parquet tables partitioned by date",
"LocationUri": "s3://prod-ecommerce-data-lake-123456789012/processed/"
}'
# Step 3: Register the processed transactions table explicitly (not via crawler)
# Explicit schema definition prevents data type inference errors
aws glue create-table \
--database-name prod_ecommerce_analytics \
--table-input '{
"Name": "transactions",
"Description": "Cleaned e-commerce transactions partitioned by order date",
"StorageDescriptor": {
"Columns": [
{"Name": "transaction_id", "Type": "string", "Comment": "UUID primary key for each transaction"},
{"Name": "customer_id", "Type": "string", "Comment": "FK to customers table"},
{"Name": "product_sku", "Type": "string", "Comment": "Product SKU from inventory system"},
{"Name": "quantity", "Type": "int", "Comment": "Number of units purchased"},
{"Name": "unit_price_cents", "Type": "bigint", "Comment": "Price in cents to avoid float rounding"},
{"Name": "total_amount_cents", "Type": "bigint", "Comment": "Total = quantity * unit_price_cents"},
{"Name": "payment_method", "Type": "string", "Comment": "credit_card, debit, apple_pay, etc."},
{"Name": "store_region", "Type": "string", "Comment": "Geographic region: us-east, us-west, eu-central"},
{"Name": "event_timestamp", "Type": "timestamp", "Comment": "UTC timestamp of the transaction"}
],
"Location": "s3://prod-ecommerce-data-lake-123456789012/processed/transactions/",
"InputFormat": "org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat",
"OutputFormat": "org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat",
"SerdeInfo": {"SerializationLibrary": "org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe"}
},
"PartitionKeys": [
{"Name": "year", "Type": "string"},
{"Name": "month", "Type": "string"},
{"Name": "day", "Type": "string"}
],
"TableType": "EXTERNAL_TABLE",
"Parameters": {
"classification": "parquet",
"compressionType": "snappy",
"has_encrypted_data": "true"
}
}'
# Step 4: Create an Athena workgroup with query cost controls
# Limits per-query scan to 10GB to prevent expensive runaway queries
aws athena create-work-group \
--name ecommerce-analytics-team \
--configuration '{
"ResultConfiguration": {
"OutputLocation": "s3://prod-athena-results-123456789012/ecommerce-analytics/",
"EncryptionConfiguration": {"EncryptionOption": "SSE_KMS", "KmsKey": "arn:aws:kms:us-east-1:123456789012:key/mrk-analytics-query-key"}
},
"EnforceWorkGroupConfiguration": true,
"BytesScannedCutoffPerQuery": 10737418240,
"EngineVersion": {"SelectedEngineVersion": "Athena engine version 3"}
}' \
--description "Analytics team workgroup with 10GB scan limit per query" \
--tags Key=Team,Value=analytics Key=CostCenter,Value=CC-4521
# Step 5: Run an Athena query that leverages partition pruning for cost efficiency
# This query only scans January 2024 data, not the entire data lake
aws athena start-query-execution \
--query-string "SELECT store_region, payment_method, COUNT(*) as transaction_count, SUM(total_amount_cents)/100.0 as total_revenue_dollars FROM prod_ecommerce_analytics.transactions WHERE year='2024' AND month='01' GROUP BY store_region, payment_method ORDER BY total_revenue_dollars DESC LIMIT 20" \
--work-group ecommerce-analytics-team \
--query-execution-context '{"Database": "prod_ecommerce_analytics"}'Interview Tip
A junior engineer typically describes the pipeline as 'put files in S3, run a Glue crawler, query with Athena' without addressing the critical production concerns that differentiate a toy pipeline from a real one. The strongest answers cover: why explicit schema registration is better than crawlers (data type inference errors), why Parquet with Snappy compression saves 90%+ on Athena costs versus CSV, how partition pruning works (WHERE year='2024' eliminates scanning other years entirely), and the financial implications of Athena's per-TB-scanned pricing model. Mention Glue job bookmarks for incremental processing, the 30-query Athena concurrency limit, and why you should store prices in cents as integers rather than floats to avoid rounding errors. Discussing Apache Iceberg for ACID transactions and time-travel queries on S3 data lakes shows cutting-edge awareness.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────────┐ │ E-Commerce Data Pipeline Architecture │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Data Sources │ │ ┌────────────────┐ ┌─────────────────┐ ┌──────────────────────────┐ │ │ │ RDS PostgreSQL │ │ Kinesis Firehose│ │ Third-Party Vendor SFTP │ │ │ │ (Order DB │ │ (Clickstream │ │ (Supplier inventory │ │ │ │ nightly dump) │ │ events) │ │ daily CSV export) │ │ │ └───────┬────────┘ └───────┬─────────┘ └────────────┬─────────────┘ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ S3 Landing Zone (Bronze) │ │ │ │ s3://prod-ecommerce-data-lake-123456789012/landing/ │ │ │ │ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐ │ │ │ │ │ orders/ │ │ clickstream/ │ │ inventory/ │ │ │ │ │ │ 2024/01/15/ │ │ 2024/01/15/ │ │ 2024/01/15/ │ │ │ │ │ │ raw_orders.csv │ │ events.json.gz │ │ supplier_feed.csv │ │ │ │ │ └─────────────────┘ └──────────────────┘ └─────────────────────┘ │ │ │ │ Lifecycle: Glacier IR after 90 days → Delete after 365 days │ │ │ └──────────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ┌────────────↓────────────┐ │ │ │ EventBridge Rule │ │ │ │ s3:ObjectCreated:* │ │ │ │ Triggers Glue Workflow │ │ │ └────────────┬─────────────┘ │ │ │ │ │ ┌────────────↓────────────┐ │ │ │ Glue ETL Job (Spark) │ │ │ │ ┌──────────────────┐ │ │ │ │ │ 1. Read raw CSV │ │ │ │ │ │ 2. Validate schema│ │ │ │ │ │ 3. Deduplicate │ │ │ │ │ │ 4. Cast types │ │ │ │ │ │ 5. Write Parquet │ │ │ │ │ │ + Snappy │ │ │ │ │ │ 6. Update catalog│ │ │ │ │ └──────────────────┘ │ │ │ │ Job Bookmarks: ON │ │ │ │ Auto-scaling: 2-10 DPU │ │ │ └────────────┬─────────────┘ │ │ │ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ S3 Processed Zone (Silver) │ │ │ │ s3://prod-ecommerce-data-lake-123456789012/processed/ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ transactions/year=2024/month=01/day=15/ │ │ │ │ │ │ part-00000-abc123.snappy.parquet (128MB target size) │ │ │ │ │ │ part-00001-def456.snappy.parquet │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ Format: Parquet + Snappy │ Partitioned: year/month/day │ │ │ └──────────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────┼──────────────────┐ │ │ ↓ ↓ ↓ │ │ ┌───────────────────┐ ┌────────────────┐ ┌─────────────────────────┐ │ │ │ Glue Data Catalog │ │ Athena Engine │ │ S3 Curated Zone (Gold) │ │ │ │ ┌───────────────┐ │ │ v3 (Trino) │ │ Daily revenue summaries │ │ │ │ │ Database: │ │ │ │ │ Cohort conversion rates │ │ │ │ │ prod_ecomm │ │ │ Workgroup: │ │ Regional sales trends │ │ │ │ │ │ │ │ analytics-team│ │ │ │ │ │ │ Tables: │ │ │ Scan limit: │ │ Consumed by: │ │ │ │ │ - transactions│←┼→│ 10 GB/query │ │ - QuickSight dashboards │ │ │ │ │ - clickstream │ │ │ Cost: $5/TB │ │ - Redshift Spectrum │ │ │ │ │ - inventory │ │ │ │ │ - ML training (SageMaker│ │ │ │ └───────────────┘ │ └────────────────┘ └─────────────────────────┘ │ │ └───────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Serverless (Lambda + API Gateway) excels for event-driven, spiky workloads with sub-100ms startup requirements and per-request billing, while container-based (ECS Fargate or EKS) suits long-running processes, steady-state traffic, WebSocket connections, and workloads needing more than 10GB memory or 15-minute execution windows. The decision hinges on request duration, concurrency patterns, cold start tolerance, state management needs, and unit economics at your traffic volume.
Detailed Answer
Choosing between serverless and containers is not a binary decision; it is a spectrum, and most production systems use both. Think of serverless like a taxi: you pay per trip, there is no vehicle to maintain, but you cannot customize the car and you wait for one to arrive (cold start). Containers are like a company car: fixed monthly cost whether you drive it or not, but it is always ready and you can customize everything under the hood.
Serverless Architecture (Lambda + API Gateway + DynamoDB). API Gateway receives HTTP requests and routes them to Lambda functions. Each Lambda invocation is isolated, runs for up to 15 minutes, and scales to 1000 concurrent executions per region by default (soft limit, requestable to tens of thousands). Lambda charges per 1ms of execution time and per request ($0.20 per million requests, $0.0000166667 per GB-second). For a workload handling 10 million requests/month at 200ms average duration with 512MB memory, the Lambda cost is approximately $18.67/month. This is transformatively cheap for startups.
However, serverless has hard constraints. Cold starts add 100ms-3s depending on runtime (Java/C# are worst, Node.js/Python best, and Rust/Go with custom runtime are near-zero). Provisioned Concurrency eliminates cold starts but adds fixed cost, defeating the pay-per-request model. Lambda has a 10GB memory ceiling, 10GB ephemeral storage, and cannot maintain persistent connections (WebSockets require API Gateway WebSocket API with connection management in DynamoDB). Each Lambda invocation is stateless, so session state must live in ElastiCache or DynamoDB. Lambda functions behind API Gateway have a 29-second timeout (API Gateway limit), even though Lambda itself allows 15 minutes.
Container Architecture (ECS Fargate or EKS behind ALB). An Application Load Balancer distributes requests to Fargate tasks running your Docker containers. Containers maintain state in memory across requests, support WebSocket connections natively, and have no cold start once running. ECS Fargate charges per vCPU-hour ($0.04048) and per GB-hour ($0.004445). A service running 2 tasks with 1 vCPU and 2GB RAM 24/7 costs approximately $96.15/month. At low traffic, this is more expensive than serverless. At high traffic (millions of requests/day), the unit economics flip because you are paying for capacity, not per-request.
EKS adds Kubernetes orchestration: service mesh (Istio/Linkerd), horizontal pod autoscaling based on custom metrics, rolling deployments with canary analysis, and a vast ecosystem of operators. However, EKS adds $73/month for the control plane, plus worker node costs, and requires Kubernetes expertise. Use EKS when you need multi-cloud portability, have a platform team, or run 20+ microservices that benefit from the Kubernetes ecosystem.
Hybrid patterns are the real-world answer. Use Lambda for event-driven glue: processing S3 uploads, responding to DynamoDB Streams, handling SNS notifications, and running scheduled cron jobs. Use containers for your core API that handles synchronous HTTP traffic with predictable latency requirements. Use Lambda@Edge or CloudFront Functions for edge logic (A/B testing, geolocation routing, header manipulation). The API Gateway can route /api/* to an ALB (containers) and /webhooks/* to Lambda in the same application.
State management differs fundamentally. Containers can use in-memory caching (local LRU cache) for hot data, reducing ElastiCache calls. Lambda functions must externalize all state, which adds latency and cost for every cache lookup. For machine learning inference, containers allow loading large models into memory once and serving many requests, while Lambda must load the model on every cold start (or use Provisioned Concurrency with large memory).
Production gotchas: Lambda concurrent execution limits are per-region, not per-function, so one runaway function can starve all other functions in the account. Use reserved concurrency to isolate critical functions. ECS Fargate task startup time is 30-60 seconds for pulling images; use ECR with VPC endpoints and slim base images (Alpine, distroless) to minimize pull time. ALB health checks default to 30-second intervals; reduce to 10 seconds with a 2-unhealthy-threshold to speed up failed task replacement. Never run ECS tasks with the awsvpc network mode in a subnet without enough available IP addresses; each task consumes one ENI (and one IP).
Code Example
# Side-by-side deployment: Serverless API + Container API in the same VPC
# Demonstrates when to use each pattern in a single application
# ============================================================
# SERVERLESS: Event-driven webhook processor (bursty, unpredictable traffic)
# Handles Stripe payment webhooks - perfect for Lambda (short, spiky, stateless)
# ============================================================
# Create the Lambda function for processing Stripe payment webhooks
# ARM64 (Graviton2) reduces cost by 20% compared to x86_64
aws lambda create-function \
--function-name prod-stripe-webhook-processor \
--runtime nodejs20.x \
--architectures arm64 \
--handler index.handler \
--role arn:aws:iam::123456789012:role/prod-stripe-webhook-lambda-role \
--memory-size 512 \
--timeout 30 \
--environment 'Variables={STRIPE_WEBHOOK_SECRET=whsec_prod_abc123,ORDERS_TABLE=prod-ecommerce-orders,REGION=us-east-1}' \
--dead-letter-config TargetArn=arn:aws:sqs:us-east-1:123456789012:prod-stripe-webhook-dlq \
--tracing-config Mode=Active \
--zip-file fileb://stripe-webhook-processor.zip \
--tags Team=payments,Service=webhook-processor,Pattern=serverless
# Set reserved concurrency to prevent this function from consuming all Lambda capacity
# 100 reserved means other functions are guaranteed remaining regional concurrency
aws lambda put-function-concurrency \
--function-name prod-stripe-webhook-processor \
--reserved-concurrent-executions 100
# Create API Gateway HTTP API (v2) for the webhook endpoint
# HTTP API is 70% cheaper than REST API and has lower latency
aws apigatewayv2 create-api \
--name prod-stripe-webhook-api \
--protocol-type HTTP \
--description "Stripe webhook receiver - serverless, auto-scaling to handle payment spikes"
# Create the Lambda integration for the API Gateway
# Payload format 2.0 uses the simplified event structure
aws apigatewayv2 create-integration \
--api-id a1b2c3d4e5 \
--integration-type AWS_PROXY \
--integration-uri arn:aws:lambda:us-east-1:123456789012:function:prod-stripe-webhook-processor \
--payload-format-version 2.0
# Create the POST /webhooks/stripe route that triggers the Lambda function
aws apigatewayv2 create-route \
--api-id a1b2c3d4e5 \
--route-key "POST /webhooks/stripe" \
--target integrations/integ-f6g7h8i9j0
# ============================================================
# CONTAINERS: Core REST API (steady traffic, needs persistent connections)
# Handles user-facing product catalog API - benefits from warm containers
# ============================================================
# Create the ECS cluster for running containerized API services
# Capacity providers enable Fargate Spot for 70% cost savings on fault-tolerant tasks
aws ecs create-cluster \
--cluster-name prod-ecommerce-api-cluster \
--capacity-providers FARGATE FARGATE_SPOT \
--default-capacity-provider-strategy '[{"capacityProvider":"FARGATE","weight":1,"base":2},{"capacityProvider":"FARGATE_SPOT","weight":3}]' \
--configuration '{"executeCommandConfiguration":{"logging":"OVERRIDE","logConfiguration":{"cloudWatchLogGroupName":"/ecs/prod-ecommerce-api/exec-logs"}}}' \
--tags key=Team,value=platform key=Service,value=product-catalog-api
# Register the task definition for the product catalog API container
# 1 vCPU + 2GB RAM handles ~500 concurrent requests per task
aws ecs register-task-definition \
--family prod-product-catalog-api \
--requires-compatibilities FARGATE \
--network-mode awsvpc \
--cpu 1024 \
--memory 2048 \
--runtime-platform '{"cpuArchitecture":"ARM64","operatingSystemFamily":"LINUX"}' \
--execution-role-arn arn:aws:iam::123456789012:role/prod-ecs-task-execution-role \
--task-role-arn arn:aws:iam::123456789012:role/prod-product-catalog-api-task-role \
--container-definitions '[{
"name": "product-catalog-api",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/prod-product-catalog-api:v2.14.3",
"portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
"essential": true,
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
"interval": 15,
"timeout": 5,
"retries": 3,
"startPeriod": 30
},
"environment": [
{"name": "NODE_ENV", "value": "production"},
{"name": "PORT", "value": "8080"},
{"name": "REDIS_ENDPOINT", "value": "prod-catalog-cache.abc123.0001.use1.cache.amazonaws.com:6379"}
],
"secrets": [
{"name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/product-catalog/database-url-AbCdEf"}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/prod-product-catalog-api",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "api"
}
}
}]'
# Create the ECS service with auto-scaling behind the Application Load Balancer
# Minimum 2 tasks (one per AZ) for high availability, max 20 for peak traffic
aws ecs create-service \
--cluster prod-ecommerce-api-cluster \
--service-name prod-product-catalog-api \
--task-definition prod-product-catalog-api \
--desired-count 2 \
--launch-type FARGATE \
--platform-version 1.4.0 \
--network-configuration '{"awsvpcConfiguration":{"subnets":["subnet-0private1a","subnet-0private1b"],"securityGroups":["sg-0prodcatalogapi"],"assignPublicIp":"DISABLED"}}' \
--load-balancers '[{"targetGroupArn":"arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/prod-catalog-api-tg/abc123def456","containerName":"product-catalog-api","containerPort":8080}]' \
--deployment-configuration '{"deploymentCircuitBreaker":{"enable":true,"rollback":true},"maximumPercent":200,"minimumHealthyPercent":100}' \
--enable-execute-command
# Configure auto-scaling based on CPU utilization and request count
# Scale out at 70% CPU to stay ahead of traffic growth
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/prod-ecommerce-api-cluster/prod-product-catalog-api \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 \
--max-capacity 20
# Target tracking policy: maintain 70% average CPU across all tasks
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/prod-ecommerce-api-cluster/prod-product-catalog-api \
--scalable-dimension ecs:service:DesiredCount \
--policy-name prod-catalog-api-cpu-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{"TargetValue":70.0,"PredefinedMetricSpecification":{"PredefinedMetricType":"ECSServiceAverageCPUUtilization"},"ScaleOutCooldown":120,"ScaleInCooldown":300}'Interview Tip
A junior engineer typically picks one side ('serverless is always better' or 'containers give more control') without acknowledging that production systems use both patterns simultaneously. The strongest answers present a decision framework: use serverless for event-driven workloads under 15 minutes with unpredictable traffic, and containers for steady-state HTTP APIs, WebSocket servers, and workloads requiring more than 10GB memory. Quantify the crossover point: below approximately 1 million requests/day, Lambda is cheaper; above that, Fargate's fixed pricing wins. Mention the hidden costs of serverless: NAT Gateway charges for VPC-attached Lambdas, API Gateway per-request fees that exceed Lambda costs at high volume, and the operational complexity of managing thousands of Lambda functions. Interviewers are especially impressed when you discuss the hybrid approach: Lambda for webhooks and async processing, containers for the synchronous API, and CloudFront Functions for edge logic.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────────┐ │ Serverless vs Container Architecture Comparison │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────┐ ┌─────────────────────────────────┐ │ │ │ SERVERLESS PATTERN │ │ CONTAINER PATTERN │ │ │ │ (Event-Driven / Spiky) │ │ (Steady-State / Persistent) │ │ │ ├───────────────────────────────────┤ ├─────────────────────────────────┤ │ │ │ │ │ │ │ │ │ ┌─────────────┐ │ │ ┌──────────────┐ │ │ │ │ │ API Gateway │ │ │ │ ALB │ │ │ │ │ │ HTTP API v2 │ │ │ │ Path-based │ │ │ │ │ │ $1/million │ │ │ │ routing │ │ │ │ │ │ requests │ │ │ │ $0.0225/hr │ │ │ │ │ └──────┬──────┘ │ │ └──────┬───────┘ │ │ │ │ │ │ │ │ │ │ │ │ ↓ │ │ ↓ │ │ │ │ ┌─────────────┐ │ │ ┌──────────────────────┐ │ │ │ │ │ Lambda │ │ │ │ ECS Fargate Service │ │ │ │ │ │ ARM64 │ │ │ │ 2-20 tasks (ARM64) │ │ │ │ │ │ 512MB │ │ │ │ 1 vCPU / 2GB each │ │ │ │ │ │ 0-1000 │ │ │ │ Auto-scaling at │ │ │ │ │ │ concurrent │ │ │ │ 70% CPU utilization │ │ │ │ │ └──────┬──────┘ │ │ └──────┬───────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ↓ │ │ ↓ │ │ │ │ ┌─────────────┐ │ │ ┌──────────────┐ │ │ │ │ │ DynamoDB │ │ │ │ Aurora │ │ │ │ │ │ On-Demand │ │ │ │ PostgreSQL │ │ │ │ │ │ Single-table│ │ │ │ + ElastiCache│ │ │ │ │ └─────────────┘ │ │ │ Redis │ │ │ │ │ │ │ └──────────────┘ │ │ │ │ Cost at 1M req/mo: ~$20 │ │ Cost at 1M req/mo: ~$96 │ │ │ │ Cost at 100M req/mo: ~$1,867 │ │ Cost at 100M req/mo: ~$384 │ │ │ │ │ │ │ │ │ │ Strengths: │ │ Strengths: │ │ │ │ - Zero cost at zero traffic │ │ - No cold starts │ │ │ │ - Auto-scales to 1000+ │ │ - Persistent connections │ │ │ │ - No servers to patch │ │ - In-memory caching │ │ │ │ - Per-ms billing │ │ - Up to 120GB RAM per task │ │ │ │ │ │ - Full Docker ecosystem │ │ │ │ Constraints: │ │ │ │ │ │ - 15min max execution │ │ Constraints: │ │ │ │ - 10GB memory max │ │ - Min 2 tasks for HA ($) │ │ │ │ - Cold starts (100ms-3s) │ │ - 30-60s task startup time │ │ │ │ - 29s API Gateway timeout │ │ - Image pull overhead │ │ │ │ - Stateless only │ │ - Requires capacity planning │ │ │ └───────────────────────────────────┘ └─────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ HYBRID PATTERN (Production Reality) │ │ │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ ┌──────────────┐ │ │ │ │ │ CloudFront │──→ /api/* ──→ ALB ──→ ECS Fargate │ │ │ │ │ Distribution│──→ /webhooks/*──→ API GW ──→ Lambda │ │ │ │ │ │──→ /static/* ──→ S3 Origin │ │ │ │ └──────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │ │ │ │ S3 Upload Event │→ │ Lambda: Resize │→ │ SNS: Notify User │ │ │ │ │ │ (product image) │ │ Image + Generate │ │ (image ready) │ │ │ │ │ │ │ │ Thumbnails │ │ │ │ │ │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │ │ │ │ EventBridge Rule │→ │ Lambda: Daily │→ │ S3: CSV Report │ │ │ │ │ │ (cron: 0 2 * * *)│ │ Revenue Report │ │ for finance team │ │ │ │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │ │ │ │ │ │ │ Decision: Containers for sync APIs, Lambda for events/webhooks │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Use Service Control Policies (SCPs) as preventive guardrails at the AWS Organizations level to deny non-compliant actions before they happen, and AWS Config rules as detective controls to continuously evaluate resource configurations against compliance baselines. Deploy Config conformance packs for framework-specific compliance (PCI DSS, HIPAA, CIS Benchmarks), use Config remediation actions with SSM Automation for auto-fix, and aggregate findings across all accounts in a delegated administrator account.
Detailed Answer
Infrastructure compliance at scale requires a layered defense model: preventive controls stop non-compliant actions before they occur, detective controls identify drift after the fact, and corrective controls automatically remediate violations. Think of it like a highway system: SCPs are the physical guardrails that prevent cars from leaving the road, Config rules are the speed cameras that detect violations, and remediation actions are the automated toll systems that fix issues without human intervention.
Service Control Policies (SCPs) are the most powerful preventive control in AWS because they set the maximum permissions boundary for an entire account. SCPs do not grant permissions; they restrict what IAM policies can grant. This is a critical distinction: if an SCP denies ec2:RunInstances in a region, no IAM policy in that account can override it, not even the root user. SCPs apply to all principals in member accounts (except the management account itself, which is why you should never run workloads in the management account).
SCP design follows a deny-list or allow-list strategy. Deny-list is more common: start with the default FullAWSAccess SCP, then add explicit denies for non-compliant actions. For example, deny launching EC2 instances without IMDSv2 (Instance Metadata Service version 2), deny creating S3 buckets without encryption, or deny using regions outside your approved list. Allow-list is stricter: remove the default FullAWSAccess and explicitly allow only the services and actions needed. Allow-list is recommended for regulated industries (healthcare, finance) but requires significant upfront planning.
SCPs are attached to Organizational Units (OUs), not individual accounts. Structure your OUs to reflect compliance boundaries: a Production OU with the strictest SCPs (no delete operations on databases, mandatory encryption, restricted regions), a Development OU with relaxed SCPs (allow experimentation but deny production resource access), and a Sandbox OU with minimal restrictions but a spending cap via AWS Budgets actions.
AWS Config continuously records resource configurations and evaluates them against rules. Config rules come in two types: AWS-managed rules (160+ pre-built rules like s3-bucket-ssl-requests-only, encrypted-volumes, iam-root-access-key-check) and custom rules (Lambda functions that evaluate arbitrary compliance logic). For custom rules, use Config Rule Development Kit (RDK) to scaffold and test Lambda-based rules locally before deployment.
Conformance packs bundle multiple Config rules into compliance frameworks. AWS provides operational best practices packs for PCI DSS 3.2.1 (35 rules), HIPAA (55 rules), CIS AWS Foundations Benchmark (49 rules), and NIST 800-53 (100+ rules). Deploy conformance packs via AWS Organizations to roll them out across all accounts simultaneously. Each rule evaluates every relevant resource in every account, producing a compliance score per account and per rule.
Remediation actions are the corrective control layer. When a Config rule finds a non-compliant resource, it can trigger an SSM Automation document to fix the issue. For example, if s3-bucket-server-side-encryption-enabled finds an unencrypted bucket, the remediation action runs the AWS-EnableS3BucketEncryption document to apply AES-256 encryption. Use manual remediation (requires human approval via SNS) for production resources and automatic remediation for development accounts.
Config Aggregator collects compliance data from all accounts and regions into a single delegated administrator account. This gives your compliance team a centralized dashboard without granting them access to individual workload accounts. The aggregator supports both AWS Organizations-based collection (automatic for all member accounts) and individual account authorization.
Production gotchas: SCPs have a 5,120 character limit per policy, which you hit quickly with complex deny statements; use condition keys like aws:RequestedRegion with StringNotEquals instead of listing individual region denies. Config rules evaluate asynchronously, so there is a delay between resource creation and compliance evaluation (typically 1-10 minutes). Config recording charges $0.003 per configuration item recorded; in accounts with frequent Auto Scaling events, this adds up. Limit Config recording to the specific resource types you need to evaluate. SCP changes take effect immediately but can take up to 24 hours to propagate to all API endpoints globally; always test SCP changes in a sandbox OU first. Never attach a restrictive SCP to the management account OU; SCPs do not apply to the management account, creating a false sense of security.
Code Example
# Infrastructure compliance at scale: SCPs + Config rules + auto-remediation
# For a financial services company with 50+ AWS accounts in AWS Organizations
# ============================================================
# STEP 1: Create SCPs (Preventive Controls)
# ============================================================
# SCP 1: Deny launching EC2 instances without IMDSv2 (prevents SSRF attacks)
# IMDSv1 is vulnerable to credential theft via server-side request forgery
aws organizations create-policy \
--name "RequireIMDSv2OnAllEC2" \
--description "Prevent EC2 launches using IMDSv1 - mitigates Capital One style SSRF attacks" \
--type SERVICE_CONTROL_POLICY \
--content '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyIMDSv1Launches",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
},
{
"Sid": "DenyIMDSv1Modification",
"Effect": "Deny",
"Action": "ec2:ModifyInstanceMetadataOptions",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}
]
}'
# SCP 2: Restrict all workloads to approved regions only (data residency compliance)
# Only us-east-1 and eu-west-1 are approved for PCI-scoped data processing
aws organizations create-policy \
--name "RestrictToApprovedRegions" \
--description "Deny resource creation outside us-east-1 and eu-west-1 for data residency" \
--type SERVICE_CONTROL_POLICY \
--content '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonApprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"sts:*",
"support:*",
"budgets:*",
"cloudfront:*",
"route53:*",
"wafv2:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"us-east-1",
"eu-west-1"
]
}
}
}
]
}'
# SCP 3: Deny S3 buckets without encryption and deny public access
# Prevents data exposure incidents caused by misconfigured bucket policies
aws organizations create-policy \
--name "EnforceS3EncryptionAndBlockPublic" \
--description "Deny unencrypted S3 operations and prevent public bucket creation" \
--type SERVICE_CONTROL_POLICY \
--content '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedS3PutObject",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": ["aws:kms", "AES256"]
},
"Null": {
"s3:x-amz-server-side-encryption": "false"
}
}
},
{
"Sid": "DenyDisablingS3BlockPublicAccess",
"Effect": "Deny",
"Action": "s3:PutBucketPublicAccessBlock",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"s3:PublicAccessBlockConfiguration/BlockPublicAcls": "true"
}
}
}
]
}'
# Attach SCPs to the Production OU where financial workloads run
# Get the Production OU ID from the organization structure
aws organizations attach-policy \
--policy-id p-imdsv2policy \
--target-id ou-root-productionworkloads
# Attach the region restriction to the root OU (applies to all accounts)
aws organizations attach-policy \
--policy-id p-regionrestrict \
--target-id r-rootorgid
# ============================================================
# STEP 2: Deploy AWS Config Rules (Detective Controls)
# ============================================================
# Enable Config recording for all supported resource types in the account
# This creates the configuration recorder that tracks resource changes
aws configservice put-configuration-recorder \
--configuration-recorder '{"name":"prod-compliance-recorder","roleARN":"arn:aws:iam::123456789012:role/prod-config-service-role","recordingGroup":{"allSupported":true,"includeGlobalResourceTypes":true}}'
# Set up the delivery channel to store Config snapshots in the compliance bucket
# SNS topic notifies the security team when compliance changes are detected
aws configservice put-delivery-channel \
--delivery-channel '{"name":"prod-compliance-channel","s3BucketName":"prod-config-compliance-snapshots-123456789012","s3KeyPrefix":"config-history","snsTopicARN":"arn:aws:sns:us-east-1:123456789012:prod-config-compliance-alerts","configSnapshotDeliveryProperties":{"deliveryFrequency":"TwentyFour_Hours"}}'
# Start the configuration recorder to begin tracking resource changes
aws configservice start-configuration-recorder \
--configuration-recorder-name prod-compliance-recorder
# Deploy CIS AWS Foundations Benchmark conformance pack (49 rules)
# Covers IAM, logging, monitoring, and networking best practices
aws configservice put-conformance-pack \
--conformance-pack-name "CIS-AWS-Foundations-Benchmark-v1-4" \
--template-s3-uri "s3://prod-config-compliance-snapshots-123456789012/conformance-packs/CIS-benchmark-v1.4.yaml" \
--conformance-pack-input-parameters '[{"ParameterName":"AccessKeysRotatedParamMaxAccessKeyAge","ParameterValue":"90"}]'
# Create a custom Config rule: detect EBS volumes not using gp3 (cost optimization)
# gp3 is 20% cheaper than gp2 with better baseline performance
aws configservice put-config-rule \
--config-rule '{
"ConfigRuleName": "ebs-volumes-must-use-gp3",
"Description": "Detects EBS volumes not using gp3 type - gp3 is 20 percent cheaper than gp2",
"Source": {
"Owner": "CUSTOM_LAMBDA",
"SourceIdentifier": "arn:aws:lambda:us-east-1:123456789012:function:prod-config-rule-ebs-gp3-check",
"SourceDetails": [{"EventSource": "aws.config", "MessageType": "ConfigurationItemChangeNotification"}]
},
"Scope": {"ComplianceResourceTypes": ["AWS::EC2::Volume"]},
"MaximumExecutionFrequency": "TwentyFour_Hours"
}'
# ============================================================
# STEP 3: Configure Auto-Remediation (Corrective Controls)
# ============================================================
# Auto-remediate unencrypted S3 buckets by enabling AES-256 encryption
# Uses the AWS-managed SSM Automation document for S3 encryption
aws configservice put-remediation-configurations \
--remediation-configurations '[{
"ConfigRuleName": "s3-bucket-server-side-encryption-enabled",
"TargetType": "SSM_DOCUMENT",
"TargetId": "AWS-EnableS3BucketEncryption",
"Parameters": {
"BucketName": {"ResourceValue": {"Value": "RESOURCE_ID"}},
"SSEAlgorithm": {"StaticValue": {"Values": ["AES256"]}},
"AutomationAssumeRole": {"StaticValue": {"Values": ["arn:aws:iam::123456789012:role/prod-config-remediation-role"]}}
},
"Automatic": true,
"MaximumAutomaticAttempts": 3,
"RetryAttemptSeconds": 60
}]'
# Auto-remediate S3 buckets missing public access blocks
# Applies the four PublicAccessBlock settings to any non-compliant bucket
aws configservice put-remediation-configurations \
--remediation-configurations '[{
"ConfigRuleName": "s3-bucket-public-read-prohibited",
"TargetType": "SSM_DOCUMENT",
"TargetId": "AWS-DisableS3BucketPublicReadWrite",
"Parameters": {
"S3BucketName": {"ResourceValue": {"Value": "RESOURCE_ID"}},
"AutomationAssumeRole": {"StaticValue": {"Values": ["arn:aws:iam::123456789012:role/prod-config-remediation-role"]}}
},
"Automatic": true,
"MaximumAutomaticAttempts": 3,
"RetryAttemptSeconds": 60
}]'
# ============================================================
# STEP 4: Set up Config Aggregator for multi-account compliance dashboard
# ============================================================
# Create an organization-wide Config aggregator in the delegated admin account
# This collects compliance data from all 50+ accounts without individual setup
aws configservice put-configuration-aggregator \
--configuration-aggregator-name prod-org-compliance-aggregator \
--organization-aggregation-source '{"RoleArn":"arn:aws:iam::123456789012:role/prod-config-org-aggregator-role","AllAwsRegions":true}' \
--tags Key=Purpose,Value=centralized-compliance Key=Team,Value=security-engineeringInterview Tip
A junior engineer typically conflates SCPs with IAM policies, saying 'SCPs grant permissions to accounts.' This is wrong and will immediately flag you as lacking hands-on Organizations experience. SCPs only restrict; they never grant permissions. They set the maximum permissions boundary for all principals in member accounts. The strongest answers clearly distinguish the three control layers: preventive (SCPs deny actions before they happen), detective (Config rules identify non-compliant resources after creation), and corrective (SSM Automation remediates automatically). Mention the 5,120 character limit on SCPs and how to work around it using condition keys instead of listing individual resources. Discuss why the management account is exempt from SCPs and why you should never run workloads there. Explaining conformance packs for PCI DSS or CIS Benchmarks shows you understand compliance frameworks, not just AWS services.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────────┐ │ Infrastructure Compliance Architecture at Scale │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ AWS Organizations Structure │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Root (r-abc123) │ │ │ │ │ │ SCP: RestrictToApprovedRegions (us-east-1, eu-west-1 only) │ │ │ │ │ └──────────┬──────────────────┬───────────────────┬───────────┘ │ │ │ │ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ │ │ ┌──────────────────┐ ┌────────────────┐ ┌─────────────────────┐ │ │ │ │ │ Production OU │ │ Development OU │ │ Sandbox OU │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ SCPs: │ │ SCPs: │ │ SCPs: │ │ │ │ │ │ - RequireIMDSv2 │ │ - DenyProdAccs │ │ - SpendingCap $500 │ │ │ │ │ │ - EnforceS3Enc │ │ - AllowExprmnt │ │ - AllowAllServices │ │ │ │ │ │ - DenyDBDelete │ │ │ │ │ │ │ │ │ │ - MandatoryTags │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Accounts: │ │ Accounts: │ │ Accounts: │ │ │ │ │ │ 123456789012 │ │ 111222333444 │ │ 555666777888 │ │ │ │ │ │ 987654321098 │ │ 222333444555 │ │ 666777888999 │ │ │ │ │ │ (20 accounts) │ │ (15 accounts) │ │ (15 accounts) │ │ │ │ │ └──────────────────┘ └────────────────┘ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Three Layers of Compliance Controls │ │ │ │ │ │ │ │ Layer 1: PREVENTIVE (SCPs) Before resource creation │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Developer tries to launch EC2 with IMDSv1 │ │ │ │ │ │ │ │ │ │ │ │ │ ↓ │ │ │ │ │ │ ┌───────────────────┐ ┌──────────────────────────────┐ │ │ │ │ │ │ │ SCP Evaluation │───→│ DENIED: ec2:MetadataHttpTokens│ │ │ │ │ │ │ │ RequireIMDSv2 │ │ must equal "required" │ │ │ │ │ │ │ └───────────────────┘ └──────────────────────────────┘ │ │ │ │ │ │ Result: Resource NEVER created. Zero remediation needed. │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ Layer 2: DETECTIVE (Config Rules) After resource creation │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Resource Created ──→ Config Records Change │ │ │ │ │ │ │ │ │ │ │ │ │ ↓ │ │ │ │ │ │ ┌───────────────────┐ ┌──────────────────────────────┐ │ │ │ │ │ │ │ Config Rule │───→│ NON_COMPLIANT: S3 bucket │ │ │ │ │ │ │ │ s3-bucket-ssl- │ │ prod-reports-bucket does not │ │ │ │ │ │ │ │ requests-only │ │ enforce SSL-only access │ │ │ │ │ │ │ └───────────────────┘ └──────────────┬───────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ Conformance Packs: │ │ │ │ │ │ │ ┌────────────────┐ ┌────────────┐ │ │ │ │ │ │ │ │ CIS Benchmark │ │ PCI DSS │ │ │ │ │ │ │ │ │ 49 rules │ │ 35 rules │ │ │ │ │ │ │ │ └────────────────┘ └────────────┘ │ │ │ │ │ │ └─────────────────────────────────────────┼──────────────────┘ │ │ │ │ │ │ │ │ │ Layer 3: CORRECTIVE (Auto-Remediation) │ │ │ │ │ ┌─────────────────────────────────────────┼──────────────────┐ │ │ │ │ │ ↓ │ │ │ │ │ │ ┌───────────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ │ │ SSM Automation │───→│ REMEDIATED: SSL policy │ │ │ │ │ │ │ │ AWS-ConfigRemediation- │ │ applied to bucket. Status│ │ │ │ │ │ │ │ SetSSLBucketPolicy │ │ changed to COMPLIANT. │ │ │ │ │ │ │ └───────────────────────┘ └──────────────────────────┘ │ │ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Centralized Compliance Dashboard │ │ │ │ │ │ │ │ ┌───────────────────────┐ │ │ │ │ │ Config Aggregator │←── All 50 accounts, all regions │ │ │ │ │ (Delegated Admin) │ │ │ │ │ └───────────┬───────────┘ │ │ │ │ │ │ │ │ │ ↓ │ │ │ │ ┌───────────────────┐ ┌────────────────┐ ┌──────────────────┐ │ │ │ │ │ Security Hub │ │ QuickSight │ │ SNS → PagerDuty │ │ │ │ │ │ Compliance Score │ │ Dashboard │ │ Critical alerts │ │ │ │ │ │ per account │ │ Trend analysis │ │ to SecOps team │ │ │ │ │ └───────────────────┘ └────────────────┘ └──────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
AWS provides four methods: SSE-S3 (Amazon-managed keys), SSE-KMS (AWS KMS-managed keys with audit trail), SSE-C (customer-provided keys), and client-side encryption (encrypt before upload). Each method addresses different compliance, key ownership, and operational requirements.
Detailed Answer
AWS S3 offers four distinct encryption-at-rest mechanisms, each serving different security postures and compliance needs. Understanding all four is critical for any DevOps engineer working in regulated environments like PCI-DSS, HIPAA, or SOC2.
First, SSE-S3 (Server-Side Encryption with S3-Managed Keys) is the simplest option. Think of it like a hotel safe where the hotel manages the master key. AWS generates, manages, and rotates a unique AES-256 key for every object. You have zero visibility into the key material itself. This is the default encryption for all new S3 buckets since January 2023. The trade-off is that anyone with s3:GetObject permission can decrypt the data — there is no separate key permission layer.
Second, SSE-KMS (Server-Side Encryption with AWS KMS Keys) adds a permission boundary. Imagine a safety deposit box that requires both the bank's master key and your personal key to open. KMS creates a Customer Master Key (CMK), and every object encryption generates a unique Data Encryption Key (DEK) via the envelope encryption pattern. The CMK encrypts the DEK, and the DEK encrypts the object. This means decryption requires both s3:GetObject AND kms:Decrypt permissions — a powerful separation of duties. Every decryption call is logged in CloudTrail, giving you a full audit trail. The gotcha in production is KMS request throttling: each S3 GET/PUT triggers a KMS API call, and the default quota is 5,500 requests per second per region (10,000 in some regions). Teams running large Spark or EMR jobs against encrypted datasets often hit this limit and see throttling errors. The fix is to request a quota increase or use S3 Bucket Keys, which reduce KMS calls by up to 99% by caching a bucket-level key.
Third, SSE-C (Server-Side Encryption with Customer-Provided Keys) means you supply the encryption key with every PUT and GET request over HTTPS. AWS uses your key, encrypts the object, then discards the key immediately — they never store it. This is like bringing your own padlock to a storage unit. The massive operational burden is that if you lose the key, your data is gone forever. AWS cannot recover it. Every API call must include the key in the request header, which means your application layer must have a robust key management system. This is rarely used in practice but is required by some government contracts where key material must never persist on a third-party system.
Fourth, client-side encryption means you encrypt data before it ever leaves your network. AWS receives only ciphertext and has zero knowledge of the plaintext or keys. You can use the AWS Encryption SDK, which implements envelope encryption with a local master key or an HSM-backed key. This is the highest security posture but adds latency and complexity. You must manage key rotation, handle encryption errors, and ensure your client library is up to date against vulnerabilities.
In production, most organizations use SSE-KMS as the default for its balance of security, auditability, and manageability. SSE-S3 is acceptable for non-sensitive data. SSE-C and client-side encryption appear in highly regulated or multi-tenant environments where cryptographic key isolation is a compliance requirement.
Code Example
# Enable SSE-KMS encryption as the default for a production analytics bucket
aws s3api put-bucket-encryption \
--bucket prod-analytics-data-lake \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:119283746501:key/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"
},
"BucketKeyEnabled": true
}
]
}'
# SSEAlgorithm: specifies KMS-managed encryption instead of S3-managed
# KMSMasterKeyID: references the specific CMK created for this data classification tier
# BucketKeyEnabled: reduces KMS API calls by caching a bucket-level data key
# Upload a file using SSE-C where the customer provides the encryption key
aws s3api put-object \
--bucket classified-documents-vault \
--key legal/contract-2026-merger.pdf \
--body ./contract-2026-merger.pdf \
--sse-customer-algorithm AES256 \
--sse-customer-key fileb://./encryption-keys/legal-team-aes256.key \
--sse-customer-key-md5 "pUNR8Yfkz3VcYtGOEcKJmA=="
# --bucket: the target bucket holding classified legal documents
# --key: the S3 object key with a logical folder prefix for legal files
# --body: local file path to the document being uploaded
# --sse-customer-algorithm: must be AES256 for SSE-C
# --sse-customer-key: path to the raw 256-bit key file the customer manages
# --sse-customer-key-md5: base64-encoded MD5 digest of the key for integrity verification
# Verify encryption status of an existing object
aws s3api head-object \
--bucket prod-analytics-data-lake \
--key transactions/2026/06/daily-settlements.parquet
# Returns ServerSideEncryption: aws:kms and the KMS key ARN used
# Confirms the object is encrypted with the expected keyInterview Tip
A junior engineer typically answers this question by naming only SSE-S3 and SSE-KMS, completely forgetting SSE-C and client-side encryption. They also fail to mention the envelope encryption pattern that KMS uses internally, where a Data Encryption Key encrypts the object and the CMK encrypts the DEK. To stand out, explain the KMS throttling gotcha in production — specifically the 5,500 requests/second default quota and how S3 Bucket Keys solve it. Mention that SSE-S3 became the default for all new buckets in January 2023, and explain why SSE-KMS adds value through its separate kms:Decrypt permission requirement and CloudTrail audit logging. Interviewers at senior levels want to hear about the operational trade-offs, not just feature lists.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────────────┐ │ S3 Encryption Methods │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ SSE-S3 │ │ SSE-KMS │ │ SSE-C │ │ │ │ AWS Managed │ │ KMS Managed │ │ Customer │ │ │ │ Keys │ │ Keys │ │ Keys │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ AES-256 │ │ Envelope │ │ Key Sent │ │ │ │ Per Object │ │ Encryption │ │ Per Request│ │ │ │ Auto Key │ │ CMK → DEK │ │ AWS Discards│ │ │ └─────────────┘ └──────┬──────┘ └─────────────┘ │ │ │ │ │ ↓ │ │ ┌─────────────┐ │ │ │ CloudTrail │ │ │ │ Audit Log │ │ │ │ Every Call │ │ │ └─────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ Client-Side Encryption │ │ │ │ App encrypts → ciphertext uploaded → S3 stores │ │ │ │ AWS has zero knowledge of keys or plaintext │ │ │ └──────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
A VPC is an isolated virtual network defined by a CIDR block. It contains public subnets (with Internet Gateway routes) and private subnets (with NAT Gateway routes for outbound-only internet). Route tables control traffic flow, and CIDR allocation must be planned to avoid overlap with peered VPCs or on-premises networks.
Detailed Answer
A Virtual Private Cloud (VPC) is your isolated section of the AWS cloud — think of it as your own private office building within a massive shared complex. You control the floor plan (CIDR block), the rooms (subnets), the hallways between rooms (route tables), and the doors to the outside world (gateways).
CIDR allocation is the foundation. When you create a VPC, you assign a primary CIDR block, such as 10.0.0.0/16, which gives you 65,536 IP addresses. This is like choosing the size of your building lot before construction. The critical production consideration is planning for growth and peering. If your payments VPC uses 10.0.0.0/16 and your analytics VPC also uses 10.0.0.0/16, you cannot peer them because overlapping CIDR blocks create routing ambiguity. Many organizations adopt a structured allocation scheme: 10.0.0.0/16 for production, 10.1.0.0/16 for staging, 10.2.0.0/16 for development, and 172.16.0.0/12 reserved for on-premises connectivity via Direct Connect.
Subnets divide the VPC CIDR into smaller segments, each tied to a single Availability Zone. A /16 VPC might be split into /24 subnets, each with 251 usable IPs (AWS reserves 5 per subnet: network address, VPC router, DNS server, future use, and broadcast). Public subnets host resources that need direct internet access — load balancers, bastion hosts, NAT gateways. Private subnets host application servers, databases, and backend services that should never be directly reachable from the internet. The distinction between public and private is not a property of the subnet itself but entirely determined by the route table associated with it.
Route tables are the traffic control system. Every subnet must be associated with exactly one route table. A public subnet's route table has a route like 0.0.0.0/0 pointing to an Internet Gateway (IGW), which allows bidirectional internet traffic. A private subnet's route table has 0.0.0.0/0 pointing to a NAT Gateway, which allows outbound-only internet access. The local route (10.0.0.0/16 → local) is automatically present and enables intra-VPC communication. You can add more specific routes for VPC peering, Transit Gateway, VPN connections, or VPC endpoints.
NAT Gateways sit in a public subnet and enable private subnet resources to initiate outbound connections (pulling Docker images, downloading OS patches, calling external APIs) without exposing them to inbound internet traffic. Think of it as a one-way mirror: your servers can see out, but the internet cannot see in. NAT Gateways are managed, highly available within an AZ, and charged per hour plus per GB processed. A common production mistake is deploying only one NAT Gateway for the entire VPC — if that AZ goes down, all private subnets lose internet access. Best practice is one NAT Gateway per AZ with AZ-specific route tables.
The Internet Gateway is a horizontally scaled, redundant VPC component that performs network address translation for instances with public IPs. Unlike NAT Gateways, IGWs are free and have no bandwidth limits. They are attached at the VPC level but only affect subnets whose route tables direct 0.0.0.0/0 traffic to the IGW.
In production multi-account architectures, VPCs are connected via Transit Gateway, which acts as a central hub. This avoids the O(n^2) complexity of full-mesh VPC peering. Security Groups (stateful, instance-level) and Network ACLs (stateless, subnet-level) add defense-in-depth. VPC Flow Logs capture metadata about all traffic for security analysis and troubleshooting.
Code Example
# Create the production payments VPC with a /16 CIDR block
aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=payments-vpc},{Key=Environment,Value=production}]'
# --cidr-block: allocates 65536 IPs for the entire payments platform
# --tag-specifications: names the VPC for console visibility and cost allocation
# Create a public subnet in us-east-1a for load balancers and NAT gateways
aws ec2 create-subnet \
--vpc-id vpc-0a1b2c3d4e5f67890 \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=payments-public-us-east-1a},{Key=Tier,Value=public}]'
# --vpc-id: references the payments VPC created above
# --cidr-block: carves out 251 usable IPs from the VPC range for this subnet
# --availability-zone: pins this subnet to a single AZ for fault isolation
# Create a private subnet in us-east-1a for payment processing application servers
aws ec2 create-subnet \
--vpc-id vpc-0a1b2c3d4e5f67890 \
--cidr-block 10.0.10.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=payments-private-app-us-east-1a},{Key=Tier,Value=private}]'
# --cidr-block: uses the 10.0.10.x range reserved for private application tier
# Separated from public range (10.0.1.x) for clear network segmentation
# Create an Internet Gateway and attach it to the payments VPC
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=payments-igw}]'
# Creates the gateway that enables bidirectional internet access for public subnets
aws ec2 attach-internet-gateway \
--internet-gateway-id igw-0abcdef1234567890 \
--vpc-id vpc-0a1b2c3d4e5f67890
# Attaches the IGW to the payments VPC — required before any public routing works
# Allocate an Elastic IP for the NAT Gateway in us-east-1a
aws ec2 allocate-address \
--domain vpc \
--tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=Name,Value=payments-nat-eip-us-east-1a}]'
# --domain vpc: allocates a static public IP that persists across NAT Gateway replacements
# Create a NAT Gateway in the public subnet for private subnet outbound access
aws ec2 create-nat-gateway \
--subnet-id subnet-0pub1a2b3c4d5e6f7 \
--allocation-id eipalloc-0a1b2c3d4e5f67890 \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=payments-nat-us-east-1a}]'
# --subnet-id: places the NAT Gateway in the public subnet so it can reach the IGW
# --allocation-id: assigns the stable Elastic IP to the NAT Gateway
# Create a route table for private subnets and add the NAT Gateway route
aws ec2 create-route-table \
--vpc-id vpc-0a1b2c3d4e5f67890 \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=payments-private-rt-us-east-1a}]'
# Creates a dedicated route table for private subnets in this AZ
aws ec2 create-route \
--route-table-id rtb-0priv1a2b3c4d5e6f \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-0a1b2c3d4e5f67890
# --destination-cidr-block 0.0.0.0/0: default route for all non-VPC traffic
# --nat-gateway-id: sends outbound internet traffic through the NAT Gateway
# Associate the private route table with the private application subnet
aws ec2 associate-route-table \
--route-table-id rtb-0priv1a2b3c4d5e6f \
--subnet-id subnet-0priv1a2b3c4d5e6f
# Links this route table to the private subnet, making NAT the outbound pathInterview Tip
A junior engineer typically describes a VPC as just a network with public and private subnets but cannot articulate what actually makes a subnet public or private. The answer is exclusively the route table — a subnet with a route to an Internet Gateway is public, period. Emphasize that you plan CIDR blocks before creating anything, accounting for VPC peering constraints and on-premises connectivity. Mention the five reserved IPs per subnet (most candidates forget this), the production best practice of one NAT Gateway per AZ, and the cost implications of NAT Gateway data processing charges. If you can discuss Transit Gateway as an alternative to full-mesh peering, you demonstrate senior-level network architecture thinking.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ payments-vpc (10.0.0.0/16) │
│ │
│ ┌──────────────── us-east-1a ──────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Public Subnet (10.0.1.0/24) │ │ │
│ │ │ ┌───────────┐ ┌──────────────┐ │ │ │
│ │ │ │ ALB │ │ NAT Gateway │ │ │ │
│ │ │ └─────┬─────┘ └──────┬───────┘ │ │ │
│ │ └────────┼─────────────────┼──────────────┘ │ │
│ │ │ │ │ │
│ │ ┌────────┼─────────────────┼──────────────┐ │ │
│ │ │ Private Subnet (10.0.10.0/24) │ │ │
│ │ │ ↓ ↑ │ │ │
│ │ │ ┌───────────┐ ┌───────────┐ │ │ │
│ │ │ │ Payment │ │ Payment │ │ │ │
│ │ │ │ App Svc 1 │ │ App Svc 2 │ │ │ │
│ │ │ └───────────┘ └───────────┘ │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ Internet Gateway│ │
│ │ (payments-igw) │ │
│ └────────┬────────┘ │
└──────────────────────────┼──────────────────────────────────────────┘
↓
┌─────────────┐
│ Internet │
└─────────────┘💬 Comments
Quick Answer
IAM (Identity and Access Management) controls who can do what in AWS. Policy evaluation follows a strict order: explicit Deny always wins, then Organizations SCPs, then resource-based policies, then identity-based policies, then permissions boundaries, then session policies. The default is implicit deny — no access unless explicitly granted.
Detailed Answer
AWS Identity and Access Management (IAM) is the foundation of every security decision in AWS. Think of it as the security system for an entire corporate campus — it manages who people are (authentication), what badge they carry (identity), what doors they can open (authorization), and logs every door they walk through (auditing).
IAM has four core building blocks. Users are individual identities with long-term credentials (access keys and passwords). Groups are collections of users that share the same permissions — like giving all developers the same badge level. Roles are temporary identities that anyone or anything can assume — humans, EC2 instances, Lambda functions, even users from other AWS accounts. Roles issue short-lived credentials via AWS STS (Security Token Service), making them far more secure than long-term user access keys. Policies are JSON documents that define permissions — they are the actual rules that say allow or deny specific actions on specific resources under specific conditions.
Policies come in several types. Identity-based policies are attached to users, groups, or roles and define what that identity can do. Resource-based policies are attached to resources (S3 buckets, SQS queues, KMS keys) and define who can access that resource. Permissions boundaries set the maximum permissions an identity can have — they do not grant access themselves but cap what identity-based policies can grant. Think of it as a fence around what an identity is allowed to request. Organizations Service Control Policies (SCPs) set the maximum permissions for an entire AWS account — they are guardrails applied at the organizational level.
The policy evaluation logic is where most engineers get confused, and getting it wrong leads to security vulnerabilities. AWS evaluates policies in a specific order with a strict hierarchy. The algorithm works as follows:
Step 1: All policies are gathered — identity-based, resource-based, SCPs, permissions boundaries, and session policies. Step 2: AWS checks for any explicit Deny across ALL policies. If any single policy statement has Effect: Deny that matches the request, access is denied immediately — no exceptions. This is the most important rule and cannot be overridden. Step 3: AWS checks Organizations SCPs. If the action is not allowed by SCPs, the request is denied even if an identity policy allows it. Step 4: For resource-based policies, if the resource policy explicitly grants access AND is in the same account, access can be granted even without an identity-based policy allowing it. This is a critical nuance — resource-based policies can grant cross-principal access within an account. Step 5: AWS checks identity-based policies. The action must be explicitly allowed here. Step 6: If a permissions boundary exists, the action must ALSO be allowed by the boundary. The effective permission is the intersection of the identity policy and the boundary. Step 7: If session policies exist (used with assumed roles or federated sessions), the action must also be allowed by the session policy.
The default state is implicit deny — if no policy explicitly allows an action, it is denied. This is the principle of least privilege enforced at the platform level.
A common production gotcha is the confused deputy problem in cross-account access. If your account lets another account assume a role, you must use the ExternalId condition to prevent a malicious third party from tricking the trusted account into accessing your resources. Another gotcha is policy size limits: managed policies max at 6,144 characters, and inline policies max at 2,048 characters for users and 10,240 for roles. Complex environments often hit these limits and must refactor into multiple policies.
In production, best practice is to use roles exclusively (never long-term access keys), enforce MFA for console access, use permissions boundaries for delegated administration, and audit with IAM Access Analyzer to find unintended external access.
Code Example
# Create an IAM policy that allows payment service to read/write to its DynamoDB table
aws iam create-policy \
--policy-name payment-processing-dynamodb-access \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPaymentTableReadWrite",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
"dynamodb:BatchWriteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:119283746501:table/payment-transactions",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
}
}
},
{
"Sid": "DenyDeleteTable",
"Effect": "Deny",
"Action": [
"dynamodb:DeleteTable",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:119283746501:table/payment-transactions"
}
]
}'
# --policy-name: descriptive name following service-action-resource convention
# Statement 1: grants specific read/write actions (not dynamodb:*) on the payment table only
# Condition: restricts requests to the us-east-1 region as additional guardrail
# Statement 2: explicitly denies destructive operations — this overrides any other Allow
# Create a permissions boundary that caps what any developer-created role can do
aws iam create-policy \
--policy-name developer-permissions-boundary \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowComputeAndStorage",
"Effect": "Allow",
"Action": [
"ec2:*",
"s3:*",
"lambda:*",
"dynamodb:*",
"logs:*",
"cloudwatch:*"
],
"Resource": "*"
},
{
"Sid": "DenyIAMEscalation",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:CreateRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"organizations:*"
],
"Resource": "*"
}
]
}'
# Statement 1: allows common compute and storage actions as the outer boundary
# Statement 2: explicitly prevents IAM privilege escalation and Org-level changes
# Even if a developer attaches AdministratorAccess to their role, this boundary blocks IAM/Org actions
# Attach the permissions boundary to a developer role
aws iam put-role-permissions-boundary \
--role-name payment-service-developer-role \
--permissions-boundary arn:aws:iam::119283746501:policy/developer-permissions-boundary
# Ensures this role can never exceed the boundary even if its identity policies are broadInterview Tip
A junior engineer typically says IAM is about users and permissions but cannot walk through the policy evaluation algorithm. The key differentiator is knowing the evaluation order: explicit Deny beats everything, then SCPs, then resource-based policies (which can independently grant access within the same account), then identity-based policies, then permissions boundaries as an intersection. Always mention that the default is implicit deny. Bring up real production patterns: permissions boundaries for delegated admin, the confused deputy problem with ExternalId, and IAM Access Analyzer for finding unintended public or cross-account access. If you can explain why resource-based policies behave differently from identity-based policies in cross-account scenarios, you demonstrate deep understanding.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────────┐ │ IAM Policy Evaluation Flow │ │ │ │ ┌──────────────────────┐ │ │ │ Request: Action on │ │ │ │ Resource by Identity│ │ │ └──────────┬───────────┘ │ │ ↓ │ │ ┌──────────────────────┐ YES │ │ │ Explicit Deny in ANY ├──────────→ ACCESS DENIED │ │ │ policy? │ │ │ └──────────┬───────────┘ │ │ ↓ NO │ │ ┌──────────────────────┐ NO │ │ │ Allowed by Org SCP? ├──────────→ ACCESS DENIED │ │ └──────────┬───────────┘ │ │ ↓ YES │ │ ┌──────────────────────┐ YES │ │ │ Resource-based policy├──────────→ ACCESS GRANTED │ │ │ grants access? │ (same account only) │ │ └──────────┬───────────┘ │ │ ↓ NO │ │ ┌──────────────────────┐ NO │ │ │ Identity-based policy├──────────→ IMPLICIT DENY │ │ │ allows action? │ │ │ └──────────┬───────────┘ │ │ ↓ YES │ │ ┌──────────────────────┐ NO │ │ │ Within Permissions ├──────────→ ACCESS DENIED │ │ │ Boundary? │ │ │ └──────────┬───────────┘ │ │ ↓ YES │ │ ACCESS GRANTED │ └──────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
EKS is AWS's managed Kubernetes service where AWS runs and maintains the control plane (API server, etcd, scheduler, controller manager) across multiple AZs. You manage worker nodes. Self-managed Kubernetes requires you to install, patch, scale, and maintain every component yourself. EKS trades customization for operational simplicity.
Detailed Answer
Understanding the difference between EKS and self-managed Kubernetes requires knowing what the Kubernetes control plane actually does and why managing it yourself is an operational burden that most teams underestimate.
Kubernetes has two layers: the control plane and the data plane. The control plane is the brain — it consists of the API server (receives all requests), etcd (the distributed key-value store holding all cluster state), the scheduler (decides which node runs each pod), and the controller manager (ensures desired state matches actual state). The data plane is the muscle — the worker nodes running kubelet, kube-proxy, and your actual application containers.
With self-managed Kubernetes, you own everything. You must install kubeadm or use a tool like kops to bootstrap the cluster. You manage etcd yourself — this means configuring it as a 3 or 5 node cluster for quorum, managing backups, handling disk pressure, and performing version upgrades that require careful rolling restarts. etcd is the single most critical component: if etcd loses data, your cluster state is gone. You must also handle API server high availability by running multiple replicas behind a load balancer, manage TLS certificates for all component-to-component communication (API server to kubelet, kubelet to API server, etcd peer communication), and handle Kubernetes version upgrades which require upgrading the control plane first, then worker nodes, with careful attention to version skew policies.
With EKS, AWS manages the entire control plane. The API server runs across at least three Availability Zones with automatic failover. etcd is managed, backed up, and encrypted by AWS. You never SSH into a control plane node — you cannot even see them. AWS handles Kubernetes version upgrades for the control plane (you trigger the upgrade, AWS performs the rolling update). The API server endpoint can be configured as public (accessible from the internet), private (accessible only within the VPC), or both. EKS also integrates natively with AWS services: IAM Roles for Service Accounts (IRSA) maps Kubernetes service accounts to IAM roles, the AWS Load Balancer Controller provisions ALBs and NLBs from Kubernetes Ingress resources, EBS CSI driver manages persistent volumes, and CloudWatch Container Insights provides monitoring.
However, EKS is not fully managed — you still manage worker nodes. You have three options: self-managed node groups (full EC2 control, you handle AMI updates and scaling), managed node groups (AWS handles node provisioning and lifecycle, you choose instance types), and Fargate profiles (fully serverless, AWS manages the underlying compute — no nodes to manage at all). Each option trades control for operational simplicity.
The cost comparison is nuanced. EKS charges $0.10 per hour ($73/month) for the control plane. Self-managed Kubernetes has no such fee, but you pay for the EC2 instances running control plane components, plus the engineering time to maintain them. For most organizations, the control plane management fee is trivial compared to the operational cost of maintaining etcd, handling certificate rotation, and performing safe upgrades. A single botched etcd backup or failed upgrade can cost days of downtime.
Production gotchas with EKS include: the API server has rate limits (you can hit throttling with large clusters), Kubernetes version support has a lifecycle (EKS supports four minor versions, and you must upgrade before end-of-support or AWS will auto-upgrade), and the VPC CNI plugin uses one ENI per pod by default, which limits pod density based on instance type ENI limits (a t3.medium supports only 17 pods). Self-managed Kubernetes avoids the CNI pod density issue since you can choose Calico or Cilium with overlay networking, but introduces its own networking complexity.
The decision framework: choose EKS if you want to focus on application delivery rather than infrastructure. Choose self-managed if you need full control over the control plane (rare), have a dedicated platform engineering team, or have compliance requirements that demand you manage every component.
Code Example
# Create an EKS cluster for the payment processing platform
aws eks create-cluster \
--name payment-processing-cluster \
--role-arn arn:aws:iam::119283746501:role/payment-eks-cluster-role \
--resources-vpc-config subnetIds=subnet-0pub1a,subnet-0pub1b,subnet-0priv1a,subnet-0priv1b,securityGroupIds=sg-0eks1a2b3c4d,endpointPublicAccess=false,endpointPrivateAccess=true \
--kubernetes-version 1.29 \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'
# --name: identifies this cluster in the payment processing domain
# --role-arn: the IAM role EKS assumes to manage AWS resources on your behalf
# --resources-vpc-config: places the cluster in the payments VPC across 4 subnets in 2 AZs
# endpointPublicAccess=false: API server is not reachable from the internet
# endpointPrivateAccess=true: API server only accessible within the VPC
# --kubernetes-version: pins to 1.29 for stability; upgrade on your schedule
# --logging: enables all control plane log types to CloudWatch for security auditing
# Create a managed node group for payment processing workloads
aws eks create-nodegroup \
--cluster-name payment-processing-cluster \
--nodegroup-name payment-workers-arm64 \
--node-role arn:aws:iam::119283746501:role/payment-eks-node-role \
--subnets subnet-0priv1a subnet-0priv1b \
--instance-types m7g.xlarge \
--scaling-config minSize=3,maxSize=20,desiredSize=5 \
--ami-type AL2_ARM_64 \
--labels environment=production,service=payment-processing \
--tags Team=payments,CostCenter=CC-4521
# --nodegroup-name: descriptive name including architecture for clarity
# --subnets: worker nodes run in private subnets only — never public
# --instance-types: ARM-based m7g for 20% better price-performance than x86
# --scaling-config: min 3 ensures HA, max 20 caps cost, desired 5 handles baseline load
# --ami-type: AL2_ARM_64 uses Amazon Linux 2 optimized for ARM processors
# --labels: Kubernetes labels for pod scheduling and node affinity rules
# --tags: AWS tags for cost tracking and team ownership
# Configure IRSA so the payment pod uses a specific IAM role without node-level credentials
aws eks create-addon \
--cluster-name payment-processing-cluster \
--addon-name vpc-cni \
--addon-version v1.16.0-eksbuild.1 \
--resolve-conflicts OVERWRITE
# Installs the VPC CNI addon managed by EKS for native VPC pod networking
# --resolve-conflicts OVERWRITE: replaces any self-managed CNI configurationInterview Tip
A junior engineer typically says EKS is managed Kubernetes without explaining what managed actually means in concrete terms. The winning answer details exactly which components AWS manages (API server, etcd, scheduler, controller manager across 3 AZs) and what you still own (worker nodes, pod networking, application deployment). Mention the three node management options (self-managed, managed node groups, Fargate) and when each applies. Bring up the VPC CNI pod density limitation tied to ENI counts per instance type — this is a real production constraint that surprises many teams. If you can discuss IRSA for secure pod-level IAM authentication, you demonstrate you have operated EKS in production, not just read about it.
◈ Architecture Diagram
┌───────────────────────────────────────────────────────────────────┐ │ EKS Architecture │ │ │ │ ┌─────────── AWS Managed (Control Plane) ─────────────┐ │ │ │ │ │ │ │ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ │ │ │ │ API Server │ │ API Server │ │ API Server │ │ │ │ │ │ (AZ-1a) │ │ (AZ-1b) │ │ (AZ-1c) │ │ │ │ │ └─────┬──────┘ └─────┬──────┘ └──────┬───────┘ │ │ │ │ └───────────────┼────────────────┘ │ │ │ │ ↓ │ │ │ │ ┌──────────────────┐ │ │ │ │ │ etcd Cluster │ │ │ │ │ │ (3-node, managed │ │ │ │ │ │ encrypted, HA) │ │ │ │ │ └──────────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────────┐ │ │ │ │ │ Scheduler │ │ Controller Mgr │ │ │ │ │ └──────────────┘ └──────────────────┘ │ │ │ └─────────────────────────┬───────────────────────────┘ │ │ ↓ │ │ ┌─────────── Customer Managed (Data Plane) ───────────┐ │ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Worker Node │ │ Worker Node │ │ Worker Node │ │ │ │ │ │ (m7g.xlarge)│ │ (m7g.xlarge)│ │ (m7g.xlarge)│ │ │ │ │ │ kubelet │ │ kubelet │ │ kubelet │ │ │ │ │ │ kube-proxy │ │ kube-proxy │ │ kube-proxy │ │ │ │ │ │ VPC CNI │ │ VPC CNI │ │ VPC CNI │ │ │ │ │ │ ┌────┐┌────┐│ │ ┌────┐┌────┐│ │ ┌────┐┌────┐│ │ │ │ │ │ │Pod ││Pod ││ │ │Pod ││Pod ││ │ │Pod ││Pod ││ │ │ │ │ │ └────┘└────┘│ │ └────┘└────┘│ │ └────┘└────┘│ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
A cold start occurs when Lambda must provision a new execution environment: downloading code, starting the runtime, initializing dependencies, and running your init code. This adds 100ms-10s of latency depending on runtime, package size, and VPC configuration. Optimize with provisioned concurrency, smaller packages, lazy initialization, and SnapStart (Java).
Detailed Answer
AWS Lambda cold starts are one of the most misunderstood performance characteristics in serverless computing. Think of it like starting a car engine versus driving an already-running car. A warm invocation (engine running) just executes your handler function. A cold start (cold engine) requires Lambda to perform several heavy steps before your code can run.
The cold start sequence has distinct phases. Phase 1 is environment provisioning: Lambda allocates a microVM using Firecracker (AWS's lightweight virtualization technology), provisions CPU and memory based on your configuration, and sets up the network stack. This takes approximately 100-200ms and is outside your control. Phase 2 is code download: Lambda downloads your deployment package from S3 (or pulls your container image from ECR). A 50MB zip takes meaningfully longer than a 5MB zip. Phase 3 is runtime initialization: the language runtime starts — the Python interpreter, the Node.js V8 engine, or the JVM. The JVM is notoriously slow here, often adding 2-5 seconds. Phase 4 is your initialization code: anything outside your handler function runs — importing libraries, establishing database connections, loading ML models, reading configuration from Parameter Store. This is where most of the controllable latency lives.
After the cold start, the execution environment stays warm for 5-15 minutes (AWS does not guarantee a specific duration). Subsequent invocations reuse the warm environment and skip directly to executing the handler. This is why global variables persist between invocations — a database connection pool initialized in the cold start is reused across warm invocations.
VPC-attached Lambda functions historically had severe cold starts (10+ seconds) because Lambda had to create an Elastic Network Interface. Since 2019, AWS uses Hyperplane (the internal network function virtualization platform), which pre-creates shared ENIs. This reduced VPC cold starts to roughly the same as non-VPC cold starts, but there is still additional latency for initial security group and subnet configuration.
1. Provisioned Concurrency: AWS pre-initializes a specified number of execution environments that are always warm. You pay for them whether they are used or not. This completely eliminates cold starts for that capacity. It is essential for latency-sensitive workloads like payment processing APIs where a 2-second cold start is unacceptable.
2. SnapStart (Java only): AWS takes a Firecracker microVM snapshot after your initialization code runs, then restores from this snapshot instead of re-initializing. This reduces Java cold starts from 5+ seconds to under 200ms. It has limitations — you must handle uniqueness restoration since random seeds and in-memory state are shared across restored snapshots.
3. Minimize package size: Remove unnecessary dependencies. Use Lambda Layers for shared libraries. For Node.js, use esbuild or webpack to tree-shake. For Python, strip unused libraries and avoid including test files. A 5MB package cold-starts measurably faster than a 50MB package.
4. Lazy initialization: Do not load everything at init time. If only 10% of invocations need the ML model, load it on first use rather than on every cold start. Cache it in a global variable for subsequent warm invocations.
5. Choose efficient runtimes: Python and Node.js cold-start in 100-300ms. Java cold-starts in 2-5 seconds (without SnapStart). Go and Rust (via custom runtime) cold-start in under 50ms because they compile to native binaries with no runtime overhead.
6. Increase memory allocation: Lambda allocates CPU proportionally to memory. A 128MB function gets a fraction of a vCPU. A 1769MB function gets a full vCPU. Higher CPU speeds up initialization code that is CPU-bound (parsing, compilation). Sometimes doubling memory halves cold start time and the cost stays the same because the function runs half as long.
Production monitoring: Use CloudWatch Metrics with the Init Duration metric to track cold starts. Set alarms for cold start percentage exceeding your SLA threshold. Use X-Ray to visualize the initialization breakdown.
Code Example
# Configure provisioned concurrency for the payment authorization Lambda
aws lambda put-provisioned-concurrency-config \
--function-name payment-authorization-handler \
--qualifier prod \
--provisioned-concurrent-executions 50
# --function-name: the Lambda handling real-time payment authorization
# --qualifier prod: targets the prod alias (required for provisioned concurrency)
# --provisioned-concurrent-executions 50: keeps 50 environments always warm
# This ensures the first 50 concurrent payment requests never hit a cold start
# Create a Lambda function with optimized configuration for fast cold starts
aws lambda create-function \
--function-name order-processing-service \
--runtime python3.12 \
--handler order_processor.handle_order \
--role arn:aws:iam::119283746501:role/order-lambda-execution-role \
--memory-size 1769 \
--timeout 30 \
--architectures arm64 \
--zip-file fileb://./dist/order-processor-optimized.zip \
--environment 'Variables={DB_HOST=order-db.cluster-abc123.us-east-1.rds.amazonaws.com,CACHE_ENDPOINT=order-cache.abc123.0001.use1.cache.amazonaws.com:6379,LOG_LEVEL=INFO}' \
--layers arn:aws:lambda:us-east-1:119283746501:layer:shared-utils-layer:7
# --runtime python3.12: Python has fast cold starts (100-300ms)
# --handler: points to the handle_order function in order_processor.py
# --memory-size 1769: allocates exactly 1 full vCPU for optimal init performance
# --timeout 30: payment processing should complete well within 30 seconds
# --architectures arm64: Graviton2 is 20% cheaper and often faster for Lambda
# --zip-file: optimized package with unnecessary dependencies stripped
# --environment: database and cache endpoints injected as env vars
# --layers: shared utilities in a separate layer to keep the core package small
# Enable SnapStart for a Java-based fraud detection Lambda
aws lambda update-function-configuration \
--function-name fraud-detection-ml-scorer \
--snap-start ApplyOn=PublishedVersions
# --snap-start: captures a Firecracker snapshot after init
# ApplyOn=PublishedVersions: snapshot is taken when you publish a new version
# Reduces Java cold start from ~5 seconds to under 200ms
# Publish a new version to trigger snapshot creation
aws lambda publish-version \
--function-name fraud-detection-ml-scorer \
--description "v2.3.1 - updated fraud model weights with SnapStart"
# Publishing triggers Firecracker to init the function, then snapshot the microVM
# All subsequent invocations restore from this snapshot instead of cold-starting
# Monitor cold start metrics with CloudWatch
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Duration \
--dimensions Name=FunctionName,Value=payment-authorization-handler \
--start-time 2026-06-17T00:00:00Z \
--end-time 2026-06-18T00:00:00Z \
--period 3600 \
--statistics Average p99
# Retrieves average and p99 duration over the last 24 hours in 1-hour buckets
# Compare with InitDuration metric to isolate cold start impact from handler timeInterview Tip
A junior engineer typically mentions cold starts exist and suggests provisioned concurrency as the fix, without understanding the internal phases or the full optimization toolkit. Walk through the four cold start phases: environment provisioning, code download, runtime initialization, and user initialization code. Explain that Firecracker microVMs underpin Lambda, showing you understand the infrastructure layer. Mention SnapStart for Java workloads and the memory-CPU proportionality trick — doubling memory from 128MB to 256MB can halve cold start time because Lambda allocates CPU linearly with memory. Discuss the VPC cold start improvement from Hyperplane ENIs. Senior interviewers want to hear that you have profiled and optimized cold starts in production, not just read the documentation.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────────────┐ │ Lambda Cold Start Phases │ │ │ │ Cold Invocation: │ │ ┌─────────────┐ ┌────────────┐ ┌───────────┐ ┌───────────┐ │ │ │ Provision │→ │ Download │→ │ Start │→ │ Run User │ │ │ │ Firecracker │ │ Code from │ │ Runtime │ │ Init │ │ │ │ microVM │ │ S3 / ECR │ │ (Python/ │ │ Code │ │ │ │ │ │ │ │ Node/JVM)│ │ │ │ │ │ ~100-200ms │ │ ~50-500ms │ │ ~50-5000ms│ │ Variable │ │ │ └─────────────┘ └────────────┘ └───────────┘ └─────┬─────┘ │ │ ↓ │ │ ┌───────────┐ │ │ │ Execute │ │ │ │ Handler │ │ │ └───────────┘ │ │ │ │ Warm Invocation: │ │ ┌───────────────────────────────────────────┐ ┌───────────┐ │ │ │ Skip all init phases — reuse existing │→ │ Execute │ │ │ │ Firecracker microVM environment │ │ Handler │ │ │ └───────────────────────────────────────────┘ └───────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Optimization Impact │ │ │ │ │ │ │ │ Provisioned Concurrency → eliminates ALL cold phases │ │ │ │ SnapStart (Java) → snapshots after init code │ │ │ │ Smaller package → reduces download phase │ │ │ │ More memory (1769MB) → speeds up runtime init │ │ │ │ ARM64 (Graviton2) → faster + 20% cheaper │ │ │ └──────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
AWS secures the infrastructure (physical data centers, hardware, networking, hypervisor) while customers secure everything they put on it (OS patches, application code, data encryption, IAM configuration, network firewall rules). The dividing line shifts depending on the service type: IaaS (EC2) gives you more responsibility, while SaaS (S3) gives you less.
Detailed Answer
The AWS Shared Responsibility Model is the foundational framework that defines who is responsible for what in cloud security. Think of it like renting an apartment: the landlord (AWS) is responsible for the building structure, plumbing, electrical wiring, and foundation. The tenant (you) is responsible for locking the doors, not leaving the stove on, and choosing who gets a key. If the building foundation cracks, that is the landlord's problem. If you leave the front door wide open and get robbed, that is your problem.
AWS's responsibility is security OF the cloud. This encompasses four layers. First, the physical layer: data center buildings with biometric access, 24/7 security guards, CCTV, mantraps, and physical destruction of decommissioned disks. AWS data center locations are undisclosed and undergo SOC1, SOC2, SOC3, ISO 27001, PCI-DSS, and FedRAMP audits. Second, the infrastructure layer: the global network backbone, edge locations, Availability Zones, and Regions. AWS manages the fiber optic connections between AZs (encrypted in transit), the redundant power supplies, and the cooling systems. Third, the compute layer: the Nitro hypervisor that isolates your EC2 instances from other tenants. AWS patched Spectre and Meltdown at the hypervisor level. Fourth, the managed services control plane: when you use RDS, AWS patches the database engine. When you use Lambda, AWS patches the runtime. When you use S3, AWS ensures the storage layer is resilient.
Your responsibility is security IN the cloud, and this is where most breaches happen. The exact scope depends on the service abstraction level. For IaaS services like EC2, you are responsible for: the guest operating system (patching Amazon Linux, Ubuntu, or Windows), the application stack running on it, security group and NACL configurations, host-based firewalls, antivirus, data encryption at rest and in transit, and IAM policies. For container services like ECS or EKS, AWS manages the underlying EC2 instances (if using Fargate) or you manage them (if using EC2 launch type). You always own the container images, application code, and network policies. For abstracted services like S3, Lambda, and DynamoDB, AWS handles much more — you are responsible for bucket policies, function permissions, data classification, encryption key management, and access logging configuration.
1. S3 bucket misconfiguration: Setting bucket policies to public access has caused multiple high-profile data breaches. AWS added S3 Block Public Access as an account-level setting specifically because so many customers misconfigured bucket policies.
2. IAM over-permissioning: Granting AdministratorAccess or using wildcard (*) permissions instead of least privilege. A compromised application with broad IAM permissions can pivot to access any resource in the account.
3. Unpatched EC2 instances: AWS patches the hypervisor, but if you run a vulnerable version of Apache on your EC2 instance and it gets exploited, that is your responsibility. You should use AWS Systems Manager Patch Manager to automate OS patching.
4. Unencrypted data: AWS provides the encryption tools (KMS, ACM, CloudHSM), but you must choose to use them. Leaving an EBS volume or RDS instance unencrypted is your responsibility.
5. Missing logging: AWS provides CloudTrail, VPC Flow Logs, and S3 Access Logs, but you must enable and monitor them. Many organizations enable CloudTrail but never review the logs.
The model also extends to compliance. AWS provides compliance-eligible infrastructure (HIPAA, PCI, FedRAMP), but achieving compliance for your application requires you to configure services correctly and implement the required controls. AWS Artifact provides compliance reports, and services like AWS Config Rules and Security Hub help automate compliance checking. The shared responsibility model is not just a concept — it is the legal and operational framework that determines liability when something goes wrong.
Code Example
# Enable S3 Block Public Access at the account level — customer responsibility
aws s3control put-public-access-block \
--account-id 119283746501 \
--public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
# --account-id: applies to ALL buckets in this AWS account
# BlockPublicAcls: prevents creating new public ACLs on any bucket
# IgnorePublicAcls: overrides existing public ACLs so they have no effect
# BlockPublicPolicy: rejects bucket policies that grant public access
# RestrictPublicBuckets: restricts access to buckets with public policies to AWS principals only
# Enable CloudTrail for security auditing — customer responsibility to configure
aws cloudtrail create-trail \
--name production-security-audit-trail \
--s3-bucket-name prod-cloudtrail-logs-119283746501 \
--is-multi-region-trail \
--enable-log-file-validation \
--kms-key-id arn:aws:kms:us-east-1:119283746501:key/b2c3d4e5-6789-01ab-cdef-EXAMPLE22222 \
--include-global-service-events
# --name: descriptive trail name indicating purpose
# --s3-bucket-name: dedicated bucket for CloudTrail logs with restricted access
# --is-multi-region-trail: captures API calls across ALL regions, not just one
# --enable-log-file-validation: creates digest files to detect log tampering
# --kms-key-id: encrypts log files with a customer-managed KMS key
# --include-global-service-events: captures IAM, STS, and CloudFront global events
# Start the trail to begin recording API calls
aws cloudtrail start-logging \
--name production-security-audit-trail
# Activates the trail — without this command, the trail exists but does not record
# Configure AWS Config to continuously evaluate resource compliance
aws configservice put-config-rule \
--config-rule '{
"ConfigRuleName": "ec2-instances-must-use-imdsv2",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "EC2_IMDSV2_CHECK"
},
"Scope": {
"ComplianceResourceTypes": ["AWS::EC2::Instance"]
},
"Description": "Ensures all EC2 instances require IMDSv2 to prevent SSRF credential theft"
}'
# ConfigRuleName: descriptive name explaining what the rule enforces
# SourceIdentifier: uses AWS-managed rule for IMDSv2 compliance
# ComplianceResourceTypes: evaluates only EC2 instances
# IMDSv2 prevents SSRF attacks from stealing instance metadata credentialsInterview Tip
A junior engineer typically recites the textbook definition — AWS secures the cloud, you secure what is in the cloud — without concrete examples of what that means operationally. The strong answer includes specific examples of customer-side failures: S3 bucket misconfigurations, unpatched EC2 instances, overly permissive IAM policies, and disabled logging. Explain how the responsibility boundary shifts with the service abstraction level — you have more responsibility with EC2 (IaaS) than with Lambda (FaaS). Mention that compliance is also shared: AWS provides HIPAA-eligible infrastructure, but you must configure services correctly to achieve HIPAA compliance for your application. Reference S3 Block Public Access and IMDSv2 as examples of AWS adding safety guardrails because customers frequently failed their side of the model.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────────────┐ │ AWS Shared Responsibility Model │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ CUSTOMER Responsibility │ │ │ │ (Security IN the Cloud) │ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ │ │ Customer │ │ Encryption │ │ IAM Policies │ │ │ │ │ │ Data │ │ at Rest & │ │ & Permissions │ │ │ │ │ │ │ │ in Transit │ │ │ │ │ │ │ └──────────┘ └──────────────┘ └───────────────────┘ │ │ │ │ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ │ │ OS / Host│ │ Network & │ │ Application │ │ │ │ │ │ Patching │ │ Firewall │ │ Configuration │ │ │ │ │ │ (EC2) │ │ Config (SG) │ │ & Code │ │ │ │ │ └──────────┘ └──────────────┘ └───────────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ Responsibility Boundary ─ ─ ─ ─ ─ ─ ─ ─ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ AWS Responsibility │ │ │ │ (Security OF the Cloud) │ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ │ │ Compute │ │ Storage │ │ Networking │ │ │ │ │ │ (Nitro │ │ (Physical │ │ (Global │ │ │ │ │ │ Hypervis)│ │ Disk Mgmt) │ │ Backbone) │ │ │ │ │ └──────────┘ └──────────────┘ └───────────────────┘ │ │ │ │ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ │ │ Physical │ │ Hardware │ │ Data Center │ │ │ │ │ │ Security │ │ Lifecycle │ │ Environmental │ │ │ │ │ │ & Access │ │ & Disposal │ │ Controls │ │ │ │ │ └──────────┘ └──────────────┘ └───────────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Multi-region DR has four tiers: Backup & Restore (cheapest, hours RTO), Pilot Light (core services running, minutes RTO), Warm Standby (scaled-down replica, seconds-to-minutes RTO), and Multi-Site Active/Active (full capacity in both regions, near-zero RTO). Choose based on RPO/RTO requirements and budget.
Detailed Answer
Designing multi-region disaster recovery (DR) on AWS requires understanding two critical metrics: Recovery Time Objective (RTO) — how long you can afford to be down, and Recovery Point Objective (RPO) — how much data you can afford to lose. A payment processing system might need RTO of 30 seconds and RPO of zero. A marketing analytics dashboard might tolerate RTO of 4 hours and RPO of 24 hours. These requirements drive the architecture and cost.
Backup and Restore is the simplest and cheapest. You regularly back up data to another region — S3 Cross-Region Replication for objects, RDS automated backups or snapshots copied cross-region, DynamoDB global tables or point-in-time recovery exports, and AMI copies for EC2 configurations. During a disaster, you spin up infrastructure from these backups. Think of it like having a storage unit in another city with copies of all your important documents — if your house burns down, you can rebuild, but it takes days. RTO is typically 4-24 hours. RPO depends on backup frequency — if you back up hourly, you lose up to an hour of data. Cost is minimal because you only pay for storage in the secondary region.
Pilot Light keeps the minimal core infrastructure running in the DR region — typically just databases with continuous replication. Application servers, load balancers, and other compute resources are pre-configured but not running. Think of it like a gas stove with the pilot light always burning — you can fire up the full flame quickly but it is not heating anything until you turn the knob. During failover, you launch the pre-configured compute resources (from AMIs or Infrastructure as Code), update DNS, and the application comes online. RTO is typically 10-30 minutes. RPO depends on replication lag, usually seconds to minutes with asynchronous replication.
Warm Standby runs a fully functional but scaled-down copy of the production environment in the DR region. All components are active — web servers, application servers, databases — but at a fraction of production capacity. Think of it like having a fully furnished guest house that is smaller than your main house. During failover, you scale up the DR region to production capacity (Auto Scaling groups increase desired count), update DNS routing, and you are operational. RTO is typically 1-10 minutes. RPO is near-zero with synchronous or near-synchronous replication. The ongoing cost is significant — you are running infrastructure 24/7 in two regions.
Multi-Site Active/Active runs full production capacity in two or more regions simultaneously, with traffic split across them using Route 53 latency-based or geolocation routing. Both regions are serving live production traffic at all times. If one region fails, the other absorbs all traffic. Think of it like having two fully staffed offices — if one office has a fire, the other seamlessly handles all customers. RTO is near-zero (just DNS failover time, typically 30-60 seconds). RPO is zero with DynamoDB Global Tables or Aurora Global Database. The cost is roughly double because you run full capacity in both regions.
DNS failover with Route 53 health checks is the traffic steering mechanism. Route 53 monitors endpoints in both regions and automatically shifts traffic when the primary fails. For Active/Active, use latency-based routing. For Active/Passive, use failover routing.
Data replication strategy depends on the database. Aurora Global Database provides cross-region replication with under 1 second lag and supports managed failover. DynamoDB Global Tables provide active-active multi-region replication with eventual consistency. RDS read replicas can be promoted to standalone instances during failover, but this is a manual process.
Infrastructure as Code (Terraform or CloudFormation StackSets) ensures both regions have identical infrastructure definitions. Never manually configure the DR region — drift between regions is the number one cause of DR failures.
Regular DR testing is non-negotiable. Chaos engineering tools like AWS Fault Injection Simulator (FIS) can simulate region-level failures. Many organizations discover during an actual disaster that their DR plan does not work because they never tested it. Netflix's Chaos Monkey philosophy applies here — if you have not tested your failover, you do not have a failover.
Code Example
# Create an Aurora Global Database for cross-region replication
aws rds create-global-cluster \
--global-cluster-identifier payment-ledger-global-db \
--source-db-cluster-identifier arn:aws:rds:us-east-1:119283746501:cluster:payment-ledger-primary \
--engine aurora-postgresql \
--engine-version 15.4
# --global-cluster-identifier: names the global database spanning regions
# --source-db-cluster-identifier: the existing primary cluster in us-east-1
# --engine: Aurora PostgreSQL for strong consistency and global replication
# Replication lag is typically under 1 second across regions
# Add a secondary cluster in the DR region
aws rds create-db-cluster \
--db-cluster-identifier payment-ledger-dr-us-west-2 \
--global-cluster-identifier payment-ledger-global-db \
--engine aurora-postgresql \
--engine-version 15.4 \
--region us-west-2 \
--db-subnet-group-name payment-db-subnet-group-west \
--vpc-security-group-ids sg-0dr1a2b3c4d5e6f7
# --db-cluster-identifier: names the DR cluster with region suffix for clarity
# --global-cluster-identifier: joins this cluster to the global database
# --region us-west-2: creates the replica in the DR region
# --db-subnet-group-name: uses pre-configured subnets in the DR VPC
# --vpc-security-group-ids: security group allowing app servers in DR region
# Configure Route 53 health check for the primary region endpoint
aws route53 create-health-check \
--caller-reference payment-api-primary-hc-2026 \
--health-check-config '{"IPAddress":"203.0.113.50","Port":443,"Type":"HTTPS","ResourcePath":"/health","FullyQualifiedDomainName":"api.payments.example.com","RequestInterval":10,"FailureThreshold":3}'
# IPAddress: the ALB public IP in the primary region
# Port 443: health check over HTTPS for the payment API
# ResourcePath: /health endpoint returns 200 when the service is healthy
# RequestInterval: checks every 10 seconds for rapid failure detection
# FailureThreshold: 3 consecutive failures trigger unhealthy status (30 seconds)
# Create Route 53 failover record for the primary region
aws route53 change-resource-record-sets \
--hosted-zone-id Z0123456789ABCDEF \
--change-batch '{
"Changes": [{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "api.payments.example.com",
"Type": "A",
"SetIdentifier": "primary-us-east-1",
"Failover": "PRIMARY",
"AliasTarget": {
"HostedZoneId": "Z35SXDOTRQ7X7K",
"DNSName": "payment-alb-primary-1234567890.us-east-1.elb.amazonaws.com",
"EvaluateTargetHealth": true
},
"HealthCheckId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE33333"
}
}]
}'
# Name: the public DNS name for the payment API
# Failover PRIMARY: this record is used when the health check passes
# AliasTarget: points to the ALB in us-east-1
# EvaluateTargetHealth: Route 53 also checks the ALB health
# HealthCheckId: links to the health check created aboveInterview Tip
A junior engineer typically jumps straight to Multi-Site Active/Active as the answer without understanding the cost-benefit trade-offs. The strong answer starts by asking about RTO and RPO requirements, then maps those to the appropriate DR tier. Walk through all four strategies (Backup & Restore, Pilot Light, Warm Standby, Multi-Site Active/Active) with specific RTO numbers. Emphasize that the most common DR failure is not a technology problem but a testing problem — organizations that never test their DR plan discover it does not work during the actual disaster. Mention Aurora Global Database for database replication, Route 53 failover routing for traffic steering, and Infrastructure as Code for ensuring region parity. If you mention AWS Fault Injection Simulator for chaos engineering DR tests, you show production maturity.
◈ Architecture Diagram
┌────────────────────────────────────────────────────────────────────┐ │ Multi-Region DR: Warm Standby Architecture │ │ │ │ ┌──────────────────┐ │ │ │ Route 53 │ │ │ │ Failover Policy │ │ │ │ Health Checks │ │ │ └────────┬─────────┘ │ │ PRIMARY │ SECONDARY │ │ ┌─────────────┼──────────────┐ │ │ ↓ ↓ │ │ ┌─────────────────────┐ ┌─────────────────────────┐ │ │ │ us-east-1 (Primary)│ │ us-west-2 (DR Region) │ │ │ │ │ │ │ │ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │ │ │ ALB (Active) │ │ │ │ ALB (Standby)│ │ │ │ │ └───────┬───────┘ │ │ └───────┬───────┘ │ │ │ │ ↓ │ │ ↓ │ │ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │ │ │ ASG: 10 inst │ │ │ │ ASG: 2 inst │ │ │ │ │ │ (full capacity│ │ │ │ (scaled down, │ │ │ │ │ │ production) │ │ │ │ ready to grow)│ │ │ │ │ └───────┬───────┘ │ │ └───────┬───────┘ │ │ │ │ ↓ │ │ ↓ │ │ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │ │ │ Aurora Primary│ │ │ │ Aurora Replica│ │ │ │ │ │ (Read/Write) │──┼────┼→ │ (Read Only) │ │ │ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │ │ Replication lag <1s │ Promotable to R/W │ │ │ └─────────────────────┘ └─────────────────────────┘ │ │ │ │ On Failover: │ │ 1. Route 53 detects primary unhealthy (30s) │ │ 2. DNS shifts traffic to us-west-2 │ │ 3. Aurora replica promoted to primary (< 1 min) │ │ 4. ASG scales from 2 → 10 instances (2-5 min) │ └────────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
S3 achieves 99.999999999% (11 nines) durability by automatically replicating every object across a minimum of three Availability Zones within a region. It uses checksums to detect bit rot, automatic healing to replace corrupted copies, and erasure coding for storage efficiency. The design means you would statistically lose one object out of 10 billion over 10,000 years.
Detailed Answer
Amazon S3's 11 nines (99.999999999%) durability is one of the most remarkable engineering achievements in cloud computing. To put this in perspective, if you stored 10 billion objects in S3, you would statistically expect to lose a single object once every 10,000 years. This durability is achieved through a combination of redundancy, continuous integrity checking, and automatic repair — not through any single technique but through defense in depth.
The foundation is multi-AZ replication. When you upload an object to S3, it is not simply stored on a single disk. S3 synchronously replicates the object across a minimum of three physically separated Availability Zones within the same region before returning a successful PUT response. Each AZ is a separate data center (or cluster of data centers) with independent power, cooling, and networking. This means that an entire data center can be destroyed — fire, flood, power failure — and your data survives intact in the remaining AZs. The key word is synchronous: S3 does not acknowledge your upload until the data is durably stored in all three locations.
S3 uses erasure coding rather than simple replication for storage efficiency. Simple three-way replication would require 3x the storage capacity. Erasure coding is a mathematical technique that splits data into fragments and generates parity fragments, such that any subset of fragments can reconstruct the original data. Think of it like a RAID-6 across data centers — even if multiple fragments are lost, the data can be fully reconstructed. The exact scheme AWS uses is not publicly documented, but it achieves the redundancy of three-copy replication while using significantly less storage capacity. This is critical at S3's scale (hundreds of exabytes).
Continuous integrity verification is the silent hero. S3 constantly reads back stored data and computes checksums (MD5, SHA-256, or CRC32C) to detect bit rot — the gradual degradation of data on storage media. Hard drives and SSDs do not fail catastrophically all at once; individual bits can silently flip due to magnetic interference, cosmic rays, or media degradation. If S3 detects a checksum mismatch (a corrupted fragment), it automatically heals the data by reconstructing the correct fragment from the remaining healthy fragments and writing it to new healthy storage. This process runs continuously across all stored data.
Automatic healing extends beyond bit rot. When an entire storage device fails (which is a daily occurrence at AWS's scale with millions of drives), S3 detects the loss and immediately begins reconstructing the affected fragments on other healthy devices. The reconstruction happens in the background without any impact to read/write operations. The system is designed to heal faster than failures accumulate — the mean time to repair is much shorter than the mean time between failures of remaining copies.
Storage class considerations affect durability differently. S3 Standard, S3 Intelligent-Tiering, S3 Glacier, and S3 Glacier Deep Archive all provide 11 nines of durability because they all replicate across three AZs. S3 One Zone-Infrequent Access stores data in only one AZ and provides 99.999999999% durability within that zone, but you lose everything if that AZ is destroyed. S3 Reduced Redundancy Storage (deprecated) offered lower durability. The storage class affects availability and retrieval time, not durability (except One Zone-IA).
At the hardware level, AWS designs custom storage servers and manages the full hardware lifecycle. Failed drives are detected automatically, decommissioned, and physically destroyed (shredded) to prevent data leakage. New drives are continuously provisioned and data is rebalanced across the fleet.
Networking integrity also matters. Data in transit between AZs during replication is protected against corruption. S3 uses Content-MD5 headers and checksums at the API level — if you enable additional checksum algorithms (SHA-256, CRC32C), S3 verifies your upload matches before storing it.
Durability versus availability is an important distinction. Durability means your data will not be lost. Availability means you can access it right now. S3 Standard offers 99.99% availability (about 53 minutes of downtime per year). A regional outage might make S3 temporarily unavailable (lowering availability), but your data is still safely stored (durability is maintained). Cross-Region Replication (CRR) adds durability against an entire region failure by maintaining copies in a second region.
Code Example
# Upload an object to S3 with additional integrity verification
aws s3api put-object \
--bucket prod-financial-records-vault \
--key transactions/2026/06/daily-ledger-20260618.parquet \
--body ./exports/daily-ledger-20260618.parquet \
--checksum-algorithm SHA256 \
--storage-class STANDARD \
--server-side-encryption aws:kms \
--ssekms-key-id arn:aws:kms:us-east-1:119283746501:key/c3d4e5f6-7890-12ab-cdef-EXAMPLE44444
# --bucket: production bucket for financial records requiring maximum durability
# --key: hierarchical key structure for time-series partitioning
# --checksum-algorithm SHA256: S3 computes and stores SHA256 for end-to-end integrity
# --storage-class STANDARD: three-AZ replication for 11 nines of durability
# --server-side-encryption: encrypts at rest with KMS for compliance
# S3 will not return success until the object is replicated across 3 AZs
# Enable Cross-Region Replication for an additional layer of durability
aws s3api put-bucket-replication \
--bucket prod-financial-records-vault \
--replication-configuration '{
"Role": "arn:aws:iam::119283746501:role/s3-cross-region-replication-role",
"Rules": [{
"ID": "replicate-all-financial-records",
"Status": "Enabled",
"Filter": {"Prefix": "transactions/"},
"Destination": {
"Bucket": "arn:aws:s3:::dr-financial-records-vault-us-west-2",
"StorageClass": "STANDARD",
"EncryptionConfiguration": {
"ReplicaKmsKeyID": "arn:aws:kms:us-west-2:119283746501:key/d4e5f6a7-8901-23bc-def0-EXAMPLE55555"
},
"ReplicationTime": {"Status": "Enabled", "Time": {"Minutes": 15}},
"Metrics": {"Status": "Enabled", "EventThreshold": {"Minutes": 15}}
},
"DeleteMarkerReplication": {"Status": "Disabled"}
}]
}'
# Role: IAM role that S3 assumes to replicate objects to the destination bucket
# Filter Prefix: only replicates objects under the transactions/ prefix
# Destination Bucket: the replica bucket in us-west-2 for region-level DR
# StorageClass STANDARD: maintains 11 nines durability in the replica region
# EncryptionConfiguration: re-encrypts with a KMS key in the destination region
# ReplicationTime: S3 Replication Time Control guarantees 99.99% within 15 min
# DeleteMarkerReplication Disabled: protects against accidental deletion propagation
# Verify object integrity by retrieving checksum attributes
aws s3api get-object-attributes \
--bucket prod-financial-records-vault \
--key transactions/2026/06/daily-ledger-20260618.parquet \
--object-attributes Checksum StorageClass ObjectSize
# Returns the SHA256 checksum, storage class, and size for verification
# Compare this checksum with your local copy to confirm end-to-end integrityInterview Tip
A junior engineer typically says S3 stores copies in multiple data centers and stops there. The strong answer explains the specific mechanisms: synchronous three-AZ replication before acknowledging the PUT, erasure coding instead of simple replication for storage efficiency, continuous checksum verification to detect bit rot, and automatic healing that reconstructs corrupted fragments from healthy ones. Make the distinction between durability (data will not be lost) and availability (data is accessible right now) — many candidates conflate these. Mention that S3 One Zone-IA breaks the three-AZ model, which is why it costs less but has AZ-level risk. If you can explain erasure coding at a high level and why it is more efficient than naive three-copy replication, you demonstrate understanding of distributed storage internals.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────────────┐ │ S3 Durability Architecture (Single Region) │ │ │ │ Client PUT Request │ │ │ │ │ ↓ │ │ ┌──────────────┐ │ │ │ S3 Frontend │ │ │ │ (API Layer) │ │ │ └──────┬───────┘ │ │ │ Synchronous replication before 200 OK │ │ │ │ │ ┌────┼──────────────────────────────────┐ │ │ │ ↓ ↓ ↓ │ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │ AZ-1 │ │ AZ-2 │ │ AZ-3 │ │ │ │ │ │Erasure │ │Erasure │ │Erasure │ │ │ │ │ │Coded │ │Coded │ │Coded │ │ │ │ │ │Fragment│ │Fragment│ │Fragment│ │ │ │ │ └───┬────┘ └───┬────┘ └───┬────┘ │ │ │ │ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ Continuous Integrity Check │ │ │ │ │ │ Checksum verification loop │ │ │ │ │ │ Detects bit rot & corruption │ │ │ │ │ └──────────────┬─────────────────┘ │ │ │ │ ↓ │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ Automatic Healing Engine │ │ │ │ │ │ Reconstructs corrupted │ │ │ │ │ │ fragments from healthy ones │ │ │ │ │ │ Writes to new healthy disk │ │ │ │ │ └────────────────────────────────┘ │ │ │ └──────────────────────────────────────┘ │ │ │ │ Result: 99.999999999% durability │ │ = 1 object lost per 10 billion objects per 10,000 years │ └──────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
ALB (Application Load Balancer) operates at Layer 7 with content-based routing, path/host rules, and WebSocket support. NLB (Network Load Balancer) operates at Layer 4 with ultra-low latency, static IPs, and millions of requests per second. CLB (Classic Load Balancer) is the legacy option with basic Layer 4/7 support and should not be used for new deployments.
Detailed Answer
AWS offers three load balancer types, each designed for different networking layers and use cases. Understanding when to use each is a fundamental networking decision that affects application latency, scalability, and architecture.
The Application Load Balancer (ALB) operates at Layer 7 (HTTP/HTTPS) of the OSI model. It understands the HTTP protocol — it reads headers, paths, query strings, and even request body content. Think of it as an intelligent receptionist who reads your appointment letter, understands which department you need, and directs you to the correct floor. ALB supports content-based routing rules: you can route /api/payments/* to one target group, /api/users/* to another, and host-based routing sends api.payments.example.com to different targets than dashboard.payments.example.com. This enables microservice architectures where a single ALB fronts dozens of services. ALB supports WebSocket connections natively, HTTP/2 for multiplexed streams, gRPC for service-to-service communication, and sticky sessions with cookies. It can perform SSL/TLS termination, authenticate users with OIDC or Amazon Cognito before forwarding requests, and return fixed responses or redirects without hitting any backend. ALB integrates with AWS WAF for web application firewall protection.
The key limitation of ALB is latency. Because it inspects HTTP content, it adds processing overhead. Typical ALB latency is 1-5ms for simple routing, but it can be higher under complex rule evaluation. ALB does not provide static IP addresses — it uses DNS names that resolve to changing IPs, which is problematic for clients that require IP whitelisting. The workaround is placing an NLB in front of an ALB or using AWS Global Accelerator for static anycast IPs.
The Network Load Balancer (NLB) operates at Layer 4 (TCP/UDP/TLS). It does not inspect HTTP content — it routes based on IP protocol data: source/destination IP and port. Think of it as a high-speed highway interchange that routes vehicles based on their lane (port) without stopping to check what is inside the vehicle. NLB is designed for extreme performance: it handles millions of requests per second with ultra-low latency (typically under 100 microseconds for the load balancer itself). It provides static IP addresses (one per AZ), supports Elastic IP assignment, and preserves the client's source IP address by default (no need for X-Forwarded-For headers).
NLB is ideal for: TCP services that are not HTTP (database connections, MQTT for IoT, custom binary protocols), applications requiring static IPs for firewall whitelisting, extreme throughput requirements (gaming, financial trading, video streaming), and as a frontend for AWS PrivateLink to expose services to other VPCs. NLB supports TLS termination but does not inspect HTTP content, so you cannot route based on URL paths or host headers.
A critical difference is health check behavior. ALB health checks are HTTP-based — it sends an HTTP GET to a path like /health and expects a 200 response. NLB health checks can be TCP (just verifying the port is open), HTTP, or HTTPS. TCP health checks are less accurate because a process might accept TCP connections but be unable to serve requests (for example, the JVM is running but the application is stuck in garbage collection).
The Classic Load Balancer (CLB) is the original AWS load balancer from 2009. It provides basic Layer 4 and Layer 7 functionality but lacks the advanced features of ALB and NLB. It does not support content-based routing, WebSockets (properly), HTTP/2, target groups, or multiple listeners with different rules. CLB uses one backend per load balancer, making microservice routing impossible. AWS has been discouraging CLB for years and recommends migrating to ALB or NLB. The only remaining reason to use CLB is EC2-Classic networking, which itself is deprecated.
Cost comparison: ALB charges per hour ($0.0225/hr) plus per Load Balancer Capacity Unit (LCU) based on new connections, active connections, and processed bytes. NLB charges per hour ($0.0225/hr) plus per Network Load Balancer Capacity Unit (NLCU) based on new connections/flows, active connections/flows, and processed bytes. NLB can be more expensive for HTTP workloads because it counts TCP connections, not HTTP requests — a single HTTP/2 connection can carry thousands of requests but NLB sees it as one flow. CLB charges per hour ($0.025/hr) plus per GB of data processed.
In modern architectures, the common pattern is: ALB for HTTP/HTTPS API traffic and web applications, NLB for non-HTTP protocols, PrivateLink services, and ultra-low-latency requirements, and CLB for nothing new — migrate away.
Code Example
# Create an ALB for the payment processing API with path-based routing
aws elbv2 create-load-balancer \
--name payment-api-alb \
--type application \
--scheme internet-facing \
--subnets subnet-0pub1a subnet-0pub1b subnet-0pub1c \
--security-groups sg-0alb1a2b3c4d5e6f \
--ip-address-type ipv4 \
--tags Key=Service,Value=payment-api Key=Environment,Value=production
# --type application: creates an ALB for Layer 7 HTTP/HTTPS routing
# --scheme internet-facing: ALB gets public IPs reachable from the internet
# --subnets: deployed across 3 AZ public subnets for high availability
# --security-groups: allows inbound 443 and outbound to target groups
# Create target groups for different microservices behind the ALB
aws elbv2 create-target-group \
--name payment-processing-tg \
--protocol HTTP \
--port 8080 \
--vpc-id vpc-0a1b2c3d4e5f67890 \
--target-type ip \
--health-check-path /actuator/health \
--health-check-interval-seconds 15 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3
# --name: target group for the payment processing microservice
# --protocol HTTP: backend communication is HTTP (TLS terminated at ALB)
# --port 8080: the port the payment service containers listen on
# --target-type ip: routes to IP addresses (for ECS Fargate or EKS pods)
# --health-check-path: Spring Boot actuator health endpoint
# --health-check-interval-seconds: checks every 15 seconds
# --healthy-threshold-count: 2 consecutive successes to mark healthy
# --unhealthy-threshold-count: 3 consecutive failures to mark unhealthy
# Create path-based routing rules on the ALB listener
aws elbv2 create-rule \
--listener-arn arn:aws:elasticloadbalancing:us-east-1:119283746501:listener/app/payment-api-alb/abc123/def456 \
--priority 10 \
--conditions '[{"Field":"path-pattern","Values":["/api/payments/*"]}]' \
--actions '[{"Type":"forward","TargetGroupArn":"arn:aws:elasticloadbalancing:us-east-1:119283746501:targetgroup/payment-processing-tg/abc123"}]'
# --priority 10: evaluated before lower-priority rules (higher number = lower priority)
# --conditions: matches any request with URL path starting with /api/payments/
# --actions: forwards matching traffic to the payment processing target group
# Create an NLB for the real-time trading data feed (TCP, ultra-low latency)
aws elbv2 create-load-balancer \
--name trading-feed-nlb \
--type network \
--scheme internal \
--subnets subnet-0priv1a subnet-0priv1b \
--tags Key=Service,Value=trading-feed Key=Environment,Value=production
# --type network: creates an NLB for Layer 4 TCP routing
# --scheme internal: not internet-facing, only accessible within the VPC
# NLB provides static IPs per AZ and sub-millisecond latency
# No security groups needed — NLB passes traffic transparently to targets
# Create a TCP target group for the trading data feed
aws elbv2 create-target-group \
--name trading-feed-tcp-tg \
--protocol TCP \
--port 9092 \
--vpc-id vpc-0a1b2c3d4e5f67890 \
--target-type instance \
--health-check-protocol TCP \
--health-check-interval-seconds 10
# --protocol TCP: Layer 4 routing without HTTP inspection
# --port 9092: Kafka broker port for the trading data feed
# --health-check-protocol TCP: verifies port is accepting connections
# TCP health checks have lower overhead than HTTP health checksInterview Tip
A junior engineer typically lists surface-level differences — ALB is Layer 7, NLB is Layer 4 — without explaining the practical implications. The strong answer connects each load balancer type to specific use cases: ALB for microservice path-based routing and WAF integration, NLB for non-HTTP protocols, static IP requirements, and PrivateLink. Explain the latency difference — NLB adds microseconds while ALB adds milliseconds. Mention the ALB static IP limitation and the NLB-in-front-of-ALB workaround for clients that need IP whitelisting. Discuss health check differences: ALB HTTP health checks are more accurate than NLB TCP health checks because a TCP check only verifies port reachability, not application health. If you can explain why NLB preserves source IP natively while ALB requires X-Forwarded-For headers, you demonstrate deep networking understanding.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────────────┐ │ Load Balancer Comparison │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ ALB (Layer 7 - HTTP/HTTPS) │ │ │ │ │ │ │ │ Client → ALB → Inspects HTTP → Routes by Path/Host │ │ │ │ │ │ │ │ ┌─────────┐ /api/payments/* ┌─────────────────┐ │ │ │ │ │ │──────────────────────→│ Payment Service │ │ │ │ │ │ ALB │ /api/users/* ┌─────────────────┐ │ │ │ │ │ │──────────────────────→│ User Service │ │ │ │ │ │ │ /api/orders/* ┌─────────────────┐ │ │ │ │ │ │──────────────────────→│ Order Service │ │ │ │ │ └─────────┘ │ │ │ │ Features: Path routing, Host routing, WAF, WebSocket │ │ │ │ Latency: 1-5ms │ IPs: Dynamic (DNS only) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ NLB (Layer 4 - TCP/UDP) │ │ │ │ │ │ │ │ Client → NLB → Routes by Port → Preserves Source IP │ │ │ │ │ │ │ │ ┌─────────┐ :9092 (TCP) ┌─────────────────┐ │ │ │ │ │ │──────────────────────→│ Kafka Brokers │ │ │ │ │ │ NLB │ :5432 (TCP) ┌─────────────────┐ │ │ │ │ │ │──────────────────────→│ PostgreSQL │ │ │ │ │ └─────────┘ │ │ │ │ Features: Static IPs, PrivateLink, millions RPS │ │ │ │ Latency: <100μs │ IPs: Static (1 per AZ) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ CLB (Legacy - Deprecated) │ │ │ │ │ │ │ │ Basic L4/L7 │ No path routing │ No target groups │ │ │ │ One backend per LB │ Migrate to ALB or NLB │ │ │ └────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
CloudTrail records every API call made in your AWS account — who did what, when, from where, and to which resource. Events are delivered to S3 and optionally CloudWatch Logs. Management events track control plane actions (creating/deleting resources), data events track data plane actions (S3 object reads, Lambda invocations). It is the foundation of AWS security auditing and incident response.
Detailed Answer
AWS CloudTrail is the comprehensive API activity logging service that records every action taken in your AWS account. Think of it as a security camera system for your entire AWS infrastructure — every door opened, every file accessed, every configuration changed is recorded with who did it, when, from what IP address, and whether it succeeded or failed. It is the single most important service for security auditing, compliance, and incident response.
CloudTrail captures three categories of events. Management events (also called control plane events) record operations that modify or manage AWS resources: creating an EC2 instance, modifying an IAM policy, deleting an S3 bucket, changing a security group rule. These are enabled by default on every AWS account. Data events (also called data plane events) record operations on the resources themselves: reading an object from S3, invoking a Lambda function, querying a DynamoDB table. These are not enabled by default because they generate high volume and cost. Insights events use machine learning to detect unusual API activity patterns — like a sudden spike in TerminateInstances calls that might indicate a compromised credential.
The anatomy of a CloudTrail event record contains critical forensic information. Every event includes: the event time (UTC timestamp), the event name (the specific API action like RunInstances), the event source (which AWS service, like ec2.amazonaws.com), the AWS region, the source IP address of the caller, the user identity (which IAM user, role, or AWS service made the call), the request parameters (what was requested), and the response elements (what AWS returned). For assumed roles, the event includes the role ARN, the session name, and the original identity that assumed the role — this is crucial for tracing actions back to human identities in environments that use role-based access.
CloudTrail delivers events to destinations. The primary destination is an S3 bucket, where events are stored as compressed JSON files organized by account, region, and date. A typical path looks like: s3://prod-cloudtrail-logs/AWSLogs/119283746501/CloudTrail/us-east-1/2026/06/18/. Events are delivered within approximately 5-15 minutes of the API call (this is not real-time). For near-real-time analysis, you send events to CloudWatch Logs, where you can create metric filters to trigger alarms on specific API patterns. For advanced querying, CloudTrail Lake provides a managed data lake with SQL-based querying across your event history.
Organization trails are essential for multi-account environments. An organization trail created in the management account automatically captures events from all member accounts in AWS Organizations. This gives your security team a single, centralized view of all API activity across every account. The trail writes to a central S3 bucket in the security account, and member account administrators cannot disable or modify the organization trail.
Unauthorized access detection: Create a CloudWatch metric filter for ConsoleLogin events where the responseElements.ConsoleLogin equals Failure. Trigger an SNS alarm when failed login attempts exceed a threshold, indicating a potential brute force attack.
Root account usage alerting: The root account should never be used for day-to-day operations. Create a metric filter for any event where userIdentity.type equals Root. Any root activity should trigger an immediate P1 alert to the security team.
Security group modification tracking: Monitor AuthorizeSecurityGroupIngress and RevokeSecurityGroupIngress events. Alert when someone opens port 22 (SSH) or port 3389 (RDP) to 0.0.0.0/0, which exposes instances to the entire internet.
IAM policy changes: Track CreatePolicy, AttachRolePolicy, PutRolePolicy, and DeletePolicy events. Any IAM change in production should go through a change management process, so out-of-band IAM modifications indicate either a process violation or compromised credentials.
Log integrity validation is critical. CloudTrail can enable log file validation, which creates a digitally signed digest file every hour. The digest contains SHA-256 hashes of all log files delivered in that period. If an attacker gains access and attempts to modify or delete CloudTrail logs to cover their tracks, the digest chain will break. You can validate the integrity using aws cloudtrail validate-logs. For defense in depth, send CloudTrail logs to a separate security account with a bucket policy that denies DeleteObject — even a compromised admin in the source account cannot delete the logs.
CloudTrail is free for management events (one copy per region). Data events and CloudTrail Lake incur charges based on the number of events. Given that a large organization can generate billions of data events monthly, be selective about which S3 buckets and Lambda functions you enable data events for — focus on sensitive data stores and critical functions rather than enabling data events globally.
Code Example
# Create a multi-region CloudTrail trail with full security configuration
aws cloudtrail create-trail \
--name production-security-audit-trail \
--s3-bucket-name prod-cloudtrail-logs-119283746501 \
--s3-key-prefix organization-trails \
--is-multi-region-trail \
--is-organization-trail \
--include-global-service-events \
--enable-log-file-validation \
--kms-key-id arn:aws:kms:us-east-1:119283746501:key/e5f6a7b8-9012-34cd-ef01-EXAMPLE66666 \
--cloud-watch-logs-log-group-arn arn:aws:logs:us-east-1:119283746501:log-group:cloudtrail-security-logs:* \
--cloud-watch-logs-role-arn arn:aws:iam::119283746501:role/cloudtrail-to-cloudwatch-role
# --name: descriptive trail name for the security audit system
# --s3-bucket-name: centralized bucket in the security account
# --s3-key-prefix: organizes logs under a prefix for multi-trail accounts
# --is-multi-region-trail: captures API calls in ALL regions, not just one
# --is-organization-trail: captures events from all AWS Organization accounts
# --include-global-service-events: captures IAM, STS, CloudFront global events
# --enable-log-file-validation: creates hourly SHA-256 digest files for tamper detection
# --kms-key-id: encrypts log files with a customer-managed KMS key
# --cloud-watch-logs: sends events to CloudWatch for real-time metric filters
# Start recording events on the trail
aws cloudtrail start-logging \
--name production-security-audit-trail
# Without this command the trail is created but not actively recording
# Enable data events for sensitive S3 buckets containing PII
aws cloudtrail put-event-selectors \
--trail-name production-security-audit-trail \
--advanced-event-selectors '[
{
"Name": "Log S3 data events for PII buckets",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Data"]},
{"Field": "resources.type", "Equals": ["AWS::S3::Object"]},
{"Field": "resources.ARN", "StartsWith": [
"arn:aws:s3:::customer-pii-data-store/",
"arn:aws:s3:::prod-financial-records-vault/"
]}
]
},
{
"Name": "Log Lambda invocations for payment functions",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Data"]},
{"Field": "resources.type", "Equals": ["AWS::Lambda::Function"]},
{"Field": "resources.ARN", "StartsWith": [
"arn:aws:lambda:us-east-1:119283746501:function:payment-"
]}
]
}
]'
# advanced-event-selectors: fine-grained control over which data events to log
# Selector 1: logs every S3 GetObject/PutObject on PII and financial buckets
# Selector 2: logs every invocation of payment-related Lambda functions
# Targeted selection keeps costs manageable while covering sensitive resources
# Create a CloudWatch metric filter to alert on root account usage
aws logs put-metric-filter \
--log-group-name cloudtrail-security-logs \
--filter-name root-account-usage-filter \
--filter-pattern '{$.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent"}' \
--metric-transformations '[{"metricName":"RootAccountUsageCount","metricNamespace":"SecurityAuditing","metricValue":"1","defaultValue":0}]'
# --filter-pattern: matches CloudTrail events where the root user took action
# Excludes AWS service events (automated actions by AWS itself)
# --metric-transformations: increments a CloudWatch metric for each root event
# Set a CloudWatch alarm on this metric to page the security team immediately
# Validate CloudTrail log integrity to detect tampering
aws cloudtrail validate-logs \
--trail-arn arn:aws:cloudtrail:us-east-1:119283746501:trail/production-security-audit-trail \
--start-time 2026-06-17T00:00:00Z \
--end-time 2026-06-18T00:00:00Z
# Verifies digest chain and SHA-256 hashes of all log files in the time range
# Reports any modified, deleted, or missing log files
# Critical for incident response to prove log integrity in forensic analysisInterview Tip
A junior engineer typically says CloudTrail logs API calls and stops there. The strong answer differentiates between management events (free, enabled by default) and data events (paid, must be explicitly enabled for specific resources). Walk through a real incident response scenario: you notice unauthorized access, you query CloudTrail for the compromised IAM user's events, you trace the source IP and session, you identify what resources were accessed, and you use log file validation to prove the logs have not been tampered with. Mention organization trails for multi-account environments and the CloudWatch metric filter pattern for root account usage alerts. If you can explain why log file integrity validation matters — because a sophisticated attacker will try to delete CloudTrail logs to cover their tracks — you demonstrate security operations maturity.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────────────┐ │ CloudTrail Architecture │ │ │ │ ┌────────────┐ ┌────────────┐ ┌─────────────┐ │ │ │ IAM User │ │ EC2 Role │ │ Lambda Fn │ │ │ │ API Call │ │ API Call │ │ API Call │ │ │ └─────┬──────┘ └─────┬──────┘ └──────┬──────┘ │ │ └───────────────┼────────────────┘ │ │ ↓ │ │ ┌──────────────────┐ │ │ │ CloudTrail │ │ │ │ Records Every │ │ │ │ API Call │ │ │ └────────┬─────────┘ │ │ │ │ │ ┌────────────┼─────────────┐ │ │ ↓ ↓ ↓ │ │ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │ │ │ S3 Bucket │ │ CloudWatch │ │ CloudTrail │ │ │ │ (Long-term │ │ Logs │ │ Lake │ │ │ │ storage) │ │ (Real-time │ │ (SQL Query │ │ │ │ │ │ alerts) │ │ Engine) │ │ │ └──────┬───────┘ └─────┬──────┘ └──────────────┘ │ │ │ │ │ │ ↓ ↓ │ │ ┌──────────────┐ ┌────────────┐ │ │ │ Log File │ │ Metric │ │ │ │ Validation │ │ Filters │ │ │ │ (SHA-256 │ │ │ │ │ │ digests) │ │ ┌────────┐ │ │ │ └──────────────┘ │ │ Alarm │ │ │ │ │ │ Rules │ │ │ │ │ └───┬────┘ │ │ │ └─────┼──────┘ │ │ ↓ │ │ ┌────────────┐ │ │ │ SNS Topic │ │ │ │ → PagerDuty│ │ │ │ → Security │ │ │ │ Team │ │ │ └────────────┘ │ │ │ │ Event Record Contains: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ eventTime │ eventName │ sourceIP │ userIdentity │ │ │ │ │ awsRegion │ requestParameters │ responseElements │ │ │ │ │ errorCode │ errorMessage │ resources │ eventID │ │ │ │ └──────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Use isolated VPCs per region with Transit Gateway for inter-region connectivity, separate subnets for each PCI scope tier, NACLs + security groups for defense in depth, and AWS PrivateLink for service-to-service communication.
Detailed Answer
- CIDR: Non-overlapping ranges (10.1.0.0/16, 10.2.0.0/16, 10.3.0.0/16) - Public subnet: ALB/NLB only (no direct internet access for apps) - App subnet (private): ECS/EKS workloads, NAT Gateway for outbound - Data subnet (isolated): RDS, ElastiCache — no internet route, no NAT - Management subnet: Bastion hosts, CI/CD agents, monitoring
- Network segmentation: Cardholder Data Environment (CDE) in isolated subnets with strict NACLs - Encryption in transit: TLS 1.2+ everywhere, mTLS between services - Flow logs: VPC Flow Logs to S3 + CloudWatch, retained for 12 months - WAF: AWS WAF on ALB with OWASP top 10 rules - No public IPs on any compute resource in CDE - PrivateLink: Access AWS services (S3, SQS, KMS) via VPC endpoints — no internet path
- Transit Gateway with inter-region peering for VPC-to-VPC routing - Route 53 latency-based routing with health checks for failover - Aurora Global Database for cross-region read replicas with <1s replication lag - DynamoDB Global Tables for multi-region active-active state - S3 Cross-Region Replication for static assets and backups
1. Route 53 → AWS Shield + WAF 2. ALB → Security Groups (allow only 443) 3. App tier → NACLs + SGs (allow only from ALB SG) 4. Data tier → NACLs + SGs (allow only from App SG, specific ports) 5. All tiers → VPC Flow Logs, GuardDuty, CloudTrail
Code Example
# Terraform VPC module for PCI-compliant architecture
module "vpc" {
source = "./modules/pci-vpc"
cidr_block = "10.1.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnets = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
app_subnets = ["10.1.11.0/24", "10.1.12.0/24", "10.1.13.0/24"]
data_subnets = ["10.1.21.0/24", "10.1.22.0/24", "10.1.23.0/24"]
enable_flow_logs = true
flow_log_retention = 365 # PCI requires 12 months
enable_vpc_endpoints = ["s3", "dynamodb", "kms", "logs", "ecr.api"]
}Interview Tip
For compliance-focused design questions, lead with the regulatory requirements (PCI-DSS scoping, encryption, logging retention) and show how the architecture satisfies each one. Interviewers want to see you think about security as a first-class concern, not an afterthought.
💬 Comments
Quick Answer
Combine Spot instances for stateless workloads (40-60% savings), right-size pods with VPA, use Karpenter for bin-packing, Reserved Instances for baseline, and eliminate idle resources.
Detailed Answer
Tier 1: Quick Wins (Week 1-2, ~$50K savings) - Right-size pods: Deploy VPA in recommend mode, identify over-provisioned pods. Most pods request 2-4x what they use. - Delete idle resources: Unused EBS volumes, unattached EIPs, idle load balancers, orphaned snapshots - Scale down non-prod: Dev/staging clusters scale to zero nights/weekends (saves ~60% of non-prod cost) - S3 lifecycle policies: Move infrequently accessed data to S3-IA/Glacier
Tier 2: Compute Optimization (Week 3-6, ~$70K savings) - Spot instances: Run stateless workloads (web servers, workers) on Spot (60-90% discount). Use Karpenter with diversified instance types for availability. - Karpenter over Cluster Autoscaler: Better bin-packing, faster scaling, automatic instance type selection - Graviton instances: ARM-based instances are 20% cheaper with equal or better performance - Reserved Instances / Savings Plans: 1-year commitment for baseline (always-on) capacity. Covers 40-60% of total compute.
Tier 3: Architecture Changes (Week 7-12, ~$30K savings) - Data transfer optimization: Keep services in same AZ where possible, use VPC endpoints instead of NAT Gateway for AWS services - Caching layer: Add Redis/ElastiCache to reduce database load and RDS costs - Serverless for bursty workloads: Move event-driven workloads to Lambda - Container image optimization: Smaller images = faster pulls = less EBS IOPS
- Kubecost or OpenCost for per-team cost allocation - Tag everything, chargeback to teams - Set resource quota per namespace - Weekly cost review in engineering standup
Code Example
# Karpenter NodePool for cost optimization
apiVersion: karpenter.sh/v1beta1
kind: NodePool
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["arm64", "amd64"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["m7g.xlarge", "m6g.xlarge", "c7g.xlarge", "r7g.xlarge"]
nodeClassRef:
name: default
limits:
cpu: "1000"
disruption:
consolidationPolicy: WhenUnderutilizedInterview Tip
Structure your answer in tiers: quick wins, medium-term optimization, and architectural changes. Always quantify expected savings. Mention governance (Kubecost, tagging, chargeback) — that's what separates staff answers from senior answers.
💬 Comments
Quick Answer
Roles define WHO can assume permissions, policies define WHAT actions are allowed, instance profiles attach roles to EC2 instances. For EKS, use IRSA (IAM Roles for Service Accounts) to give each pod its own IAM role.
Detailed Answer
Without IRSA, all pods on a node share the node's IAM role — a single compromised pod can access all permissions. IRSA solves this:
1. Create an IAM OIDC provider for the EKS cluster 2. Create per-service IAM roles with trust policy referencing the Kubernetes ServiceAccount 3. Annotate the Kubernetes ServiceAccount with the IAM role ARN 4. Pod gets its own credentials via projected service account token (not instance metadata)
- One IAM role per microservice, scoped to exactly what it needs - Use IAM policy conditions: aws:RequestedRegion, aws:ResourceTag, kms:ViaService - Deny by default, allow explicitly - Use AWS Access Analyzer to identify unused permissions and tighten policies - Rotate to short-lived credentials (IRSA tokens expire in 12 hours by default) - Block IMDS access from pods to prevent credential theft from node role
Code Example
# IRSA: Terraform for per-service IAM role
resource "aws_iam_role" "order_service" {
name = "eks-order-service"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.eks.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${var.oidc_issuer}:sub" = "system:serviceaccount:orders:order-service"
}
}
}]
})
}
# Kubernetes ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: order-service
namespace: orders
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/eks-order-serviceInterview Tip
IRSA is the answer Amazon interviewers want for EKS security. Show you understand the OIDC trust chain and why it's superior to node-level IAM roles. Mention blocking IMDS from pods as an extra security measure.
💬 Comments
Quick Answer
EKS Pod Identity maps an IAM role to a Kubernetes service account, so pods get short-lived AWS credentials through the EKS Pod Identity Agent instead of static keys or broad node-role credentials. It improves least privilege, auditability, and operational separation between IAM and cluster administration.
Detailed Answer
Putting AWS access keys in Kubernetes secrets creates a rotation and leakage problem. Relying on the node IAM role is also too broad because every pod on that node can potentially reach credentials unless IMDS access is tightly restricted. EKS Pod Identity gives the application a role through its Kubernetes service account, similar in spirit to an EC2 instance profile but scoped to the pod identity.
Operationally, each association maps one IAM role to one service account in one namespace in the cluster. When a pod uses that service account, EKS injects environment variables so supported AWS SDKs and the AWS CLI use the Pod Identity credential flow. The Pod Identity Agent runs as a DaemonSet on Linux EC2 nodes and serves credentials to pods on the same node.
A strong production answer includes the tradeoffs: restrict IMDS so pods cannot fall back to node credentials; remember that containers are not a security boundary; account for eventual consistency after creating associations; configure proxy NO_PROXY for the link-local agent address; and know that Fargate and Windows pods are not supported for EKS Pod Identity.
Code Example
aws eks create-pod-identity-association \ --cluster-name prod-eks \ --namespace payments \ --service-account checkout-api \ --role-arn arn:aws:iam::123456789012:role/payments-checkout-s3 kubectl -n payments set serviceaccount deployment/checkout-api checkout-api # The workload should use the default AWS SDK credential chain.
Interview Tip
Do not stop at 'it avoids secrets.' Mention service-account scoping, IMDS restriction, the DaemonSet agent, eventual consistency, supported node types, and CloudTrail audit value.
◈ Architecture Diagram
Pod service account ↓ association IAM role ↓ short-lived creds AWS API
💬 Comments
Quick Answer
Place both the application tier and the RDS instance inside private subnets with no route to an internet gateway, and rely on the VPC's internal routing for all traffic between them, since two resources in the same VPC (or peered/connected VPCs) never need to leave AWS's internal network to reach each other. For cross-VPC or cross-account architectures, use VPC Peering, Transit Gateway, or PrivateLink instead of public endpoints, and enforce the never-touches-the-internet guarantee at the network layer with security groups and NACLs, not just application-layer TLS.
Detailed Answer
Picture a bank's internal pneumatic tube system connecting the teller counter to the vault in the back room — a physical, private pipe that never routes money-filled canisters out onto the public street just to travel twenty feet. The tube system is entirely self-contained within the building; there's no window it passes to get from point A to point B. A private-subnet-to-private-subnet RDS connection works the same way: as long as both endpoints live inside the VPC's private address space, AWS routes traffic through its own internal network fabric, never touching the public internet, regardless of how far apart the resources are physically.
AWS designed VPCs around this exact separation of concerns: a subnet is 'private' specifically because its route table has no route to an Internet Gateway (IGW), only to the VPC's local CIDR range (and optionally a NAT gateway for one-way outbound internet access, which is irrelevant here since RDS traffic never needs to leave the VPC at all). This means the compliance requirement isn't actually something you have to build — it's the default behavior of AWS networking, as long as you don't accidentally give either side a path to the internet, such as placing the RDS instance in a public subnet or attaching a public IP to the database (which AWS disables by default for RDS, but can be misconfigured).
Internally, when the application server issues a query, DNS resolution for the RDS endpoint returns a private IP address within the VPC's CIDR block (assuming the RDS instance has 'Publicly Accessible' set to No), and the packet is routed entirely through the VPC's internal routing table — AWS's software-defined networking layer handles this at the hypervisor level using its own backbone, never emitting the packet onto the public internet even for cross-Availability-Zone traffic within the same region, since AZs within a VPC are connected by AWS's private inter-AZ links.
At production scale for a banking client specifically, the design typically goes further than 'just use private subnets': security groups on the RDS instance are locked down to allow inbound traffic only from the application tier's specific security group (not a CIDR range, which is more precise and self-documenting), NACLs add a stateless second layer of defense at the subnet boundary, and traffic is encrypted in transit via TLS/SSL enforced through the RDS parameter group (rds.force_ssl = 1) even though it's already physically private, satisfying compliance frameworks that require encryption in transit as a defense-in-depth measure regardless of network isolation. For multi-account setups (common in banking, where prod and the database might sit in separate AWS accounts for blast-radius isolation), the same 'never touches the internet' guarantee is achieved via VPC Peering or Transit Gateway rather than a public endpoint with an IP allowlist, since IP allowlisting a public endpoint technically does route through the internet even if access is restricted.
The non-obvious gotcha: enabling a Multi-AZ RDS deployment or a read replica in a different region can silently reintroduce a compliance violation if the second region's VPC isn't connected via a private path (like a cross-region VPC peering connection or Transit Gateway peering) — engineers sometimes assume Multi-AZ failover traffic is automatically private, but a misconfigured cross-region replica without inter-region private connectivity will replicate over the public internet by default unless explicitly set up otherwise, silently breaking the exact guarantee the compliance team mandated.
Code Example
# Terraform: RDS instance placed in private subnets, never publicly accessible
resource "aws_db_instance" "core_banking" {
identifier = "core-banking-prod"
engine = "postgres"
instance_class = "db.r6g.xlarge"
publicly_accessible = false # never assign a public IP
db_subnet_group_name = aws_db_subnet_group.private.name
vpc_security_group_ids = [aws_security_group.rds_sg.id]
multi_az = true # sync replica, same-region private link
}
# Subnet group built only from private (no-IGW-route) subnets
resource "aws_db_subnet_group" "private" {
name = "core-banking-private-subnets"
subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
}
# Security group: allow Postgres only from the app tier's SG, never a CIDR block
resource "aws_security_group" "rds_sg" {
name = "core-banking-rds-sg"
vpc_id = aws_vpc.banking.id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app_tier_sg.id] # SG-to-SG, not IP-based
}
}
# Enforce TLS in transit as defense-in-depth even though the path is already private
resource "aws_db_parameter_group" "force_ssl" {
family = "postgres15"
parameter {
name = "rds.force_ssl"
value = "1"
}
}Interview Tip
A junior engineer typically says 'put it in a private subnet' and stops there. For a senior or architect role, the interviewer wants the full defense-in-depth picture: why private subnets guarantee no internet path structurally (no IGW route) rather than by policy, why security-group-to-security-group references are preferred over CIDR-based rules for self-documenting least privilege, and why TLS-in-transit is still enforced even on an already-private path to satisfy compliance frameworks that check for encryption independent of network topology. The strongest answers proactively flag the cross-region replica gotcha — that Multi-AZ failover and same-region replicas stay private automatically, but a cross-region read replica needs explicit private connectivity (VPC peering or Transit Gateway) or it will silently violate the exact mandate being designed for.
◈ Architecture Diagram
┌─────────────── VPC (banking-prod) ───────────────┐
│ ┌── Private Subnet A ──┐ ┌── Private Subnet B ──┐│
│ │ App Tier (ECS) │ │ RDS Multi-AZ ││
│ │ SG: app-tier-sg │─►│ SG: rds-sg ││
│ └──────────────────────┘ └───────────────────────┘│
│ no route to IGW | no route to IGW │
└────────────────────────────────────────────────────┘
✗ no Internet Gateway attached to either subnet💬 Comments
Quick Answer
First, deactivate or delete the exposed root access keys immediately via the AWS Console or a break-glass alternate credential, since root keys grant unrestricted account access and every second they remain valid is active exposure — do this before even removing the commit from GitHub, because GitHub history and forks can preserve the secret indefinitely regardless of a force-push. Then rotate every other credential that could have been derived or accessed using root (IAM users, access keys, other secrets stored in the account), audit CloudTrail for any API activity from the exposed key during its exposure window, and only after containment is confirmed do you deal with scrubbing the Git history.
Detailed Answer
Think of this like discovering your house master key is taped to a flyer stapled to a public bulletin board. Your first move isn't to carefully peel the flyer off the board — by the time you're doing that, anyone walking by could have already photographed it, and photographs don't come off the board when you remove the original. Your first move is to change the locks. Everything else — figuring out who saw it, cleaning up the bulletin board — happens after the locks are changed, because that's the only action that actually stops ongoing risk.
AWS designed root account credentials to have complete, unrestricted control over the account with no permission boundary possible — unlike IAM users, root cannot be constrained by an SCP, permission boundary, or resource policy in most cases, which is exactly why AWS's own best practice is to avoid creating root access keys at all and use IAM users or roles for everyday operations. Once root keys are public, the account is compromised in the fullest sense: an attacker could create new IAM users, delete CloudTrail logging, spin up expensive compute in unused regions for cryptomining, or exfiltrate data from every service in the account, all with zero permission checks standing in the way.
Internally, the response needs to happen in strict priority order because each step closes a different door. Deactivating the key (or deleting it and creating a new one, then deactivating the exposed one immediately) via the IAM console under 'My Security Credentials' takes effect within seconds and is the single action that stops any further API calls with that credential — this must happen before anything else, including notifying anyone or investigating scope, because scope investigation doesn't reduce ongoing risk and every minute of delay is a minute of continued exposure. Once the immediate bleeding is stopped, CloudTrail (assuming it wasn't disabled by an attacker, which is itself something to check) provides an audit trail of exactly which API calls were made with the exposed key, letting you distinguish 'exposed for 6 minutes with zero suspicious calls' from 'exposed for 6 minutes with a new IAM user created and attached to AdministratorAccess,' which are two very different incidents requiring very different follow-up.
At production scale, mature incident response here also includes checking AWS Config history and CloudTrail for any new IAM users, roles, or access keys created during the exposure window (since an attacker's first move is usually to create a persistent backdoor credential before the original one gets revoked), reviewing billing alerts and Cost Explorer for anomalous spend in unused regions (a classic sign of cryptomining), and rotating not just root but every other secret potentially reachable from root-level access, since root can read Secrets Manager, Parameter Store, and any KMS-encrypted data in the account. AWS's own Trusted Advisor and GuardDuty often flag exposed credentials automatically within minutes of a public GitHub push, since AWS actively scans public repos for leaked key patterns and may auto-quarantine the key by attaching a deny-all policy — treating this proactive AWS notification as confirmation, not as the primary detection mechanism, is a key operational maturity signal.
The non-obvious gotcha: git commit --amend or a force-push to remove the secret from the latest commit does not remove it from GitHub's history, cached forks, or any CI system that already cloned the repo and may have cached the .git objects — the secret must be treated as permanently public the moment it's pushed, and the entire remediation strategy should assume it has already been scraped by an automated bot, since these exist specifically to harvest credentials from public commits within seconds of a push, often faster than a human notices the mistake.
Code Example
# Step 1 (URGENT — do this first, before anything else): deactivate/delete exposed root keys # Via console: IAM > My Security Credentials > Access Keys > Deactivate # Or via CLI using an alternate valid credential: aws iam update-access-key --access-key-id AKIAEXAMPLEEXPOSED --status Inactive # Step 2: audit exactly what the exposed key did during its exposure window aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXAMPLEEXPOSED \ --start-time 2026-07-04T00:00:00Z # Step 3: check for attacker-created persistence (new users, keys, roles) during that window aws iam list-users --query 'Users[?CreateDate>=`2026-07-04`]' aws iam list-access-keys --user-name <suspect-user> # Step 4: rotate every other credential root could reach aws secretsmanager list-secrets aws ssm describe-parameters --filters Key=Type,Values=SecureString # Step 5: only now, scrub history (does NOT undo prior exposure — for hygiene only) git filter-repo --path .env --invert-paths # Then contact GitHub support to purge cached views/forks if the repo was public
Interview Tip
A junior engineer typically starts with 'delete the commit from GitHub.' For a senior or architect role, the interviewer specifically wants to hear the priority ordering justified: revoke the credential first because it's the only step that stops ongoing risk, and Git history scrubbing happens last because it doesn't undo exposure that's already occurred — bots scrape public commits for credentials within seconds. They're also listening for whether you treat this as a full compromise investigation rather than a cleanup task: checking CloudTrail for attacker-created persistence (new IAM users or keys), checking billing for cryptomining, and verifying CloudTrail logging itself wasn't disabled by the attacker. Mentioning that AWS proactively scans public GitHub for leaked keys and may auto-quarantine the account signals awareness of the real-world tooling involved, not just the textbook steps.
◈ Architecture Diagram
GitHub push (root keys exposed)
│
▼ (bots scrape within seconds — assume compromised NOW)
┌─────────────────┐
│ 1. Revoke key │ ← stops ongoing risk, do this FIRST
└────────┬────────┘
▼
┌─────────────────┐
│ 2. CloudTrail │ ← what did the attacker actually do?
│ audit window │
└────────┬────────┘
▼
┌─────────────────┐
│ 3. Check for new │ ← attacker backdoor persistence
│ IAM users/keys│
└────────┬────────┘
▼
┌─────────────────┐
│ 4. Rotate other │ ← everything root could reach
│ secrets │
└────────┬────────┘
▼
┌─────────────────┐
│ 5. Scrub history │ ← hygiene only, doesn't undo exposure
└─────────────────┘💬 Comments
Quick Answer
An IAM User represents a persistent identity with long-lived credentials (a password and/or access keys) meant for a human or a single fixed application, while an IAM Role has no credentials of its own and is instead assumed temporarily by a trusted identity, issuing short-lived STS tokens that automatically expire — making roles the correct choice for anything that shouldn't hold a permanent secret, including AWS services themselves. A Service-Linked Role is specifically required when an AWS service needs to make API calls into other AWS services on your behalf with a permission set that AWS itself predefines and locks down (you can't broaden it), commonly seen with services like Auto Scaling, Elastic Load Balancing, or AWS Organizations that must reach into EC2 or other services to do their job automatically without you manually wiring up a custom role.
Detailed Answer
Think of an IAM User like an employee badge with a photo and a permanent PIN code, valid indefinitely until someone in security manually deactivates it — if that badge is lost or copied, whoever has it can walk in whenever they want until someone notices and revokes it. An IAM Role is more like a visitor badge that a receptionist hands out after checking ID at the front desk: it works for a few hours, expires automatically, and the visitor never actually possesses a permanent credential — even if someone photographs the visitor badge, it's useless the next day.
AWS designed this distinction specifically around credential lifecycle risk: IAM Users hold durable secrets (access keys that don't expire on their own, passwords protected only by rotation policy), which is exactly the shape of credential that ends up leaked in GitHub repos, laptop backups, or CI logs, because it's meant to be reused indefinitely. IAM Roles solve this by never handing out a durable secret at all — instead, a trusted principal (a user, an EC2 instance, a Lambda function, another AWS account, or an AWS service itself) calls sts:AssumeRole, and STS issues a temporary credential set (access key, secret key, and session token) that's valid for a bounded window, typically 15 minutes to 12 hours, after which it's cryptographically useless even if somehow exposed.
Internally, this is why EC2 instance profiles, ECS task roles, and Lambda execution roles are all IAM Roles rather than Users with embedded keys — an application running on an EC2 instance queries the instance metadata service, which itself talks to STS to fetch fresh temporary credentials automatically, rotating them behind the scenes without the application ever needing to store or manage a secret. This is a foundational shift in the security model: the credential simply cannot be exfiltrated in a way that remains useful for long, which is precisely why 'never use long-lived access keys for applications, only roles' is close to a universal best practice at any AWS shop with a real security program.
A Service-Linked Role exists for a narrower, more specific reason: some AWS services need to reach into other AWS services on your behalf to perform automated background actions — Auto Scaling launching and terminating EC2 instances, Elastic Load Balancing registering targets, RDS creating ENIs for Multi-AZ — and AWS predefines exactly what permissions that service needs, locks the role's trust policy and (often) its permissions policy so you cannot accidentally over-scope or under-scope it, and links the role's lifecycle to the resource using it (deleting the last Auto Scaling group referencing the role allows the role itself to be deleted, whereas a normal role could be deleted while still in active use, breaking things silently). This matters operationally because a hand-rolled custom role for the same purpose risks either being too permissive (a security finding) or missing a permission AWS added in a later API version update, which is exactly the class of bug Service-Linked Roles are designed to eliminate by having AWS itself own and maintain the permission set.
The non-obvious gotcha: you cannot always predict when a service silently requires a Service-Linked Role until you hit a permission error creating a specific resource — for example, enabling AWS Organizations' consolidated billing or certain ELB features auto-creates a service-linked role behind the scenes on first use, and in tightly locked-down accounts where IAM role creation itself is restricted via an SCP, this first-use auto-creation can fail with a cryptic access-denied error that has nothing to do with the actual resource you're trying to create, since the real blocker is the account's inability to create the prerequisite service-linked role at all.
Code Example
# IAM User — long-lived credential, avoid for anything but break-glass/human access
aws iam create-user --user-name jane.doe
aws iam create-access-key --user-name jane.doe # durable key, must be rotated manually
# IAM Role — assumed temporarily, no durable secret ever stored
resource "aws_iam_role" "ecs_task_role" {
name = "payments-api-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" } # ECS assumes this role, not a human
Action = "sts:AssumeRole"
}]
})
}
# Service-Linked Role — AWS-managed, permissions locked, created for you on first use
aws iam create-service-linked-role --aws-service-name autoscaling.amazonaws.com
# Trust policy and permission boundary are owned by AWS, not editable by the account admin
# Verify short-lived nature of assumed-role credentials
aws sts assume-role --role-arn arn:aws:iam::123456789012:role/payments-api-task-role \
--role-session-name debug-session --duration-seconds 900Interview Tip
A junior engineer typically says 'roles are for services, users are for people' and stops there. For a senior or architect role, the interviewer wants the credential-lifecycle reasoning underneath: users hold durable, exfiltratable secrets while roles issue STS tokens that expire on their own, which is the actual security property that matters, not just the naming convention. They're also testing whether you understand Service-Linked Roles as AWS-owned and permission-locked specifically so a service's evolving API surface doesn't require customers to keep a hand-rolled role in sync — and whether you've hit the operational gotcha of a first-use auto-created service-linked role failing in an account with SCPs restricting IAM role creation, which produces a confusing error that looks unrelated to IAM at first glance.
◈ Architecture Diagram
IAM User IAM Role
┌──────────────┐ ┌──────────────┐
│ Access Key │ durable, │ (no secret) │
│ never expires │ must rotate │ AssumeRole │
└──────────────┘ manually └──────┬───────┘
▼
STS temp credentials
(expires in mins-hrs)
Service-Linked Role: AWS owns the permission policy,
lifecycle tied to the resource using it (e.g. Auto Scaling group)💬 Comments
Quick Answer
Standard (frequent access), Standard-IA and One Zone-IA (infrequent access, lower storage cost, retrieval fee), Glacier Instant/Flexible and Glacier Deep Archive (archival, cheapest, higher retrieval latency), and Intelligent-Tiering (auto-moves objects between tiers by access pattern).
Detailed Answer
The trade-off is storage price vs access cost/latency and durability/AZ scope. One Zone-IA is cheaper but stores in a single AZ (less resilient). Use lifecycle policies to transition objects automatically (e.g., Standard to IA after 30 days to Glacier after 90), and Intelligent-Tiering when access patterns are unpredictable so you do not have to tune it.
Interview Tip
Explain the price-vs-access/latency trade-off and mention lifecycle policies to transition objects automatically — that is how the classes are used in practice.
💬 Comments
Quick Answer
ALB (layer 7, HTTP/HTTPS, path/host routing), NLB (layer 4, TCP/UDP, ultra-low latency and static IPs), and the legacy CLB. In Kubernetes/EKS, a Service type LoadBalancer provisions an NLB/ALB, and an Ingress controller (AWS Load Balancer Controller) provisions an ALB.
Detailed Answer
Pick ALB for HTTP routing, TLS termination, and WAF integration; NLB for raw TCP/UDP, extreme performance, or preserving client IP. On EKS the AWS Load Balancer Controller maps Ingress to ALBs (one ALB fronting many services) and Service type LoadBalancer to NLBs — consolidating at the ALB via Ingress is usually cheaper than one LB per service.
Interview Tip
Map ALB=L7/HTTP, NLB=L4/performance, and note Ingress+ALB consolidates many services behind one LB — cost awareness scores well.
💬 Comments
Quick Answer
Match the instance family to the workload profile — general purpose (t3/m-series), compute-optimized (c-series), memory-optimized (r-series) — sized by CPU, memory, network, and expected load, and validated with monitoring and load testing rather than guessed.
Detailed Answer
Start from measured requirements (CPU/memory/IO), pick the family that fits (burstable t3 for spiky low-baseline, m/c/r for steady workloads), and right-size from CloudWatch utilization after go-live. Combine with Auto Scaling for elasticity and Spot/Reserved/Savings Plans for cost. Avoid over-provisioning by iterating on real metrics.
Interview Tip
Show a data-driven process (measure to pick family to right-size from metrics) rather than naming a fixed type like t3.medium.
💬 Comments
Quick Answer
Compute VMs: EC2 / Azure VM / Compute Engine. Object storage: S3 / Blob Storage / Cloud Storage. Managed Kubernetes: EKS / AKS / GKE. Serverless: Lambda / Azure Functions / Cloud Functions.
Detailed Answer
The big-three offer near-equivalent primitives under different names. Compute: EC2 ≈ Azure VM ≈ Compute Engine. Object storage: S3 ≈ Blob ≈ Cloud Storage. Block: EBS ≈ Managed Disks ≈ Persistent Disk. SQL: RDS ≈ Azure SQL ≈ Cloud SQL. K8s: EKS ≈ AKS ≈ GKE. Registry: ECR ≈ ACR ≈ Artifact Registry. Knowing the mapping lets you translate architectures across clouds in interviews.
Interview Tip
Have the EC2/VM/Compute Engine, S3/Blob/Cloud Storage, EKS/AKS/GKE triads memorized — they come up constantly.
💬 Comments
Quick Answer
Identity: IAM / Entra ID (Azure AD) / Cloud IAM. Virtual network: VPC / VNet / VPC Network. Monitoring: CloudWatch / Azure Monitor / Cloud Monitoring. DNS: Route 53 / Azure DNS / Cloud DNS. Secrets: Secrets Manager / Key Vault / Secret Manager.
Detailed Answer
Identity & access: AWS IAM ≈ Microsoft Entra ID (formerly Azure AD) ≈ GCP Cloud IAM. Networking: VPC ≈ VNet ≈ VPC Network, with load balancers ELB ≈ Azure LB ≈ Cloud Load Balancing. Observability: CloudWatch ≈ Azure Monitor ≈ Cloud Monitoring (+ Log Analytics / Cloud Logging). CI/CD: CodePipeline ≈ Azure DevOps ≈ Cloud Build. These mappings are the most common cloud-round interview questions.
Interview Tip
Note Azure AD is now Entra ID — using the current name signals you follow the ecosystem.
💬 Comments
Context
A health-tech startup ran 63 AWS workloads and had grown faster than its architecture review process. Two single-AZ dependencies caused production incidents in the same month.
Problem
Teams knew they had reliability debt, but every service claimed to be urgent. Without a shared framework, platform engineers argued from anecdotes and service owners optimized their own roadmaps.
Solution
They mapped tier-1 user journeys, reviewed workloads against the AWS Reliability Pillar, and turned findings into a ranked remediation backlog. Multi-AZ databases, backup restore tests, dependency timeouts, and autoscaling limits were addressed before lower-risk cleanup.
Commands
aws wellarchitected list-workloads # Inventory workloads under review
aws rds describe-db-instances # Find single-AZ databases
aws autoscaling describe-auto-scaling-groups # Check capacity and zone distribution
Outcome
The team closed 17 high-risk reliability findings in six weeks. Two quarterly game days validated database restore and AZ impairment procedures.
Lessons Learned
Reliability reviews work best when tied to user journeys and incident history, not when treated as a compliance questionnaire.
◈ Architecture Diagram
┌──────────┐
│ Trigger │
└────┬─────┘
↓
┌──────────┐
│ UseCase │
└────┬─────┘
↓
┌──────────┐
│ Verify │
└──────────┘💬 Comments
Context
A product organization runs dozens of Kubernetes workloads on Amazon EKS. The platform team manages node groups, Karpenter settings, load balancing add-ons, VPC networking, storage classes, OS patching, and GPU capacity. Application teams want faster cluster creation and less time spent on infrastructure mechanics.
Problem
The existing platform is flexible but operationally expensive. Node provisioning, add-on upgrades, GPU capacity selection, and networking defaults require specialist review for every new environment. During incidents, teams must distinguish app failures from node, add-on, and scaling failures.
Solution
Evaluate EKS Auto Mode for new clusters and selected existing clusters. Use it where AWS-managed automation for compute, storage, networking, load balancing, DNS, GPU support, operating system patching, and cost optimization matches the workload control requirements. Keep custom clusters for workloads that need unusual node or networking behavior. Write migration checks for node pool behavior, storage class assumptions, network policy behavior, and load balancer annotations before moving production traffic.
Commands
eksctl create cluster --name payments-auto --enable-auto-mode
aws eks describe-cluster --name payments-auto
kubectl get nodes -o wide
kubectl get storageclass
kubectl get ingress,svc -A
Outcome
The platform team uses EKS Auto Mode as the default for standard service clusters and reserves custom EKS designs for special cases. New environment lead time drops, while operational reviews shift from node plumbing to workload readiness, security boundaries, and SLOs.
Lessons Learned
Managed automation changes the operating model; it does not remove ownership. Teams still need workload observability, deployment safety, cost visibility, IAM controls, and an explicit exception path for workloads that need deeper infrastructure control.
◈ Architecture Diagram
App needs ↓ EKS Auto Mode fit check ↓ Managed infra defaults ↓ Workload readiness review
💬 Comments
Context
A digital banking startup with 22 engineers ran their core ledger service on ECS Fargate across 3 environments (dev, staging, prod), serving roughly 400 req/sec at peak in production, with a single Multi-AZ PostgreSQL RDS instance backing the ledger. An upcoming SOC 2 Type II and banking-partner compliance audit required documented proof that production application-to-database traffic never traverses the public internet, and the existing architecture had never been explicitly reviewed against that specific requirement.
Problem
During the audit prep, the security team discovered that while the RDS instance itself had publicly_accessible set to false, the ECS Fargate tasks were running in subnets originally created as 'public' during an early prototyping phase 14 months prior, since Fargate tasks needed outbound internet access at the time to pull dependencies before a NAT gateway was ever provisioned. Traffic between the app and RDS was technically still routed through the VPC's internal fabric rather than the public internet — both resources were still within the same VPC — but the app tier's subnet having a route to an Internet Gateway meant the compliance team could not produce clean network-topology documentation proving isolation as a structural guarantee, since a public subnet is, by AWS's own definition, one with a default route out to the internet, and auditors specifically flag this regardless of whether any given traffic flow actually uses that route. The team's first instinct — just add a security group rule restricting RDS inbound to the app tier's IP range — was rejected by the auditor as insufient, since IP-range security group rules don't change the underlying subnet's public/private classification and don't structurally prevent the app tier itself from being reached from the internet if a misconfiguration ever exposed a port.
Solution
The team re-architected the network layer in three stages, treating it as a zero-downtime migration since the ledger service could not tolerate any connection interruption during business hours. First, they created new private subnets (no route to an Internet Gateway) in each existing Availability Zone, alongside a NAT Gateway in the existing public subnets to provide the outbound-only internet access Fargate tasks still needed for pulling images and calling third-party APIs, ensuring private subnets could reach the internet outbound without being reachable inbound. Second, they updated the ECS service's network configuration to launch new tasks into the private subnets while leaving the RDS subnet group unchanged (it was already private), then let ECS's standard rolling deployment gradually replace old public-subnet tasks with new private-subnet tasks behind the existing internal Application Load Balancer, which itself was moved to a separate internal-only ALB in front of an external-facing ALB used only for public API endpoints unrelated to the ledger's direct database access path. Third, they replaced the RDS security group's CIDR-based ingress rule with a security-group-reference rule allowing traffic only from the new app-tier security group, removing IP-range-based rules entirely so the network topology diagram had a single, auditable statement: the database's security group permits ingress only from a named security group, not any address range, and neither the app tier's nor the database's subnet has a route to an Internet Gateway.
Commands
aws ec2 create-subnet --vpc-id vpc-0abc123 --cidr-block 10.0.20.0/24 --availability-zone us-east-1a # new private subnet, no IGW route
aws ec2 create-route-table --vpc-id vpc-0abc123 # separate route table for private subnets, default route via NAT only
aws ecs update-service --cluster ledger-prod --service ledger-api --network-configuration "awsvpcConfiguration={subnets=[subnet-priv-a,subnet-priv-b],securityGroups=[sg-app-tier],assignPublicIp=DISABLED}"aws ec2 revoke-security-group-ingress --group-id sg-rds --protocol tcp --port 5432 --cidr 10.0.0.0/16 # remove old CIDR-based rule
aws ec2 authorize-security-group-ingress --group-id sg-rds --protocol tcp --port 5432 --source-group sg-app-tier # SG-to-SG only
Outcome
The ledger service completed the migration with zero connection-level downtime, verified by continuous synthetic health checks against the ALB throughout the cutover window, and application error rates showed no measurable change during the transition. The compliance audit passed this specific control on first review, with the auditor explicitly noting the network topology diagram now showed structural isolation (no IGW route on either tier's subnet) rather than relying solely on security group policy, which they considered a materially stronger control. NAT Gateway costs increased by roughly $95/month across the 3 environments, which the team accepted as the cost of a passing audit finding versus a remediation flag.
Lessons Learned
Security group rules alone, even tightly scoped ones, don't satisfy auditors looking for structural network isolation — a subnet's route table (specifically, the absence of a route to an Internet Gateway) is the property that actually proves a resource cannot be reached from or reach the public internet, and it's worth documenting that distinction explicitly rather than assuming 'private-ish' is good enough. Prototyping decisions made under early time pressure (like launching Fargate tasks in public subnets to avoid provisioning a NAT gateway) tend to silently persist well past the point where they matter, since nothing breaks functionally — the gap only surfaces when someone specifically audits against a compliance framework's exact wording.
◈ Architecture Diagram
Before: App tier in PUBLIC subnet (route to IGW exists) App(public subnet) ──SG rule──► RDS(private subnet) ✗ auditor flag: app subnet has IGW route, even if unused After: App tier moved to PRIVATE subnet + NAT for outbound only App(private subnet)──SG-to-SG──►RDS(private subnet) NAT Gateway (public subnet) ← outbound-only internet for app ✓ neither tier has an inbound path from the internet
💬 Comments
Symptom
3:21 AM: API 5xx alarm paged the platform rotation, but the alarm description contained no service owner, dashboard, runbook, or recent deploy link.
Root Cause
The workload had alarms, but they were infrastructure-centric and not operationally ready. The first responder spent twenty minutes discovering ownership and impact before any mitigation happened. Operational excellence is not just detecting failure; it is preparing people and systems to respond efficiently. The underlying issue was a combination of factors that individually seemed harmless but together created a cascading failure. The monitoring that should have caught this early was either missing or configured with thresholds too high to trigger before user impact. The on-call engineer initially pursued the wrong hypothesis because the symptoms resembled a different, more common failure mode. By the time the real root cause was identified, the blast radius had expanded beyond the original service to affect downstream dependencies. This incident exposed a gap in the team's runbooks and highlighted the need for better correlation between metrics, logs, and traces during diagnosis.
Diagnosis Steps
Solution
Update alarm descriptions with runbooks, dashboards, owners, and escalation paths. Add service and team tags to resources. Review alarm actionability in the next operational readiness review.
Commands
aws cloudwatch put-metric-alarm --alarm-name payments-5xx-high --alarm-description "Owner: payments-oncall; Runbook: https://internal/runbooks/payments-5xx"
Prevention
Make runbook and owner fields mandatory for paging alarms. Audit alarms monthly for actionability. Connect deployment events to dashboards and incident timelines.
◈ Architecture Diagram
┌──────────┐
│ Learn │
└────┬─────┘
↓
┌──────────┐
│ Incident │
└────┬─────┘
↓
┌──────────┐
│ Operate │
└──────────┘💬 Comments
Symptom
On October 20, 2025, many internet services reported failures or degraded behavior as AWS services in US-East-1 experienced increased errors and latency. Public reports linked the incident to DynamoDB DNS endpoint resolution problems that affected dependent AWS services and customer applications.
Error Message
DNS resolution failures, increased AWS API errors, elevated latency, dependency timeouts
Root Cause
The incident was reported as a DynamoDB DNS management or endpoint resolution failure in US-East-1. Because many AWS services and customer systems depend on DynamoDB or service APIs in that region, a DNS-level failure propagated into broader dependency failures.
Diagnosis Steps
Solution
Route user traffic away from affected regional dependencies where the application supports it. Fail over read paths to replicated data stores, pause non-critical batch jobs, reduce retry pressure, and communicate dependency impact clearly. After recovery, remove hidden single-region dependencies and test regional isolation.
Commands
aws health describe-events --filter services=DYNAMODB
dig dynamodb.us-east-1.amazonaws.com
aws dynamodb list-tables --region us-east-1
aws dynamodb list-tables --region us-west-2
Check application metrics for retry count, timeout count, and regional error split
Prevention
Avoid depending on one AWS region for tier-1 serving paths. Use multi-region architecture for critical data, bounded retries with jitter, cached fallbacks where safe, and dependency dashboards that show AWS region and service-level health.
◈ Architecture Diagram
DynamoDB DNS issue -> endpoint failures -> AWS service errors -> customer app outages
💬 Comments
Symptom
3:14 AM — an AWS Billing Anomaly alert fired for a 6-person startup's account, flagging spend 40x above the account's typical daily average. Simultaneously, a GuardDuty finding titled 'UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration' appeared, and a separate automated email from AWS Trust & Safety warned that credentials matching the account had been detected in a public GitHub repository roughly 5 hours earlier.
Error Message
GuardDuty Finding: UnauthorizedAccess:IAMUser/MaliciousIPCaller.Custom — API calls made using credential AKIA****EXPOSED from an IP address associated with known cryptocurrency mining infrastructure, across 9 previously unused AWS regions.
Root Cause
A developer had committed a local .env file containing the AWS account's root access keys — created months earlier for a one-off billing API script and never deleted — to a personal GitHub repository that was set to public by default when the developer created it, rather than the org's usual private-by-default repos. Automated credential-scraping bots, which continuously scan GitHub's public commit firehose for patterns matching AWS access key formats, detected and validated the leaked key within approximately 11 minutes of the push, well before AWS's own proactive leaked-credential detection (which typically emails the account owner) had a chance to fire. Because it was a root key with zero permission restrictions, the attacker immediately launched EC2 GPU instances across 9 regions the account had never previously used, specifically choosing unused regions because the team had no CloudWatch billing alarms or Config rules covering regions outside their normal us-east-1 footprint, so the spend accumulated for nearly 4 hours before anyone noticed anything beyond the eventual anomaly alert.
Diagnosis Steps
Solution
Deactivated and deleted the exposed root access key within minutes of detection, then immediately terminated every unauthorized EC2 instance found across all 9 affected regions using the region enumeration script, since each running GPU instance was actively accruing cost every additional minute. Filed an AWS Support case citing the compromise and requested a billing adjustment under AWS's compromised-account forgiveness policy, providing the GuardDuty finding IDs and CloudTrail evidence as documentation — AWS credited back the majority of the fraudulent charges after review, though this process took roughly 2 weeks and required demonstrating the remediation steps taken. Rotated every other secret in the account reachable via root (IAM user keys, Secrets Manager entries, RDS master passwords) as a precaution, since root-level access could have read any of them even though CloudTrail showed no evidence it had.
Commands
aws iam update-access-key --access-key-id AKIA****EXPOSED --status Inactive
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do aws ec2 describe-instances --region $r --filters Name=instance-state-name,Values=running --query 'Reservations[].Instances[].InstanceId' --output text | xargs -I{} aws ec2 terminate-instances --region $r --instance-ids {}; doneaws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIA****EXPOSED
aws support create-case --subject 'Compromised account - fraudulent charges' --category-code billing --service-code account-management
Prevention
Never create root access keys at all — the account's root user should have MFA enabled and no access keys, with all programmatic access going through IAM roles or scoped IAM users instead, eliminating this entire failure class structurally. Set AWS Budgets and CloudWatch billing alarms covering total account spend across all regions, not just the home region, so an anomalous spike in an unused region is caught within an hour rather than discovered via a 3am anomaly digest. Enable an org-wide SCP that denies EC2 launches in regions the account doesn't legitimately use, and require git-secrets or gitleaks as a mandatory pre-commit hook across every repo, including personal ones connected to the org's GitHub, to catch credential patterns before they're ever pushed.
◈ Architecture Diagram
GitHub push (root key in .env)
│ (scraped by bot in ~11 min)
▼
Attacker launches GPU EC2 across 9 unused regions
│ (no cross-region billing alarm)
▼
4 hrs undetected → $38k spend
│
▼
GuardDuty + Billing Anomaly alert fires
│
▼
Revoke key → terminate all regions → AWS billing dispute💬 Comments