This commit is contained in:
Toby
2026-03-24 18:02:50 -04:00
parent 2d209b9258
commit ba9e489584
111 changed files with 48382 additions and 3099 deletions
@@ -9,49 +9,306 @@ metadata:
# AWS Cost Optimization
Apply practical FinOps controls without sacrificing reliability.
Apply practical FinOps controls to reduce AWS spend without sacrificing reliability or performance.
## When to Use This Skill
Use this skill when:
- Monthly AWS cost spikes unexpectedly
- Preparing cost reviews with engineering and finance
- Rightsizing EC2, RDS, and EKS workloads
- Choosing Savings Plans or Reserved Instances
- Monthly AWS bill spikes unexpectedly or exceeds budget thresholds
- Preparing cost reviews with engineering and finance teams
- Rightsizing EC2, RDS, EKS, or Lambda workloads after load testing
- Choosing between Savings Plans, Reserved Instances, or on-demand pricing
- Setting up automated budget alerts and anomaly detection
- Cleaning up unused resources (unattached EBS, idle load balancers, old snapshots)
- Optimizing data transfer costs across regions and AZs
## Prerequisites
- AWS CLI v2 installed and configured (`aws configure`)
- IAM permissions: `ce:*`, `budgets:*`, `ec2:Describe*`, `cloudwatch:PutMetricAlarm`, `s3:PutLifecycleConfiguration`
- Cost Explorer enabled in the AWS billing console (takes 24 hours to populate)
- Cost allocation tags activated in the Billing console
## Cost Review Workflow
1. Tag resources by team, service, and environment.
2. Use Cost Explorer and CUR to identify top spend drivers.
3. Rightsize underutilized compute and storage.
4. Apply commitment discounts for stable baseline usage.
5. Set budgets, anomaly alerts, and KPI reporting.
1. Tag every resource by team, service, environment, and cost center.
2. Enable Cost Explorer and activate Cost and Usage Reports (CUR) to S3.
3. Identify top spend drivers by service, account, and tag.
4. Rightsize underutilized compute and storage based on CloudWatch metrics.
5. Apply commitment discounts (Savings Plans or RIs) for stable baseline usage.
6. Set budgets, anomaly alerts, and build KPI dashboards.
7. Review monthly and iterate.
## High-Impact Actions
- Move bursty non-prod compute to Spot where safe.
- Configure S3 lifecycle rules for infrequent access and archive tiers.
- Reduce NAT Gateway and inter-AZ data transfer surprises.
- Schedule dev/test shutdown windows outside business hours.
- Tune log retention (CloudWatch, OpenSearch) to policy requirements.
## Useful Commands
## Cost Explorer CLI Commands
```bash
# Cost Explorer rightsizing recommendations (example)
# Get cost and usage for the last 30 days grouped by service
aws ce get-cost-and-usage \
--time-period Start=2026-02-01,End=2026-03-01 \
--granularity MONTHLY \
--metrics "BlendedCost" "UnblendedCost" "UsageQuantity" \
--group-by Type=DIMENSION,Key=SERVICE
# Get cost forecast for the next 30 days
aws ce get-cost-forecast \
--time-period Start=2026-03-24,End=2026-04-24 \
--metric UNBLENDED_COST \
--granularity MONTHLY
# Get cost grouped by a specific tag (e.g., team)
aws ce get-cost-and-usage \
--time-period Start=2026-02-01,End=2026-03-01 \
--granularity MONTHLY \
--metrics "UnblendedCost" \
--group-by Type=TAG,Key=team
# Get rightsizing recommendations for EC2
aws ce get-rightsizing-recommendation \
--service "AmazonEC2" \
--configuration file://rightsizing-config.json
--configuration '{"RecommendationTarget":"SAME_INSTANCE_FAMILY","BenefitsConsidered":true}'
# List unattached EBS volumes
aws ec2 describe-volumes --filters Name=status,Values=available
# Get Savings Plans purchase recommendation
aws ce get-savings-plans-purchase-recommendation \
--savings-plans-type COMPUTE_SP \
--term-in-years ONE_YEAR \
--payment-option NO_UPFRONT \
--lookback-period-in-days SIXTY_DAYS
# Retrieve budget alerts
aws budgets describe-budgets --account-id 123456789012
# Get Savings Plans utilization
aws ce get-savings-plans-utilization \
--time-period Start=2026-02-01,End=2026-03-01 \
--granularity MONTHLY
# Get Reserved Instance utilization
aws ce get-reservation-utilization \
--time-period Start=2026-02-01,End=2026-03-01 \
--granularity MONTHLY
```
## Budget Alerts
```bash
# Create a monthly cost budget with email alert at 80% and 100%
aws budgets create-budget \
--account-id 123456789012 \
--budget '{
"BudgetName": "monthly-total",
"BudgetLimit": {"Amount": "5000", "Unit": "USD"},
"TimeUnit": "MONTHLY",
"BudgetType": "COST",
"CostFilters": {},
"CostTypes": {
"IncludeTax": true,
"IncludeSubscription": true,
"UseBlended": false
}
}' \
--notifications-with-subscribers '[
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [{"SubscriptionType": "EMAIL", "Address": "finops@example.com"}]
},
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 100,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [{"SubscriptionType": "EMAIL", "Address": "finops@example.com"}]
}
]'
# List all budgets
aws budgets describe-budgets --account-id 123456789012
# Enable Cost Anomaly Detection monitor for all services
aws ce create-anomaly-monitor \
--anomaly-monitor '{
"MonitorName": "all-services",
"MonitorType": "DIMENSIONAL",
"MonitorDimension": "SERVICE"
}'
# Create anomaly subscription (alert when impact > $50)
aws ce create-anomaly-subscription \
--anomaly-subscription '{
"SubscriptionName": "cost-alerts",
"MonitorArnList": ["arn:aws:ce::123456789012:anomalymonitor/monitor-id"],
"Subscribers": [{"Type": "EMAIL", "Address": "finops@example.com"}],
"Threshold": 50,
"Frequency": "DAILY"
}'
```
## CloudWatch Cost Alarm
```bash
# Create alarm for estimated charges exceeding $4000
aws cloudwatch put-metric-alarm \
--alarm-name "billing-alarm-4000" \
--alarm-description "Alert when estimated charges exceed $4000" \
--metric-name EstimatedCharges \
--namespace AWS/Billing \
--statistic Maximum \
--period 21600 \
--threshold 4000 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--dimensions Name=Currency,Value=USD \
--alarm-actions "arn:aws:sns:us-east-1:123456789012:billing-alerts" \
--treat-missing-data notBreaching
```
## Find and Clean Unused Resources
```bash
# List unattached EBS volumes (wasted storage spend)
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query "Volumes[].{ID:VolumeId,Size:Size,Created:CreateTime}" \
--output table
# Find old EBS snapshots (older than 90 days)
aws ec2 describe-snapshots \
--owner-ids self \
--query "Snapshots[?StartTime<='2025-12-24'].{ID:SnapshotId,Size:VolumeSize,Date:StartTime}" \
--output table
# List unused Elastic IPs (charged when not associated)
aws ec2 describe-addresses \
--query "Addresses[?AssociationId==null].{IP:PublicIp,AllocId:AllocationId}" \
--output table
# Find idle load balancers (zero healthy targets)
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-tg/abc123
# List RDS instances and their utilization
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name CPUUtilization \
--dimensions Name=DBInstanceIdentifier,Value=mydb \
--start-time 2026-03-17T00:00:00Z \
--end-time 2026-03-24T00:00:00Z \
--period 86400 \
--statistics Average
```
## S3 Lifecycle Cost Optimization
```bash
# Apply tiered lifecycle policy to reduce storage costs
aws s3api put-bucket-lifecycle-configuration \
--bucket my-data-bucket \
--lifecycle-configuration '{
"Rules": [
{
"ID": "TierDownOldData",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER"},
{"Days": 365, "StorageClass": "DEEP_ARCHIVE"}
],
"NoncurrentVersionTransitions": [
{"NoncurrentDays": 30, "StorageClass": "GLACIER"}
],
"NoncurrentVersionExpiration": {"NoncurrentDays": 90}
},
{
"ID": "CleanupIncompleteUploads",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7}
}
]
}'
```
## Terraform Budget and Alarm Example
```hcl
resource "aws_budgets_budget" "monthly" {
name = "monthly-total"
budget_type = "COST"
limit_amount = "5000"
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = ["finops@example.com"]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = ["finops@example.com"]
}
}
resource "aws_cloudwatch_metric_alarm" "billing" {
alarm_name = "billing-alarm-4000"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "EstimatedCharges"
namespace = "AWS/Billing"
period = 21600
statistic = "Maximum"
threshold = 4000
alarm_description = "Billing exceeds $4000"
alarm_actions = [aws_sns_topic.billing_alerts.arn]
dimensions = {
Currency = "USD"
}
}
```
## Scheduling Non-Production Shutdowns
```bash
# Stop all dev instances tagged Environment=dev (run via EventBridge + Lambda)
aws ec2 describe-instances \
--filters "Name=tag:Environment,Values=dev" "Name=instance-state-name,Values=running" \
--query "Reservations[].Instances[].InstanceId" \
--output text | xargs -n1 aws ec2 stop-instances --instance-ids
# Scale down dev ECS services to zero at night
aws ecs update-service \
--cluster dev-cluster \
--service dev-api \
--desired-count 0
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Cost Explorer returns empty data | CE not enabled or < 24h old | Enable in Billing console, wait 24h |
| Budget alert not firing | SNS subscription not confirmed | Check email and confirm subscription |
| Rightsizing shows no recommendations | Not enough usage data | Wait 14 days for sufficient metrics |
| Savings Plans utilization low | Over-purchased or workload changed | Review and adjust SP coverage |
| Unattached EBS not showing | Wrong region queried | Loop through all active regions |
| Billing alarm never triggers | Billing metrics only in us-east-1 | Create alarm in us-east-1 region |
| CUR data missing in S3 | Report not configured or bucket policy wrong | Verify CUR setup in Billing console |
| Tag-based cost allocation empty | Tags not activated | Activate cost allocation tags in Billing |
## Related Skills
- [aws-ec2](../aws-ec2/) - EC2 operations and sizing
- [aws-s3](../aws-s3/) - S3 storage and lifecycle controls
- [terraform-aws](../terraform-aws/) - Codifying cost guardrails
- [aws-ec2](../aws-ec2/) - EC2 operations, sizing, and Spot instances
- [aws-s3](../aws-s3/) - S3 storage classes and lifecycle controls
- [aws-rds](../aws-rds/) - RDS instance sizing and reserved instances
- [aws-lambda](../aws-lambda/) - Lambda pricing and concurrency tuning
- [terraform-aws](../terraform-aws/) - Codifying cost guardrails in IaC
+373 -44
View File
@@ -9,73 +9,402 @@ metadata:
# AWS EC2
Deploy and manage Amazon EC2 compute instances.
Deploy and manage Amazon EC2 compute instances for production, staging, and development workloads.
## Launch Instance
## When to Use This Skill
- Launching new compute instances for application hosting
- Building golden AMIs for consistent deployments
- Setting up auto-scaling groups behind load balancers
- Migrating workloads to Spot instances for cost savings
- Troubleshooting instance connectivity, performance, or launch failures
- Creating launch templates for repeatable infrastructure
## Prerequisites
- AWS CLI v2 installed and configured (`aws configure`)
- IAM permissions: `ec2:*`, `autoscaling:*`, `elasticloadbalancing:*`, `iam:PassRole`
- An existing VPC with subnets (see [aws-vpc](../aws-vpc/))
- SSH key pair created (`aws ec2 create-key-pair --key-name my-key --query 'KeyMaterial' --output text > my-key.pem`)
## Instance Type Selection Guide
| Category | Types | Use Case |
|---|---|---|
| General Purpose | t3, t3a, m6i, m7g | Web servers, small databases, dev/test |
| Compute Optimized | c6i, c7g | Batch processing, media encoding, ML inference |
| Memory Optimized | r6i, r7g, x2idn | In-memory caches, large databases |
| Storage Optimized | i3, i4i, d3 | Data warehousing, distributed file systems |
| Accelerated | p4d, g5, inf2 | ML training, GPU rendering, inference |
| Burstable | t3.micro-t3.2xlarge | Low-steady-state with occasional bursts |
## Launch an Instance
```bash
# Launch a production web server
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--instance-type t3.medium \
--key-name my-key \
--security-group-ids sg-12345678 \
--subnet-id subnet-12345678 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=web-server}]'
--iam-instance-profile Name=EC2AppProfile \
--metadata-options "HttpTokens=required,HttpEndpoint=enabled" \
--block-device-mappings '[{
"DeviceName": "/dev/xvda",
"Ebs": {
"VolumeSize": 30,
"VolumeType": "gp3",
"Iops": 3000,
"Throughput": 125,
"Encrypted": true
}
}]' \
--tag-specifications 'ResourceType=instance,Tags=[
{Key=Name,Value=web-server-01},
{Key=Environment,Value=production},
{Key=Team,Value=platform}
]' \
--user-data file://userdata.sh
# Launch with IMDSv2 required (security best practice)
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--metadata-options "HttpTokens=required,HttpPutResponseHopLimit=1,HttpEndpoint=enabled" \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=secure-instance}]'
```
## Auto Scaling
```bash
# Create launch template
aws ec2 create-launch-template \
--launch-template-name web-template \
--version-description v1 \
--launch-template-data '{
"ImageId": "ami-xxx",
"InstanceType": "t3.micro"
}'
# Create ASG
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name web-asg \
--launch-template LaunchTemplateName=web-template \
--min-size 2 --max-size 10 --desired-capacity 2 \
--vpc-zone-identifier "subnet-xxx,subnet-yyy"
```
## User Data
## User Data Scripts
```bash
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
# userdata.sh - Bootstrap a web server on Amazon Linux 2023
set -euxo pipefail
# System updates
dnf update -y
# Install and start web server
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
# Install CloudWatch agent
dnf install -y amazon-cloudwatch-agent
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 \
-s -c ssm:AmazonCloudWatch-linux
# Install CodeDeploy agent
dnf install -y ruby wget
cd /home/ec2-user
wget https://aws-codedeploy-us-east-1.s3.us-east-1.amazonaws.com/latest/install
chmod +x ./install
./install auto
# Signal CloudFormation (if launched via CFN)
# /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ASG --region ${AWS::Region}
```
## Launch Templates
```bash
# Create a launch template with full configuration
aws ec2 create-launch-template \
--launch-template-name web-server-template \
--version-description "v1 - AL2023 with nginx" \
--launch-template-data '{
"ImageId": "ami-0abcdef1234567890",
"InstanceType": "t3.medium",
"KeyName": "my-key",
"SecurityGroupIds": ["sg-12345678"],
"IamInstanceProfile": {"Name": "EC2AppProfile"},
"MetadataOptions": {
"HttpTokens": "required",
"HttpEndpoint": "enabled"
},
"BlockDeviceMappings": [{
"DeviceName": "/dev/xvda",
"Ebs": {
"VolumeSize": 30,
"VolumeType": "gp3",
"Encrypted": true
}
}],
"TagSpecifications": [{
"ResourceType": "instance",
"Tags": [
{"Key": "Environment", "Value": "production"},
{"Key": "ManagedBy", "Value": "launch-template"}
]
}],
"Monitoring": {"Enabled": true},
"UserData": "'"$(base64 -w0 userdata.sh)"'"
}'
# Create a new version of the launch template
aws ec2 create-launch-template-version \
--launch-template-name web-server-template \
--source-version 1 \
--version-description "v2 - updated AMI" \
--launch-template-data '{"ImageId": "ami-0newami1234567890"}'
# Set the default version
aws ec2 modify-launch-template \
--launch-template-name web-server-template \
--default-version 2
```
## Auto Scaling Group
```bash
# Create ASG with mixed instances (on-demand + spot)
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name web-asg \
--mixed-instances-policy '{
"LaunchTemplate": {
"LaunchTemplateSpecification": {
"LaunchTemplateName": "web-server-template",
"Version": "$Default"
},
"Overrides": [
{"InstanceType": "t3.medium"},
{"InstanceType": "t3a.medium"},
{"InstanceType": "m5.large"}
]
},
"InstancesDistribution": {
"OnDemandBaseCapacity": 2,
"OnDemandPercentageAboveBaseCapacity": 25,
"SpotAllocationStrategy": "capacity-optimized"
}
}' \
--min-size 2 --max-size 10 --desired-capacity 4 \
--vpc-zone-identifier "subnet-aaa,subnet-bbb" \
--target-group-arns "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/web-tg/abc123" \
--health-check-type ELB \
--health-check-grace-period 300 \
--tags '[
{"Key":"Name","Value":"web-asg","PropagateAtLaunch":true},
{"Key":"Environment","Value":"production","PropagateAtLaunch":true}
]'
# Create target tracking scaling policy (target 60% CPU)
aws autoscaling put-scaling-policy \
--auto-scaling-group-name web-asg \
--policy-name cpu-target-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-configuration '{
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ASGAverageCPUUtilization"
},
"TargetValue": 60.0,
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
# Create scheduled scaling for known traffic patterns
aws autoscaling put-scheduled-update-group-action \
--auto-scaling-group-name web-asg \
--scheduled-action-name scale-up-morning \
--recurrence "0 8 * * MON-FRI" \
--min-size 4 --max-size 20 --desired-capacity 8
aws autoscaling put-scheduled-update-group-action \
--auto-scaling-group-name web-asg \
--scheduled-action-name scale-down-evening \
--recurrence "0 20 * * MON-FRI" \
--min-size 2 --max-size 10 --desired-capacity 2
```
## Spot Instances
```bash
# Request Spot instances
aws ec2 request-spot-instances \
--spot-price "0.05" \
--instance-count 3 \
--type "one-time" \
--launch-specification '{
"ImageId": "ami-0abcdef1234567890",
"InstanceType": "c5.xlarge",
"KeyName": "my-key",
"SecurityGroupIds": ["sg-12345678"],
"SubnetId": "subnet-12345678"
}'
# Check current Spot prices
aws ec2 describe-spot-price-history \
--instance-types t3.medium t3a.medium m5.large \
--availability-zone us-east-1a \
--product-descriptions "Linux/UNIX" \
--start-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--query "SpotPriceHistory[].{Type:InstanceType,Price:SpotPrice,AZ:AvailabilityZone}" \
--output table
# Create a Spot Fleet request
aws ec2 request-spot-fleet \
--spot-fleet-request-config '{
"IamFleetRole": "arn:aws:iam::123456789012:role/aws-ec2-spot-fleet-role",
"TargetCapacity": 10,
"SpotPrice": "0.10",
"AllocationStrategy": "capacityOptimized",
"LaunchSpecifications": [
{"ImageId": "ami-xxx", "InstanceType": "c5.xlarge", "SubnetId": "subnet-aaa"},
{"ImageId": "ami-xxx", "InstanceType": "c5a.xlarge", "SubnetId": "subnet-bbb"}
]
}'
```
## AMI Management
```bash
# Create an AMI from a running instance
aws ec2 create-image \
--instance-id i-0abc123def456 \
--name "web-server-$(date +%Y%m%d)" \
--description "Web server golden AMI" \
--no-reboot \
--tag-specifications 'ResourceType=image,Tags=[
{Key=Name,Value=web-server-golden},
{Key=Version,Value=2026.03.24}
]'
# Copy AMI to another region for disaster recovery
aws ec2 copy-image \
--source-image-id ami-0abcdef1234567890 \
--source-region us-east-1 \
--region us-west-2 \
--name "web-server-dr-copy"
# Deregister old AMIs and delete associated snapshots
aws ec2 deregister-image --image-id ami-old123
aws ec2 delete-snapshot --snapshot-id snap-old123
# Share AMI with another AWS account
aws ec2 modify-image-attribute \
--image-id ami-0abcdef1234567890 \
--launch-permission "Add=[{UserId=987654321098}]"
```
## Instance Management
```bash
# List instances
aws ec2 describe-instances --filters "Name=tag:Name,Values=web*"
# List running instances with key details
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query "Reservations[].Instances[].{ID:InstanceId,Type:InstanceType,IP:PrivateIpAddress,Name:Tags[?Key=='Name']|[0].Value,State:State.Name}" \
--output table
# Stop/Start
aws ec2 stop-instances --instance-ids i-xxx
aws ec2 start-instances --instance-ids i-xxx
# Stop and start instances
aws ec2 stop-instances --instance-ids i-0abc123def456
aws ec2 start-instances --instance-ids i-0abc123def456
# Create AMI
aws ec2 create-image --instance-id i-xxx --name "my-ami"
# Resize an instance (stop first)
aws ec2 stop-instances --instance-ids i-0abc123def456
aws ec2 wait instance-stopped --instance-ids i-0abc123def456
aws ec2 modify-instance-attribute \
--instance-id i-0abc123def456 \
--instance-type '{"Value": "t3.large"}'
aws ec2 start-instances --instance-ids i-0abc123def456
# Get console output for debugging boot issues
aws ec2 get-console-output --instance-id i-0abc123def456
# Get instance screenshot (helps debug GUI issues)
aws ec2 get-console-screenshot --instance-id i-0abc123def456
```
## Best Practices
## Terraform EC2 with Auto Scaling
- Use launch templates
- Implement auto-scaling
- Use spot instances for cost savings
- Regular AMI updates
- Instance metadata service v2
```hcl
resource "aws_launch_template" "web" {
name_prefix = "web-"
image_id = data.aws_ami.amazon_linux.id
instance_type = "t3.medium"
iam_instance_profile {
name = aws_iam_instance_profile.web.name
}
metadata_options {
http_tokens = "required"
http_endpoint = "enabled"
}
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = 30
volume_type = "gp3"
encrypted = true
}
}
user_data = base64encode(file("userdata.sh"))
tag_specifications {
resource_type = "instance"
tags = {
Name = "web-server"
Environment = "production"
}
}
}
resource "aws_autoscaling_group" "web" {
name = "web-asg"
min_size = 2
max_size = 10
desired_capacity = 4
vpc_zone_identifier = [aws_subnet.private_a.id, aws_subnet.private_b.id]
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
tag {
key = "Name"
value = "web-asg"
propagate_at_launch = true
}
}
resource "aws_autoscaling_policy" "cpu" {
name = "cpu-target-tracking"
autoscaling_group_name = aws_autoscaling_group.web.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 60.0
}
}
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Instance stuck in `pending` | Insufficient capacity | Try a different AZ or instance type |
| Cannot SSH to instance | Security group or NACL blocks port 22 | Check SG ingress rules and route tables |
| Instance immediately terminates | EBS volume limit or AMI issue | Check `describe-instances` for StateReason |
| IMDSv1 deprecation warnings | Metadata options not set | Set `HttpTokens=required` in launch template |
| User data not running | Script missing shebang or not base64 | Verify `#!/bin/bash` header; check `/var/log/cloud-init-output.log` |
| Spot instance terminated | Capacity reclaimed by AWS | Use capacity-optimized allocation and diversify types |
| ASG not replacing unhealthy | Health check grace period too short | Increase grace period to cover app boot time |
| EBS throughput bottleneck | gp2 volume too small for IOPS | Migrate to gp3 and set explicit IOPS/throughput |
## Related Skills
- [terraform-aws](../terraform-aws/) - IaC deployment
- [aws-vpc](../aws-vpc/) - Networking
- [aws-vpc](../aws-vpc/) - VPC networking, subnets, and security groups
- [aws-iam](../aws-iam/) - Instance profiles and roles
- [aws-cost-optimization](../aws-cost-optimization/) - Rightsizing and Spot strategies
- [terraform-aws](../terraform-aws/) - Infrastructure as Code deployment
- [cloudformation](../cloudformation/) - AWS-native IaC templates
+324 -40
View File
@@ -9,7 +9,63 @@ metadata:
# AWS ECS & Fargate
Run containerized applications on Amazon ECS with Fargate.
Run containerized applications on Amazon ECS with Fargate serverless compute or EC2 launch type.
## When to Use This Skill
- Deploying Docker containers to AWS without managing servers (Fargate)
- Running microservices with service discovery and load balancing
- Setting up blue/green or rolling deployments for containerized apps
- Configuring auto-scaling for container workloads
- Migrating from docker-compose or Kubernetes to ECS
- Troubleshooting task failures, health check issues, or networking problems
## Prerequisites
- AWS CLI v2 installed and configured
- Docker installed for building and pushing images
- IAM permissions: `ecs:*`, `ecr:*`, `elasticloadbalancing:*`, `logs:*`, `iam:PassRole`
- An ECR repository for storing container images
- A VPC with subnets and an ALB (see [aws-vpc](../aws-vpc/))
## Cluster Setup
```bash
# Create an ECS cluster with Container Insights enabled
aws ecs create-cluster \
--cluster-name production \
--capacity-providers FARGATE FARGATE_SPOT \
--default-capacity-provider-strategy '[
{"capacityProvider": "FARGATE", "weight": 1, "base": 2},
{"capacityProvider": "FARGATE_SPOT", "weight": 3}
]' \
--settings '[{"name": "containerInsights", "value": "enabled"}]'
# List clusters
aws ecs list-clusters
# Describe cluster details
aws ecs describe-clusters --clusters production --include STATISTICS ATTACHMENTS
```
## Push Image to ECR
```bash
# Create ECR repository
aws ecr create-repository \
--repository-name myapp \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=KMS
# Authenticate Docker to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
# Build, tag, and push
docker build -t myapp:latest .
docker tag myapp:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
```
## Task Definition
@@ -18,71 +74,299 @@ Run containerized applications on Amazon ECS with Fargate.
"family": "myapp",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::xxx:role/ecsTaskExecutionRole",
"containerDefinitions": [{
"name": "myapp",
"image": "xxx.dkr.ecr.region.amazonaws.com/myapp:latest",
"portMappings": [{
"containerPort": 8080,
"protocol": "tcp"
}],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/myapp",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole",
"containerDefinitions": [
{
"name": "myapp",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest",
"essential": true,
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"environment": [
{"name": "NODE_ENV", "value": "production"},
{"name": "PORT", "value": "8080"}
],
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/db-password"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/api-key"
}
],
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/myapp",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs",
"awslogs-create-group": "true"
}
},
"ulimits": [
{"name": "nofile", "softLimit": 65536, "hardLimit": 65536}
]
}
}]
]
}
```
## Create Service
```bash
# Register the task definition
aws ecs register-task-definition --cli-input-json file://task-definition.json
# List task definition revisions
aws ecs list-task-definitions --family-prefix myapp
# Deregister an old revision
aws ecs deregister-task-definition --task-definition myapp:1
```
## Create Service with ALB
```bash
# Create the CloudWatch log group first
aws logs create-log-group --log-group-name /ecs/myapp
aws logs put-retention-policy --log-group-name /ecs/myapp --retention-in-days 30
# Create ECS service with load balancer
aws ecs create-service \
--cluster my-cluster \
--cluster production \
--service-name myapp \
--task-definition myapp:1 \
--desired-count 2 \
--task-definition myapp:2 \
--desired-count 3 \
--launch-type FARGATE \
--platform-version LATEST \
--deployment-configuration '{
"deploymentCircuitBreaker": {"enable": true, "rollback": true},
"maximumPercent": 200,
"minimumHealthyPercent": 100
}' \
--network-configuration '{
"awsvpcConfiguration": {
"subnets": ["subnet-xxx"],
"securityGroups": ["sg-xxx"],
"assignPublicIp": "ENABLED"
"subnets": ["subnet-private-a", "subnet-private-b"],
"securityGroups": ["sg-app"],
"assignPublicIp": "DISABLED"
}
}' \
--load-balancers '[{
"targetGroupArn": "arn:aws:elasticloadbalancing:...",
"targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/myapp-tg/abc123",
"containerName": "myapp",
"containerPort": 8080
}]'
}]' \
--service-registries '[{
"registryArn": "arn:aws:servicediscovery:us-east-1:123456789012:service/srv-abc123"
}]' \
--enable-execute-command \
--propagate-tags SERVICE
```
## Deployment
## Deployments
```bash
# Update service
# Rolling update - update task definition and force new deployment
aws ecs update-service \
--cluster my-cluster \
--cluster production \
--service myapp \
--task-definition myapp:2 \
--task-definition myapp:3 \
--force-new-deployment
# Watch deployment progress
aws ecs describe-services \
--cluster production \
--services myapp \
--query "services[0].deployments[].{Status:status,Running:runningCount,Desired:desiredCount,TaskDef:taskDefinition}" \
--output table
# Wait for service to stabilize
aws ecs wait services-stable --cluster production --services myapp
# Exec into a running container for debugging
aws ecs execute-command \
--cluster production \
--task arn:aws:ecs:us-east-1:123456789012:task/production/abc123 \
--container myapp \
--interactive \
--command "/bin/sh"
```
## Best Practices
## Auto Scaling
- Use ECR for images
- Implement service discovery
- Configure health checks
- Use secrets manager for secrets
- Enable container insights
```bash
# Register ECS service as a scalable target
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/production/myapp \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 \
--max-capacity 20
# Target tracking policy - scale on CPU utilization
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/production/myapp \
--scalable-dimension ecs:service:DesiredCount \
--policy-name cpu-target-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"TargetValue": 70.0,
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
# Scale on request count per target (ALB)
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/production/myapp \
--scalable-dimension ecs:service:DesiredCount \
--policy-name request-count-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ALBRequestCountPerTarget",
"ResourceLabel": "app/my-alb/abc123/targetgroup/myapp-tg/def456"
},
"TargetValue": 1000.0,
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
```
## Terraform ECS Fargate Service
```hcl
resource "aws_ecs_cluster" "main" {
name = "production"
setting {
name = "containerInsights"
value = "enabled"
}
}
resource "aws_ecs_task_definition" "myapp" {
family = "myapp"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([{
name = "myapp"
image = "${aws_ecr_repository.myapp.repository_url}:latest"
essential = true
portMappings = [{
containerPort = 8080
protocol = "tcp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.myapp.name
awslogs-region = "us-east-1"
awslogs-stream-prefix = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}])
}
resource "aws_ecs_service" "myapp" {
name = "myapp"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.myapp.arn
desired_count = 3
launch_type = "FARGATE"
deployment_circuit_breaker {
enable = true
rollback = true
}
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.app.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.myapp.arn
container_name = "myapp"
container_port = 8080
}
enable_execute_command = true
propagate_tags = "SERVICE"
}
```
## Viewing Logs
```bash
# Tail logs from CloudWatch
aws logs tail /ecs/myapp --follow --since 1h
# Get logs for a specific task
aws logs get-log-events \
--log-group-name /ecs/myapp \
--log-stream-name "ecs/myapp/abc123def456" \
--start-from-head
# Filter logs for errors
aws logs filter-log-events \
--log-group-name /ecs/myapp \
--filter-pattern "ERROR" \
--start-time $(date -d '1 hour ago' +%s000)
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Task stuck in PROVISIONING | No available capacity in subnets | Check subnet availability and capacity provider |
| Task fails immediately | Container crashes on startup | Check CloudWatch logs; run image locally first |
| Health check failing | App not ready within startPeriod | Increase `startPeriod`; verify health endpoint |
| Cannot pull ECR image | Execution role missing ECR permissions | Attach `AmazonECSTaskExecutionRolePolicy` |
| Service stuck at 0 running | Security group blocks ALB health check | Allow ALB SG to reach container port |
| Exec command fails | SSM agent not initialized | Ensure `enableExecuteCommand` is true; check task role |
| High Fargate costs | Not using Fargate Spot for tolerant workloads | Add FARGATE_SPOT capacity provider |
| Container OOM killed | Memory limit too low | Increase `memory` in task definition; check for leaks |
| Slow deployments | minimumHealthyPercent too high | Set to 50% for faster rolling updates |
## Related Skills
- [docker-management](../../../devops/containers/docker-management/) - Container basics
- [container-registries](../../../devops/containers/container-registries/) - ECR
- [docker-management](../../../devops/containers/docker-management/) - Container fundamentals
- [container-registries](../../../devops/containers/container-registries/) - ECR and image management
- [aws-vpc](../aws-vpc/) - Networking for ECS tasks
- [aws-iam](../aws-iam/) - Task and execution roles
- [terraform-aws](../terraform-aws/) - Infrastructure as Code deployment
+395 -45
View File
@@ -9,28 +9,70 @@ metadata:
# AWS IAM
Manage identity and access in AWS.
Manage identity and access in AWS with least-privilege policies, roles, federation, and permission boundaries.
## IAM Policies
## When to Use This Skill
- Creating roles for EC2 instances, Lambda functions, or ECS tasks
- Writing custom IAM policies with least-privilege access
- Setting up OIDC federation for GitHub Actions or other CI/CD systems
- Implementing permission boundaries for delegated administration
- Auditing access with IAM Access Analyzer and credential reports
- Configuring cross-account access with assume-role patterns
- Enforcing MFA and session policies
## Prerequisites
- AWS CLI v2 installed and configured
- IAM permissions: `iam:*` (or scoped to specific actions for least privilege)
- For OIDC: ability to create identity providers (`iam:CreateOpenIDConnectProvider`)
- AWS Organizations access for Service Control Policies (SCPs)
## IAM Policy Structure
Every IAM policy follows the same JSON structure. Always specify the minimum actions and resources required.
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
}]
"Statement": [
{
"Sid": "AllowS3ReadWrite",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-bucket",
"arn:aws:s3:::my-app-bucket/*"
],
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
},
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-app-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}
```
## Create Role
## Create and Manage Roles
```bash
# Create role with trust policy
# Create an EC2 instance role with trust policy
aws iam create-role \
--role-name EC2AppRole \
--assume-role-policy-document '{
@@ -40,58 +82,366 @@ aws iam create-role \
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}' \
--tags '[{"Key":"Team","Value":"platform"},{"Key":"Environment","Value":"production"}]'
# Create and attach an inline policy
aws iam put-role-policy \
--role-name EC2AppRole \
--policy-name s3-access \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}]
}'
# Attach policy
# Attach a managed policy
aws iam attach-role-policy \
--role-name EC2AppRole \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
--policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
# Create instance profile and associate the role
aws iam create-instance-profile --instance-profile-name EC2AppProfile
aws iam add-role-to-instance-profile \
--instance-profile-name EC2AppProfile \
--role-name EC2AppRole
# Create a Lambda execution role
aws iam create-role \
--role-name LambdaExecRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
aws iam attach-role-policy \
--role-name LambdaExecRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
```
## Service-Linked Roles
## Cross-Account Access
```bash
# For services like ECS, RDS
aws iam create-service-linked-role \
--aws-service-name ecs.amazonaws.com
# In Account B: create role that Account A can assume
aws iam create-role \
--role-name CrossAccountReadRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111111111111:root"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"sts:ExternalId": "unique-external-id-12345"}
}
}]
}'
# In Account A: assume the role
aws sts assume-role \
--role-arn arn:aws:iam::222222222222:role/CrossAccountReadRole \
--role-session-name cross-account-session \
--external-id unique-external-id-12345
# Use the temporary credentials
export AWS_ACCESS_KEY_ID="ASIAXXX"
export AWS_SECRET_ACCESS_KEY="xxx"
export AWS_SESSION_TOKEN="xxx"
```
## Best Practices
## OIDC Federation for GitHub Actions
```bash
# Create the GitHub OIDC identity provider
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1"
# Create a role for GitHub Actions with repo-scoped trust
aws iam create-role \
--role-name GitHubActionsDeployRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
}
}]
}'
# Attach deployment permissions to the role
aws iam attach-role-policy \
--role-name GitHubActionsDeployRole \
--policy-arn arn:aws:iam::123456789012:policy/DeploymentPolicy
```
GitHub Actions workflow usage:
```yaml
security_practices:
- Use roles, not long-term credentials
- Implement least privilege
- Enable MFA
- Regular access reviews
- Use IAM Access Analyzer
- Implement SCPs for organizations
# .github/workflows/deploy.yml
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
aws-region: us-east-1
- run: aws sts get-caller-identity
```
## Policy Conditions
## Permission Boundaries
```json
{
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
},
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
}
```bash
# Create a permission boundary policy
aws iam create-policy \
--policy-name DeveloperBoundary \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowedServices",
"Effect": "Allow",
"Action": [
"s3:*",
"lambda:*",
"dynamodb:*",
"sqs:*",
"sns:*",
"logs:*",
"cloudwatch:*",
"ecr:*",
"ecs:*"
],
"Resource": "*"
},
{
"Sid": "DenyIAMChanges",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:DeleteUser",
"iam:CreateRole",
"iam:DeleteRole",
"iam:AttachRolePolicy",
"iam:PutRolePermissionsBoundary",
"iam:DeleteRolePermissionsBoundary"
],
"Resource": "*"
},
{
"Sid": "DenyOutsideRegion",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2"]
},
"ForAnyValue:StringNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/admin-*"
}
}
}
]
}'
# Create a role with the permission boundary
aws iam create-role \
--role-name DeveloperRole \
--assume-role-policy-document file://trust-policy.json \
--permissions-boundary "arn:aws:iam::123456789012:policy/DeveloperBoundary"
```
## IAM Access Analyzer and Auditing
```bash
# Create an IAM Access Analyzer
aws accessanalyzer create-analyzer \
--analyzer-name account-analyzer \
--type ACCOUNT
# List findings (externally accessible resources)
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/account-analyzer
# Generate credential report
aws iam generate-credential-report
aws iam get-credential-report --output text --query Content | base64 -d > credential-report.csv
# Find users with console access but no MFA
aws iam list-users --query "Users[].UserName" --output text | while read user; do
mfa=$(aws iam list-mfa-devices --user-name "$user" --query "MFADevices" --output text)
if [ -z "$mfa" ]; then
echo "NO MFA: $user"
fi
done
# List all policies attached to a role
aws iam list-attached-role-policies --role-name EC2AppRole
aws iam list-role-policies --role-name EC2AppRole
# Get the last-accessed services for a role
aws iam generate-service-last-accessed-details --arn arn:aws:iam::123456789012:role/EC2AppRole
# Then retrieve results with the returned JobId
aws iam get-service-last-accessed-details --job-id "job-id-from-above"
# Simulate a policy to test access
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/EC2AppRole \
--action-names s3:GetObject s3:PutObject \
--resource-arns arn:aws:s3:::my-app-bucket/data.json
```
## Terraform IAM Role with OIDC
```hcl
# OIDC provider for GitHub Actions
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
# Role for GitHub Actions
resource "aws_iam_role" "github_actions" {
name = "GitHubActionsDeployRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:my-org/my-repo:*"
}
}
}]
})
permissions_boundary = aws_iam_policy.boundary.arn
}
resource "aws_iam_role_policy_attachment" "deploy" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.deployment.arn
}
# Permission boundary
resource "aws_iam_policy" "boundary" {
name = "DeveloperBoundary"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowedServices"
Effect = "Allow"
Action = ["s3:*", "lambda:*", "dynamodb:*", "ecs:*", "logs:*"]
Resource = "*"
},
{
Sid = "DenyIAMEscalation"
Effect = "Deny"
Action = ["iam:CreateUser", "iam:CreateRole", "iam:AttachRolePolicy"]
Resource = "*"
}
]
})
}
```
## Best Practices
## Service Control Policies (Organizations)
- Follow least privilege
- Use IAM roles for applications
- Enable CloudTrail for auditing
- Regular credential rotation
- Use permission boundaries
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRootAccount",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::*:root"
}
}
},
{
"Sid": "RequireIMDSv2",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
},
{
"Sid": "DenyRegionsOutsideUS",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2"]
},
"ForAnyValue:StringNotLike": {
"aws:PrincipalArn": ["arn:aws:iam::*:role/OrganizationAdmin"]
}
}
}
]
}
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Access Denied on API call | Missing or incorrect policy | Use `simulate-principal-policy` to test; check resource ARN format |
| Role cannot be assumed | Trust policy does not include the caller | Verify Principal in trust policy matches caller ARN |
| OIDC federation fails | Thumbprint or audience mismatch | Verify OIDC provider URL, client ID list, and condition keys |
| Permission boundary blocks action | Boundary does not include the action | Add the action to the boundary; effective = identity AND boundary |
| Credential report shows stale keys | Keys not rotated in 90+ days | Rotate keys; disable unused access keys |
| Service-linked role creation fails | Organization SCP blocks iam:CreateServiceLinkedRole | Add exception in SCP for the specific service |
| Cross-account assume role fails | Missing ExternalId or wrong account | Verify ExternalId matches; check account number in Principal |
| MFA condition not enforced | Condition key not in policy | Add `aws:MultiFactorAuthPresent` condition |
## Related Skills
- [terraform-aws](../terraform-aws/) - IaC deployment
- [access-review](../../../compliance/governance/access-review/) - Access auditing
- [terraform-aws](../terraform-aws/) - IaC deployment of IAM resources
- [aws-ec2](../aws-ec2/) - Instance profiles and roles
- [aws-lambda](../aws-lambda/) - Lambda execution roles
- [aws-ecs-fargate](../aws-ecs-fargate/) - ECS task and execution roles
- [access-review](../../../compliance/governance/access-review/) - Access auditing and governance
+367 -39
View File
@@ -9,76 +9,404 @@ metadata:
# AWS Lambda
Build serverless applications with AWS Lambda.
Build serverless applications with AWS Lambda, covering function creation, event sources, layers, SAM templates, and cold start optimization.
## Create Function
## When to Use This Skill
- Building event-driven applications triggered by API Gateway, S3, SQS, or EventBridge
- Running scheduled tasks (cron) without managing servers
- Processing data streams from Kinesis or DynamoDB
- Building lightweight APIs with API Gateway or function URLs
- Implementing webhooks, Slack bots, or automation scripts
- Reducing compute costs for intermittent or bursty workloads
## Prerequisites
- AWS CLI v2 installed and configured
- IAM permissions: `lambda:*`, `iam:PassRole`, `logs:*`, `apigateway:*`, `s3:*`
- Python 3.11+, Node.js 20+, or another supported runtime installed locally
- (Optional) AWS SAM CLI for local development and deployment
## Create and Deploy a Function
```bash
# Create function
# Create a deployment package
cd my-function
zip -r function.zip app.py
# Create the Lambda function
aws lambda create-function \
--function-name myfunction \
--runtime python3.11 \
--function-name my-api-handler \
--runtime python3.12 \
--handler app.handler \
--role arn:aws:iam::xxx:role/lambda-role \
--role arn:aws:iam::123456789012:role/LambdaExecRole \
--zip-file fileb://function.zip \
--memory-size 256 \
--timeout 30 \
--environment 'Variables={STAGE=production,LOG_LEVEL=INFO}' \
--architectures arm64 \
--tracing-config Mode=Active \
--tags '{"Team":"backend","Environment":"production"}'
# Update function code
aws lambda update-function-code \
--function-name my-api-handler \
--zip-file fileb://function.zip
# Update code
aws lambda update-function-code \
--function-name myfunction \
--zip-file fileb://function.zip
# Update function configuration
aws lambda update-function-configuration \
--function-name my-api-handler \
--memory-size 512 \
--timeout 60 \
--environment 'Variables={STAGE=production,LOG_LEVEL=WARNING}'
# Publish a version (immutable snapshot)
aws lambda publish-version \
--function-name my-api-handler \
--description "v1.2.0 - added rate limiting"
# Create an alias pointing to the version
aws lambda create-alias \
--function-name my-api-handler \
--name live \
--function-version 3
# Weighted alias for canary deployments (90% v3, 10% v4)
aws lambda update-alias \
--function-name my-api-handler \
--name live \
--function-version 4 \
--routing-config '{"AdditionalVersionWeights":{"3":0.9}}'
```
## Function Code
## Function Code Examples
```python
# app.py
# app.py - API Gateway handler with structured logging
import json
import logging
import os
logger = logging.getLogger()
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
def handler(event, context):
"""Handle API Gateway proxy event."""
logger.info("Request: %s %s", event["httpMethod"], event["path"])
try:
body = json.loads(event.get("body", "{}"))
result = process_request(body)
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"X-Request-Id": context.aws_request_id
},
"body": json.dumps(result)
}
except ValueError as e:
logger.warning("Validation error: %s", e)
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
except Exception as e:
logger.exception("Unhandled error")
return {"statusCode": 500, "body": json.dumps({"error": "Internal server error"})}
def process_request(body):
return {"message": "OK", "data": body}
```
```python
# sqs_processor.py - SQS batch processor with partial failure reporting
import json
import logging
logger = logging.getLogger()
logger.setLevel("INFO")
def handler(event, context):
"""Process SQS messages with partial batch failure reporting."""
failed_ids = []
for record in event["Records"]:
try:
body = json.loads(record["body"])
logger.info("Processing message: %s", record["messageId"])
process_message(body)
except Exception as e:
logger.error("Failed message %s: %s", record["messageId"], e)
failed_ids.append(record["messageId"])
# Return failed items so only those get retried
return {
'statusCode': 200,
'body': json.dumps({'message': 'Hello!'})
"batchItemFailures": [
{"itemIdentifier": msg_id} for msg_id in failed_ids
]
}
def process_message(body):
pass # your logic here
```
## API Gateway Integration
## Lambda Layers
```bash
# Create REST API
aws apigateway create-rest-api --name myapi
# Build a layer for Python dependencies
mkdir -p layer/python
pip install requests boto3-stubs -t layer/python/
cd layer
zip -r ../my-layer.zip python/
# Add Lambda permission
# Publish the layer
aws lambda publish-layer-version \
--layer-name common-deps \
--description "Shared Python dependencies" \
--zip-file fileb://my-layer.zip \
--compatible-runtimes python3.11 python3.12 \
--compatible-architectures arm64 x86_64
# Attach layer to a function
aws lambda update-function-configuration \
--function-name my-api-handler \
--layers "arn:aws:lambda:us-east-1:123456789012:layer:common-deps:1"
# List available layers
aws lambda list-layers --compatible-runtime python3.12
```
## Event Source Mappings
```bash
# SQS trigger with batch processing
aws lambda create-event-source-mapping \
--function-name sqs-processor \
--event-source-arn arn:aws:sqs:us-east-1:123456789012:my-queue \
--batch-size 10 \
--maximum-batching-window-in-seconds 5 \
--function-response-types ReportBatchItemFailures
# DynamoDB Streams trigger
aws lambda create-event-source-mapping \
--function-name stream-processor \
--event-source-arn arn:aws:dynamodb:us-east-1:123456789012:table/my-table/stream/2026-01-01T00:00:00.000 \
--batch-size 100 \
--starting-position LATEST \
--maximum-retry-attempts 3 \
--bisect-batch-on-function-error \
--destination-config '{"OnFailure":{"Destination":"arn:aws:sqs:us-east-1:123456789012:dlq"}}'
# S3 event notification (via Lambda permission + S3 config)
aws lambda add-permission \
--function-name myfunction \
--statement-id apigateway \
--function-name image-processor \
--statement-id s3-trigger \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::my-uploads-bucket \
--source-account 123456789012
aws s3api put-bucket-notification-configuration \
--bucket my-uploads-bucket \
--notification-configuration '{
"LambdaFunctionConfigurations": [{
"LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:image-processor",
"Events": ["s3:ObjectCreated:*"],
"Filter": {"Key": {"FilterRules": [{"Name": "suffix", "Value": ".jpg"}]}}
}]
}'
# Schedule with EventBridge (cron)
aws events put-rule \
--name daily-cleanup \
--schedule-expression "cron(0 2 * * ? *)" \
--state ENABLED
aws lambda add-permission \
--function-name daily-cleanup \
--statement-id eventbridge \
--action lambda:InvokeFunction \
--principal events.amazonaws.com \
--source-arn arn:aws:events:us-east-1:123456789012:rule/daily-cleanup
aws events put-targets \
--rule daily-cleanup \
--targets '[{"Id":"1","Arn":"arn:aws:lambda:us-east-1:123456789012:function:daily-cleanup"}]'
```
## Environment & Configuration
## Function URLs (No API Gateway Needed)
```bash
# Set environment variables
aws lambda update-function-configuration \
--function-name myfunction \
--environment "Variables={DB_HOST=xxx,API_KEY=yyy}"
# Create a function URL (public HTTPS endpoint)
aws lambda create-function-url-config \
--function-name my-api-handler \
--auth-type NONE \
--cors '{
"AllowOrigins": ["https://myapp.com"],
"AllowMethods": ["GET", "POST"],
"AllowHeaders": ["Content-Type"],
"MaxAge": 86400
}'
# Set memory and timeout
aws lambda update-function-configuration \
--function-name myfunction \
--memory-size 256 \
--timeout 30
# Grant public invoke for function URL
aws lambda add-permission \
--function-name my-api-handler \
--statement-id function-url-public \
--action lambda:InvokeFunctionUrl \
--principal "*" \
--function-url-auth-type NONE
```
## Best Practices
## Cold Start Optimization
- Minimize cold starts
- Use layers for dependencies
- Implement proper error handling
- Use provisioned concurrency for latency-sensitive functions
- Monitor with CloudWatch
```bash
# Enable provisioned concurrency to eliminate cold starts
aws lambda put-provisioned-concurrency-config \
--function-name my-api-handler \
--qualifier live \
--provisioned-concurrent-executions 10
# Set reserved concurrency (throttle limit)
aws lambda put-function-concurrency \
--function-name my-api-handler \
--reserved-concurrent-executions 100
# Enable SnapStart for Java functions (near-zero cold starts)
aws lambda update-function-configuration \
--function-name my-java-handler \
--snap-start '{"ApplyOn": "PublishedVersions"}'
aws lambda publish-version --function-name my-java-handler
```
Cold start reduction tips:
- Use `arm64` architecture (Graviton) for faster init and lower cost
- Minimize deployment package size; use layers for large dependencies
- Initialize SDK clients outside the handler function
- Avoid VPC unless required (VPC cold starts are longer)
- Use provisioned concurrency for latency-sensitive paths
## SAM Template
```yaml
# template.yaml - AWS SAM application
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: My serverless API
Globals:
Function:
Runtime: python3.12
Architectures: [arm64]
MemorySize: 256
Timeout: 30
Tracing: Active
Environment:
Variables:
STAGE: !Ref Stage
LOG_LEVEL: INFO
Parameters:
Stage:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${Stage}-api-handler"
Handler: app.handler
CodeUri: src/
Layers:
- !Ref DepsLayer
Events:
GetItems:
Type: Api
Properties:
Path: /items
Method: get
PostItem:
Type: Api
Properties:
Path: /items
Method: post
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ItemsTable
QueueProcessor:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${Stage}-queue-processor"
Handler: sqs_processor.handler
CodeUri: src/
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt ProcessingQueue.Arn
BatchSize: 10
FunctionResponseTypes:
- ReportBatchItemFailures
DepsLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: common-deps
ContentUri: layer/
CompatibleRuntimes:
- python3.12
ItemsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub "${Stage}-items"
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProcessingQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub "${Stage}-processing"
VisibilityTimeout: 360
Outputs:
ApiEndpoint:
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"
```
```bash
# SAM CLI commands
sam build
sam local invoke ApiFunction --event events/get-items.json
sam local start-api --port 3000
sam deploy --guided
sam logs --name ApiFunction --stack-name my-stack --tail
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Function times out | Timeout too low or downstream slow | Increase timeout; check VPC/NAT config |
| Out of memory | Memory limit too small | Increase `--memory-size`; profile with CloudWatch Insights |
| Permission denied on AWS API | Execution role missing policy | Attach required policy to the execution role |
| Cold starts > 5s | Large package or VPC overhead | Use layers, arm64, provisioned concurrency; remove VPC if not needed |
| SQS messages reprocessed | Visibility timeout < function timeout | Set queue visibility timeout to 6x function timeout |
| Event source mapping disabled | Too many consecutive errors | Fix the function error; re-enable the mapping |
| Layer not found | Wrong region or deleted version | Verify layer ARN region matches function region |
| Canary deployment not shifting | Alias routing config wrong | Verify version numbers in routing config |
| Cannot invoke function URL | Missing resource-based policy | Add `lambda:InvokeFunctionUrl` permission |
## Related Skills
- [terraform-aws](../terraform-aws/) - IaC deployment
- [aws-iam](../aws-iam/) - Execution roles
- [aws-iam](../aws-iam/) - Execution roles and permissions
- [terraform-aws](../terraform-aws/) - IaC deployment for Lambda
- [aws-s3](../aws-s3/) - S3 event triggers
- [aws-vpc](../aws-vpc/) - VPC configuration for Lambda
- [aws-cost-optimization](../aws-cost-optimization/) - Optimizing Lambda spend
+326 -40
View File
@@ -9,70 +9,356 @@ metadata:
# AWS RDS
Deploy managed relational databases with Amazon RDS.
Deploy and manage Amazon RDS relational databases with production-grade backups, replication, monitoring, and security.
## Create Database
## When to Use This Skill
- Provisioning a managed PostgreSQL, MySQL, MariaDB, Oracle, or SQL Server database
- Setting up Multi-AZ deployments for high availability
- Creating read replicas for horizontal read scaling
- Configuring automated backups, snapshots, and point-in-time recovery
- Tuning database parameters for performance
- Migrating from self-managed databases to RDS
- Monitoring database performance and setting up alarms
## Prerequisites
- AWS CLI v2 installed and configured
- IAM permissions: `rds:*`, `ec2:DescribeSecurityGroups`, `ec2:DescribeSubnets`, `kms:*`, `cloudwatch:*`
- A VPC with at least two subnets in different AZs (for subnet group)
- Security group allowing database port access from application subnets only
## Create a DB Subnet Group
```bash
# Create a subnet group spanning two AZs
aws rds create-db-subnet-group \
--db-subnet-group-name production-db-subnets \
--db-subnet-group-description "Production database subnets" \
--subnet-ids subnet-private-a subnet-private-b
# List subnet groups
aws rds describe-db-subnet-groups \
--query "DBSubnetGroups[].{Name:DBSubnetGroupName,VPC:VpcId,Status:SubnetGroupStatus}" \
--output table
```
## Create a Production Database
```bash
# Create a PostgreSQL 16 Multi-AZ instance
aws rds create-db-instance \
--db-instance-identifier mydb \
--db-instance-class db.t3.micro \
--db-instance-identifier production-api-db \
--db-instance-class db.r6g.large \
--engine postgres \
--engine-version 15 \
--master-username admin \
--master-user-password secretpassword \
--allocated-storage 20 \
--engine-version 16.4 \
--master-username appadmin \
--manage-master-user-password \
--allocated-storage 100 \
--max-allocated-storage 500 \
--storage-type gp3 \
--storage-encrypted \
--vpc-security-group-ids sg-xxx \
--db-subnet-group-name my-subnet-group \
--backup-retention-period 7 \
--multi-az
--kms-key-id alias/rds-key \
--vpc-security-group-ids sg-db-access \
--db-subnet-group-name production-db-subnets \
--db-name appdb \
--backup-retention-period 14 \
--preferred-backup-window "03:00-04:00" \
--preferred-maintenance-window "sun:05:00-sun:06:00" \
--multi-az \
--auto-minor-version-upgrade \
--deletion-protection \
--copy-tags-to-snapshot \
--monitoring-interval 60 \
--monitoring-role-arn arn:aws:iam::123456789012:role/rds-monitoring-role \
--enable-performance-insights \
--performance-insights-retention-period 7 \
--enable-cloudwatch-logs-exports '["postgresql","upgrade"]' \
--tags '[
{"Key":"Environment","Value":"production"},
{"Key":"Team","Value":"backend"},
{"Key":"Backup","Value":"daily"}
]'
# Wait for instance to become available
aws rds wait db-instance-available --db-instance-identifier production-api-db
# Get connection endpoint
aws rds describe-db-instances \
--db-instance-identifier production-api-db \
--query "DBInstances[0].Endpoint.{Address:Address,Port:Port}" \
--output table
```
## Retrieve Master Password from Secrets Manager
```bash
# When using --manage-master-user-password, RDS stores the password in Secrets Manager
aws rds describe-db-instances \
--db-instance-identifier production-api-db \
--query "DBInstances[0].MasterUserSecret.SecretArn" \
--output text
# Retrieve the secret value
aws secretsmanager get-secret-value \
--secret-id arn:aws:secretsmanager:us-east-1:123456789012:secret:rds-db-secret-abc123 \
--query SecretString --output text
```
## Parameter Groups
```bash
# Create a custom parameter group
aws rds create-db-parameter-group \
--db-parameter-group-name custom-pg \
--db-parameter-group-family postgres15 \
--description "Custom PostgreSQL parameters"
--db-parameter-group-name production-pg16 \
--db-parameter-group-family postgres16 \
--description "Production PostgreSQL 16 parameters"
# Set performance parameters
aws rds modify-db-parameter-group \
--db-parameter-group-name custom-pg \
--parameters "ParameterName=max_connections,ParameterValue=200,ApplyMethod=pending-reboot"
```
--db-parameter-group-name production-pg16 \
--parameters \
"ParameterName=max_connections,ParameterValue=200,ApplyMethod=pending-reboot" \
"ParameterName=shared_buffers,ParameterValue={DBInstanceClassMemory/4},ApplyMethod=pending-reboot" \
"ParameterName=effective_cache_size,ParameterValue={DBInstanceClassMemory*3/4},ApplyMethod=pending-reboot" \
"ParameterName=work_mem,ParameterValue=65536,ApplyMethod=immediate" \
"ParameterName=maintenance_work_mem,ParameterValue=524288,ApplyMethod=immediate" \
"ParameterName=random_page_cost,ParameterValue=1.1,ApplyMethod=immediate" \
"ParameterName=log_min_duration_statement,ParameterValue=1000,ApplyMethod=immediate" \
"ParameterName=log_statement,ParameterValue=ddl,ApplyMethod=immediate" \
"ParameterName=idle_in_transaction_session_timeout,ParameterValue=60000,ApplyMethod=immediate"
## Snapshots & Recovery
```bash
# Create snapshot
aws rds create-db-snapshot \
--db-instance-identifier mydb \
--db-snapshot-identifier mydb-snapshot
# Restore from snapshot
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier mydb-restored \
--db-snapshot-identifier mydb-snapshot
# Apply parameter group to the instance
aws rds modify-db-instance \
--db-instance-identifier production-api-db \
--db-parameter-group-name production-pg16 \
--apply-immediately
```
## Read Replicas
```bash
# Create a read replica in the same region
aws rds create-db-instance-read-replica \
--db-instance-identifier mydb-replica \
--source-db-instance-identifier mydb
--db-instance-identifier production-api-db-read1 \
--source-db-instance-identifier production-api-db \
--db-instance-class db.r6g.large \
--availability-zone us-east-1b \
--enable-performance-insights \
--monitoring-interval 60 \
--monitoring-role-arn arn:aws:iam::123456789012:role/rds-monitoring-role
# Create a cross-region read replica for DR
aws rds create-db-instance-read-replica \
--db-instance-identifier dr-api-db-read \
--source-db-instance-identifier arn:aws:rds:us-east-1:123456789012:db:production-api-db \
--db-instance-class db.r6g.large \
--region us-west-2 \
--storage-encrypted \
--kms-key-id alias/rds-dr-key
# Promote a read replica to standalone (for DR failover)
aws rds promote-read-replica \
--db-instance-identifier dr-api-db-read
# Check replication lag
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name ReplicaLag \
--dimensions Name=DBInstanceIdentifier,Value=production-api-db-read1 \
--start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 300 \
--statistics Average \
--output table
```
## Best Practices
## Snapshots and Point-in-Time Recovery
- Enable Multi-AZ for production
- Use encryption at rest
- Implement automated backups
- Use read replicas for read scaling
- Store credentials in Secrets Manager
```bash
# Create a manual snapshot
aws rds create-db-snapshot \
--db-instance-identifier production-api-db \
--db-snapshot-identifier production-api-db-pre-migration-$(date +%Y%m%d)
# Wait for snapshot to complete
aws rds wait db-snapshot-available \
--db-snapshot-identifier production-api-db-pre-migration-20260324
# Restore from snapshot (creates a new instance)
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier production-api-db-restored \
--db-snapshot-identifier production-api-db-pre-migration-20260324 \
--db-instance-class db.r6g.large \
--db-subnet-group-name production-db-subnets \
--vpc-security-group-ids sg-db-access
# Point-in-time recovery (restore to a specific second)
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier production-api-db \
--target-db-instance-identifier production-api-db-pitr \
--restore-time "2026-03-24T10:30:00Z" \
--db-instance-class db.r6g.large \
--db-subnet-group-name production-db-subnets
# Copy snapshot to another region
aws rds copy-db-snapshot \
--source-db-snapshot-identifier arn:aws:rds:us-east-1:123456789012:snapshot:production-api-db-pre-migration-20260324 \
--target-db-snapshot-identifier production-api-db-dr-copy \
--region us-west-2 \
--kms-key-id alias/rds-dr-key
# Delete old snapshots
aws rds delete-db-snapshot --db-snapshot-identifier old-snapshot-name
```
## Monitoring and Alarms
```bash
# Set CPU utilization alarm
aws cloudwatch put-metric-alarm \
--alarm-name rds-production-cpu-high \
--alarm-description "RDS CPU > 80% for 5 minutes" \
--metric-name CPUUtilization \
--namespace AWS/RDS \
--dimensions Name=DBInstanceIdentifier,Value=production-api-db \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:us-east-1:123456789012:db-alerts
# Set free storage space alarm (alert below 10 GB)
aws cloudwatch put-metric-alarm \
--alarm-name rds-production-storage-low \
--alarm-description "RDS free storage < 10GB" \
--metric-name FreeStorageSpace \
--namespace AWS/RDS \
--dimensions Name=DBInstanceIdentifier,Value=production-api-db \
--statistic Average \
--period 300 \
--threshold 10737418240 \
--comparison-operator LessThanThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:us-east-1:123456789012:db-alerts
# Check current database connections
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name DatabaseConnections \
--dimensions Name=DBInstanceIdentifier,Value=production-api-db \
--start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 300 \
--statistics Average Maximum \
--output table
```
## Terraform RDS Example
```hcl
resource "aws_db_subnet_group" "main" {
name = "production-db-subnets"
subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
tags = {
Environment = "production"
}
}
resource "aws_db_parameter_group" "postgres16" {
name = "production-pg16"
family = "postgres16"
parameter {
name = "max_connections"
value = "200"
}
parameter {
name = "shared_buffers"
value = "{DBInstanceClassMemory/4}"
apply_method = "pending-reboot"
}
parameter {
name = "log_min_duration_statement"
value = "1000"
}
}
resource "aws_db_instance" "main" {
identifier = "production-api-db"
engine = "postgres"
engine_version = "16.4"
instance_class = "db.r6g.large"
allocated_storage = 100
max_allocated_storage = 500
storage_type = "gp3"
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
db_name = "appdb"
username = "appadmin"
manage_master_user_password = true
multi_az = true
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
parameter_group_name = aws_db_parameter_group.postgres16.name
backup_retention_period = 14
backup_window = "03:00-04:00"
maintenance_window = "sun:05:00-sun:06:00"
copy_tags_to_snapshot = true
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "production-api-db-final"
performance_insights_enabled = true
performance_insights_retention_period = 7
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
tags = {
Environment = "production"
Team = "backend"
}
}
resource "aws_db_instance" "read_replica" {
identifier = "production-api-db-read1"
replicate_source_db = aws_db_instance.main.identifier
instance_class = "db.r6g.large"
performance_insights_enabled = true
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
}
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Cannot connect to RDS | Security group blocks traffic | Verify SG allows app subnet CIDR on DB port |
| Storage full | Auto-scaling not enabled or limit reached | Set `--max-allocated-storage`; increase manually |
| High replication lag | Write-heavy workload or replica undersized | Upgrade replica instance class; reduce write volume |
| Parameter change not applied | Requires reboot for static params | Reboot with `--force-failover` during maintenance window |
| Snapshot restore slow | Large database size | Use larger instance class for restore; consider PITR |
| Performance Insights empty | Not enabled or instance type unsupported | Enable PI; check instance class supports it |
| Multi-AZ failover happened | Hardware or AZ failure | Check RDS events; review failover logs |
| Connection count maxed out | Application connection leak | Implement connection pooling (PgBouncer/RDS Proxy) |
| Master password unknown | Using managed secret | Retrieve from Secrets Manager ARN in instance details |
## Related Skills
- [terraform-aws](../terraform-aws/) - IaC deployment
- [aws-secrets-manager](../../../security/secrets/aws-secrets-manager/) - Credentials
- [terraform-aws](../terraform-aws/) - IaC deployment for RDS
- [aws-vpc](../aws-vpc/) - Subnet groups and security groups
- [aws-iam](../aws-iam/) - RDS IAM authentication
- [aws-cost-optimization](../aws-cost-optimization/) - Reserved instances for RDS
- [aws-s3](../aws-s3/) - Export snapshots to S3
+371 -41
View File
@@ -9,80 +9,410 @@ metadata:
# AWS S3
Manage object storage with Amazon S3.
Manage Amazon S3 object storage with production-grade security, lifecycle policies, replication, and access controls.
## Create Bucket
## When to Use This Skill
- Creating S3 buckets with security hardening (encryption, public access block, versioning)
- Writing bucket policies to enforce HTTPS, restrict IP ranges, or grant cross-account access
- Setting up lifecycle rules to transition objects between storage classes
- Configuring cross-region replication for disaster recovery
- Generating presigned URLs for temporary access to private objects
- Setting up static website hosting or CloudFront origins
- Troubleshooting access denied errors or policy conflicts
## Prerequisites
- AWS CLI v2 installed and configured
- IAM permissions: `s3:*`, `s3-object-lambda:*`, `kms:*` (for SSE-KMS)
- For replication: IAM role with replication permissions and destination bucket in target region
- For logging: a separate logging bucket with appropriate ACL
## Create and Secure a Bucket
```bash
# Create a bucket (us-east-1 does not need LocationConstraint)
aws s3api create-bucket \
--bucket my-bucket \
--bucket my-app-data-prod \
--region us-east-1
# Enable versioning
aws s3api put-bucket-versioning \
--bucket my-bucket \
--versioning-configuration Status=Enabled
# Create a bucket in another region
aws s3api create-bucket \
--bucket my-app-data-dr \
--region us-west-2 \
--create-bucket-configuration LocationConstraint=us-west-2
# Block public access
# Block ALL public access (always do this first)
aws s3api put-public-access-block \
--bucket my-bucket \
--bucket my-app-data-prod \
--public-access-block-configuration '{
"BlockPublicAcls": true,
"IgnorePublicAcls": true,
"BlockPublicPolicy": true,
"RestrictPublicBuckets": true
}'
# Enable versioning
aws s3api put-bucket-versioning \
--bucket my-app-data-prod \
--versioning-configuration Status=Enabled
# Enable server-side encryption with SSE-KMS
aws s3api put-bucket-encryption \
--bucket my-app-data-prod \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "alias/s3-key"
},
"BucketKeyEnabled": true
}]
}'
# Enable access logging
aws s3api put-bucket-logging \
--bucket my-app-data-prod \
--bucket-logging-status '{
"LoggingEnabled": {
"TargetBucket": "my-access-logs-bucket",
"TargetPrefix": "s3-logs/my-app-data-prod/"
}
}'
# Add tags
aws s3api put-bucket-tagging \
--bucket my-app-data-prod \
--tagging '{
"TagSet": [
{"Key": "Environment", "Value": "production"},
{"Key": "Team", "Value": "platform"},
{"Key": "DataClassification", "Value": "confidential"}
]
}'
```
## Bucket Policy
## Bucket Policies
```bash
# Apply a bucket policy (enforce HTTPS and restrict to VPC endpoint)
aws s3api put-bucket-policy \
--bucket my-app-data-prod \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-app-data-prod",
"arn:aws:s3:::my-app-data-prod/*"
],
"Condition": {
"Bool": {"aws:SecureTransport": "false"}
}
},
{
"Sid": "RestrictToVPCEndpoint",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-app-data-prod",
"arn:aws:s3:::my-app-data-prod/*"
],
"Condition": {
"StringNotEquals": {
"aws:sourceVpce": "vpce-abc123"
}
}
}
]
}'
```
Cross-account access policy:
```json
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "EnforceHTTPS",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {"aws:SecureTransport": "false"}
"Statement": [
{
"Sid": "CrossAccountRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::987654321098:role/DataAnalystRole"
},
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-data-prod",
"arn:aws:s3:::my-app-data-prod/shared/*"
]
}
}]
]
}
```
## Lifecycle Rules
```bash
# Apply a comprehensive lifecycle configuration
aws s3api put-bucket-lifecycle-configuration \
--bucket my-bucket \
--bucket my-app-data-prod \
--lifecycle-configuration '{
"Rules": [{
"ID": "Archive old objects",
"Status": "Enabled",
"Filter": {"Prefix": "logs/"},
"Transitions": [{
"Days": 30,
"StorageClass": "GLACIER"
}],
"Expiration": {"Days": 365}
}]
"Rules": [
{
"ID": "TierDownOldData",
"Status": "Enabled",
"Filter": {"Prefix": "data/"},
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER_IR"},
{"Days": 180, "StorageClass": "GLACIER"},
{"Days": 365, "StorageClass": "DEEP_ARCHIVE"}
]
},
{
"ID": "ExpireLogs",
"Status": "Enabled",
"Filter": {"Prefix": "logs/"},
"Expiration": {"Days": 90},
"Transitions": [
{"Days": 7, "StorageClass": "STANDARD_IA"},
{"Days": 30, "StorageClass": "GLACIER"}
]
},
{
"ID": "CleanupOldVersions",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"NoncurrentVersionTransitions": [
{"NoncurrentDays": 30, "StorageClass": "STANDARD_IA"},
{"NoncurrentDays": 90, "StorageClass": "GLACIER"}
],
"NoncurrentVersionExpiration": {"NoncurrentDays": 180}
},
{
"ID": "AbortIncompleteUploads",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7}
},
{
"ID": "ExpireDeleteMarkers",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"Expiration": {"ExpiredObjectDeleteMarker": true}
}
]
}'
```
## Best Practices
## Cross-Region Replication
- Enable versioning
- Block public access
- Use encryption (SSE-S3 or SSE-KMS)
- Implement lifecycle policies
- Enable access logging
```bash
# Enable replication (requires versioning on both buckets)
aws s3api put-bucket-replication \
--bucket my-app-data-prod \
--replication-configuration '{
"Role": "arn:aws:iam::123456789012:role/S3ReplicationRole",
"Rules": [
{
"ID": "ReplicateAll",
"Status": "Enabled",
"Priority": 1,
"Filter": {"Prefix": ""},
"Destination": {
"Bucket": "arn:aws:s3:::my-app-data-dr",
"StorageClass": "STANDARD_IA",
"EncryptionConfiguration": {
"ReplicaKmsKeyID": "arn:aws:kms:us-west-2:123456789012:key/dr-key-id"
},
"Metrics": {"Status": "Enabled", "EventThreshold": {"Minutes": 15}},
"ReplicationTime": {"Status": "Enabled", "Time": {"Minutes": 15}}
},
"DeleteMarkerReplication": {"Status": "Enabled"},
"SourceSelectionCriteria": {
"SseKmsEncryptedObjects": {"Status": "Enabled"}
}
}
]
}'
# Check replication status
aws s3api head-object \
--bucket my-app-data-prod \
--key data/important-file.json \
--query "ReplicationStatus"
```
## Presigned URLs
```bash
# Generate a presigned URL for downloading (valid 1 hour)
aws s3 presign s3://my-app-data-prod/reports/quarterly.pdf \
--expires-in 3600
# Generate a presigned URL for uploading
aws s3 presign s3://my-app-data-prod/uploads/user-file.zip \
--expires-in 3600
# Presigned URL with specific content type (using the API directly)
aws s3api generate-presigned-url \
--client-method put_object \
--params '{"Bucket":"my-app-data-prod","Key":"uploads/photo.jpg","ContentType":"image/jpeg"}' \
--expires-in 3600
```
## Common S3 Operations
```bash
# Sync a local directory to S3
aws s3 sync ./build s3://my-app-data-prod/static/ \
--delete \
--exclude "*.tmp" \
--cache-control "max-age=31536000" \
--content-encoding "gzip"
# Copy with storage class
aws s3 cp large-archive.tar.gz s3://my-app-data-prod/archives/ \
--storage-class GLACIER_IR
# List objects with size summary
aws s3 ls s3://my-app-data-prod/ --recursive --summarize --human-readable
# Remove all objects with a prefix
aws s3 rm s3://my-app-data-prod/temp/ --recursive
# Get bucket size via CloudWatch (most efficient for large buckets)
aws cloudwatch get-metric-statistics \
--namespace AWS/S3 \
--metric-name BucketSizeBytes \
--dimensions Name=BucketName,Value=my-app-data-prod Name=StorageType,Value=StandardStorage \
--start-time "$(date -u -d '2 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 86400 \
--statistics Average \
--output table
```
## Terraform S3 Bucket
```hcl
resource "aws_s3_bucket" "main" {
bucket = "my-app-data-prod"
tags = {
Environment = "production"
DataClassification = "confidential"
}
}
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_lifecycle_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
id = "tier-down"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
noncurrent_version_transition {
noncurrent_days = 30
storage_class = "GLACIER"
}
noncurrent_version_expiration {
noncurrent_days = 180
}
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
}
resource "aws_s3_bucket_policy" "enforce_https" {
bucket = aws_s3_bucket.main.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "DenyInsecureTransport"
Effect = "Deny"
Principal = "*"
Action = "s3:*"
Resource = [
aws_s3_bucket.main.arn,
"${aws_s3_bucket.main.arn}/*"
]
Condition = {
Bool = { "aws:SecureTransport" = "false" }
}
}]
})
}
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Access Denied on GetObject | Bucket policy or IAM denies access | Check bucket policy, IAM policy, and public access block |
| Access Denied on PutObject | Missing encryption header when required | Add SSE header; check bucket policy encryption conditions |
| 403 on presigned URL | URL expired or wrong region | Regenerate; ensure region matches bucket region |
| Replication not working | Versioning disabled on source or dest | Enable versioning on both buckets |
| Lifecycle not transitioning | Rule filter does not match objects | Verify prefix and tag filters; check rule status |
| Bucket delete fails | Bucket not empty or has versioned objects | Delete all objects and versions first; disable versioning |
| Slow uploads for large files | Single-part upload | Use `aws s3 cp` (auto multipart) or set multipart threshold |
| Cross-account access denied | Both bucket policy AND IAM policy needed | Grant in bucket policy and in caller's IAM policy |
| Object Lock prevents deletion | Governance or compliance mode active | Use governance bypass (with permission) or wait for retention |
## Related Skills
- [terraform-aws](../terraform-aws/) - IaC deployment
- [aws-iam](../aws-iam/) - Access policies
- [aws-iam](../aws-iam/) - Bucket and object access policies
- [aws-vpc](../aws-vpc/) - VPC endpoints for private S3 access
- [aws-cost-optimization](../aws-cost-optimization/) - Storage class optimization
- [terraform-aws](../terraform-aws/) - IaC deployment for S3
- [cloudformation](../cloudformation/) - AWS-native S3 templates
+381 -45
View File
@@ -9,76 +9,412 @@ metadata:
# AWS VPC
Design and manage Virtual Private Cloud networking.
Design and manage Virtual Private Cloud networking for production AWS environments with proper subnet isolation, routing, and security.
## Create VPC
## When to Use This Skill
```bash
# Create VPC
aws ec2 create-vpc --cidr-block 10.0.0.0/16
- Building a new VPC for production, staging, or development
- Setting up public/private subnet architecture across multiple AZs
- Configuring NAT Gateways for private subnet internet access
- Creating security groups and NACLs for network segmentation
- Setting up VPC peering or Transit Gateway for multi-VPC connectivity
- Implementing VPC endpoints for private access to AWS services
- Troubleshooting connectivity issues between resources
# Create subnets
aws ec2 create-subnet \
--vpc-id vpc-xxx \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a
## Prerequisites
# Create internet gateway
aws ec2 create-internet-gateway
aws ec2 attach-internet-gateway --vpc-id vpc-xxx --internet-gateway-id igw-xxx
```
- AWS CLI v2 installed and configured
- IAM permissions: `ec2:*` (or scoped to VPC-related actions)
- CIDR range planning completed (avoid overlaps with on-premises or other VPCs)
- For VPC peering: access to both VPCs (same or different accounts)
## Network Architecture
```
VPC (10.0.0.0/16)
├── Public Subnets
│ ├── 10.0.1.0/24 (us-east-1a)
── 10.0.2.0/24 (us-east-1b)
├── Private Subnets
│ ├── 10.0.11.0/24 (us-east-1a)
── 10.0.12.0/24 (us-east-1b)
VPC (10.0.0.0/16) - 65,536 IPs
├── Public Subnets (internet-facing via IGW)
│ ├── 10.0.1.0/24 (us-east-1a) - 256 IPs - ALBs, NAT GW, bastion
── 10.0.2.0/24 (us-east-1b) - 256 IPs
│ └── 10.0.3.0/24 (us-east-1c) - 256 IPs
├── Private Subnets (app tier, NAT GW for outbound)
── 10.0.11.0/24 (us-east-1a) - 256 IPs - ECS, EC2, Lambda
│ ├── 10.0.12.0/24 (us-east-1b) - 256 IPs
│ └── 10.0.13.0/24 (us-east-1c) - 256 IPs
├── Data Subnets (isolated, no internet)
│ ├── 10.0.21.0/24 (us-east-1a) - 256 IPs - RDS, ElastiCache
│ ├── 10.0.22.0/24 (us-east-1b) - 256 IPs
│ └── 10.0.23.0/24 (us-east-1c) - 256 IPs
├── Internet Gateway
├── NAT Gateway (in public subnet)
── Route Tables
├── NAT Gateways (one per AZ for HA)
── Route Tables (public, private, data)
└── VPC Flow Logs → CloudWatch / S3
```
## Create a VPC with CLI
```bash
# Create the VPC
VPC_ID=$(aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=production-vpc},{Key=Environment,Value=production}]' \
--query 'Vpc.VpcId' --output text)
# Enable DNS support and hostnames
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-support '{"Value":true}'
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames '{"Value":true}'
# Create public subnets
PUB_SUB_A=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-a},{Key=Tier,Value=public}]' \
--query 'Subnet.SubnetId' --output text)
PUB_SUB_B=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.2.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-b},{Key=Tier,Value=public}]' \
--query 'Subnet.SubnetId' --output text)
# Enable auto-assign public IP on public subnets
aws ec2 modify-subnet-attribute --subnet-id $PUB_SUB_A --map-public-ip-on-launch
aws ec2 modify-subnet-attribute --subnet-id $PUB_SUB_B --map-public-ip-on-launch
# Create private subnets
PRIV_SUB_A=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.11.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-a},{Key=Tier,Value=private}]' \
--query 'Subnet.SubnetId' --output text)
PRIV_SUB_B=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.12.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-b},{Key=Tier,Value=private}]' \
--query 'Subnet.SubnetId' --output text)
# Create data subnets (isolated)
DATA_SUB_A=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.21.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=data-a},{Key=Tier,Value=data}]' \
--query 'Subnet.SubnetId' --output text)
DATA_SUB_B=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.22.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=data-b},{Key=Tier,Value=data}]' \
--query 'Subnet.SubnetId' --output text)
```
## Internet Gateway and NAT Gateway
```bash
# Create and attach Internet Gateway
IGW_ID=$(aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=production-igw}]' \
--query 'InternetGateway.InternetGatewayId' --output text)
aws ec2 attach-internet-gateway --vpc-id $VPC_ID --internet-gateway-id $IGW_ID
# Create public route table
PUB_RT=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=public-rt}]' \
--query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route --route-table-id $PUB_RT --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID
aws ec2 associate-route-table --route-table-id $PUB_RT --subnet-id $PUB_SUB_A
aws ec2 associate-route-table --route-table-id $PUB_RT --subnet-id $PUB_SUB_B
# Allocate Elastic IPs for NAT Gateways (one per AZ for HA)
EIP_A=$(aws ec2 allocate-address --domain vpc --query 'AllocationId' --output text)
EIP_B=$(aws ec2 allocate-address --domain vpc --query 'AllocationId' --output text)
# Create NAT Gateways in public subnets
NAT_A=$(aws ec2 create-nat-gateway \
--subnet-id $PUB_SUB_A \
--allocation-id $EIP_A \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=nat-a}]' \
--query 'NatGateway.NatGatewayId' --output text)
NAT_B=$(aws ec2 create-nat-gateway \
--subnet-id $PUB_SUB_B \
--allocation-id $EIP_B \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=nat-b}]' \
--query 'NatGateway.NatGatewayId' --output text)
# Wait for NAT Gateways
aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_A $NAT_B
# Create private route tables (one per AZ for HA NAT)
PRIV_RT_A=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=private-rt-a}]' \
--query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route --route-table-id $PRIV_RT_A --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_A
aws ec2 associate-route-table --route-table-id $PRIV_RT_A --subnet-id $PRIV_SUB_A
PRIV_RT_B=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=private-rt-b}]' \
--query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route --route-table-id $PRIV_RT_B --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_B
aws ec2 associate-route-table --route-table-id $PRIV_RT_B --subnet-id $PRIV_SUB_B
```
## Security Groups
```bash
aws ec2 create-security-group \
--group-name web-sg \
--description "Web server security group" \
--vpc-id vpc-xxx
# ALB security group (public-facing)
ALB_SG=$(aws ec2 create-security-group \
--group-name alb-sg \
--description "Application Load Balancer" \
--vpc-id $VPC_ID \
--query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress --group-id $ALB_SG --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id $ALB_SG --protocol tcp --port 80 --cidr 0.0.0.0/0
# Application security group (only from ALB)
APP_SG=$(aws ec2 create-security-group \
--group-name app-sg \
--description "Application tier" \
--vpc-id $VPC_ID \
--query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress \
--group-id sg-xxx \
--group-id $APP_SG \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
--port 8080 \
--source-group $ALB_SG
# Database security group (only from app tier)
DB_SG=$(aws ec2 create-security-group \
--group-name db-sg \
--description "Database tier" \
--vpc-id $VPC_ID \
--query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress \
--group-id $DB_SG \
--protocol tcp \
--port 5432 \
--source-group $APP_SG
# List all security groups in the VPC
aws ec2 describe-security-groups \
--filters "Name=vpc-id,Values=$VPC_ID" \
--query "SecurityGroups[].{Name:GroupName,ID:GroupId,Description:Description}" \
--output table
```
## NAT Gateway
## VPC Endpoints (Private Access to AWS Services)
```bash
# Allocate EIP
aws ec2 allocate-address --domain vpc
# Gateway endpoint for S3 (free, route-table based)
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids $PRIV_RT_A $PRIV_RT_B \
--tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=s3-endpoint}]'
# Create NAT Gateway
aws ec2 create-nat-gateway \
--subnet-id subnet-public \
--allocation-id eipalloc-xxx
# Gateway endpoint for DynamoDB (free)
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.us-east-1.dynamodb \
--route-table-ids $PRIV_RT_A $PRIV_RT_B
# Interface endpoint for Secrets Manager (ENI-based, has hourly cost)
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.us-east-1.secretsmanager \
--subnet-ids $PRIV_SUB_A $PRIV_SUB_B \
--security-group-ids $APP_SG \
--private-dns-enabled \
--tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=secretsmanager-endpoint}]'
```
## Best Practices
## VPC Flow Logs
- Use multiple AZs
- Separate public/private subnets
- Implement VPC Flow Logs
- Use security groups effectively
- Plan CIDR ranges carefully
```bash
# Enable VPC flow logs to CloudWatch
aws ec2 create-flow-log \
--resource-type VPC \
--resource-ids $VPC_ID \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /vpc/production-flow-logs \
--deliver-logs-permission-arn arn:aws:iam::123456789012:role/VPCFlowLogRole \
--max-aggregation-interval 60 \
--tag-specifications 'ResourceType=vpc-flow-log,Tags=[{Key=Name,Value=production-flow-log}]'
# Enable VPC flow logs to S3 (cheaper for long-term storage)
aws ec2 create-flow-log \
--resource-type VPC \
--resource-ids $VPC_ID \
--traffic-type ALL \
--log-destination-type s3 \
--log-destination arn:aws:s3:::my-flow-logs-bucket/vpc-logs/ \
--max-aggregation-interval 60
```
## VPC Peering
```bash
# Request peering connection
PEERING_ID=$(aws ec2 create-vpc-peering-connection \
--vpc-id vpc-requester \
--peer-vpc-id vpc-accepter \
--peer-owner-id 987654321098 \
--peer-region us-west-2 \
--tag-specifications 'ResourceType=vpc-peering-connection,Tags=[{Key=Name,Value=prod-to-shared}]' \
--query 'VpcPeeringConnection.VpcPeeringConnectionId' --output text)
# Accept peering (from the accepter account/region)
aws ec2 accept-vpc-peering-connection --vpc-peering-connection-id $PEERING_ID
# Add routes in both VPCs
aws ec2 create-route --route-table-id rtb-requester --destination-cidr-block 10.1.0.0/16 --vpc-peering-connection-id $PEERING_ID
aws ec2 create-route --route-table-id rtb-accepter --destination-cidr-block 10.0.0.0/16 --vpc-peering-connection-id $PEERING_ID
```
## Terraform VPC Module
```hcl
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "production-vpc"
Environment = "production"
}
}
resource "aws_subnet" "public" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 1)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-${data.aws_availability_zones.available.names[count.index]}"
Tier = "public"
}
}
resource "aws_subnet" "private" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 11)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "private-${data.aws_availability_zones.available.names[count.index]}"
Tier = "private"
}
}
resource "aws_subnet" "data" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 21)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "data-${data.aws_availability_zones.available.names[count.index]}"
Tier = "data"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "production-igw" }
}
resource "aws_eip" "nat" {
count = 2
domain = "vpc"
tags = { Name = "nat-eip-${count.index}" }
}
resource "aws_nat_gateway" "main" {
count = 2
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = { Name = "nat-${count.index}" }
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = { Name = "public-rt" }
}
resource "aws_route_table_association" "public" {
count = 3
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
count = 2
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
tags = { Name = "private-rt-${count.index}" }
}
resource "aws_route_table_association" "private" {
count = 2
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
route_table_ids = aws_route_table.private[*].id
tags = { Name = "s3-endpoint" }
}
resource "aws_flow_log" "main" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL"
log_destination_type = "s3"
log_destination = "${aws_s3_bucket.flow_logs.arn}/vpc-logs/"
max_aggregation_interval = 60
}
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Cannot reach internet from private subnet | NAT Gateway route missing | Add 0.0.0.0/0 route to NAT GW in private route table |
| Cannot reach internet from public subnet | IGW not attached or route missing | Attach IGW; add 0.0.0.0/0 route to IGW in public RT |
| EC2 cannot reach S3 | No VPC endpoint or NAT | Add S3 gateway endpoint (free) or ensure NAT GW route |
| Security group rule not working | Wrong direction (ingress vs egress) | SG is stateful; check inbound rule on destination |
| NACL blocking traffic | NACLs are stateless; need both directions | Add matching inbound AND outbound rules with correct ports |
| VPC peering one-way only | Routes missing in one VPC | Add routes in BOTH VPC route tables |
| DNS resolution failing | DNS hostnames not enabled on VPC | Enable `enableDnsHostnames` on VPC |
| NAT Gateway charges high | All AZs routing through one NAT | Deploy NAT GW per AZ with separate route tables |
| Cross-AZ data transfer costs | Resources in different AZs communicating | Co-locate tightly coupled services in same AZ |
## Related Skills
- [terraform-aws](../terraform-aws/) - IaC deployment
- [firewall-config](../../../security/network/firewall-config/) - Security
- [aws-ec2](../aws-ec2/) - Instances deployed in VPC subnets
- [aws-ecs-fargate](../aws-ecs-fargate/) - ECS tasks in VPC networking
- [aws-rds](../aws-rds/) - Database subnet groups
- [terraform-aws](../terraform-aws/) - IaC for VPC infrastructure
- [firewall-config](../../../security/network/firewall-config/) - Network security controls
+393 -41
View File
@@ -9,85 +9,437 @@ metadata:
# CloudFormation
Deploy AWS infrastructure with native CloudFormation templates.
Deploy AWS infrastructure with native CloudFormation templates, change sets, nested stacks, and drift detection.
## When to Use This Skill
- Deploying AWS resources using AWS-native Infrastructure as Code
- Creating repeatable, parameterized infrastructure templates
- Managing multi-environment deployments (dev, staging, prod) with the same template
- Implementing safe deployments with change sets and rollback protection
- Detecting and remediating configuration drift
- Organizing large infrastructure into nested stacks
- Exporting/importing values between stacks
## Prerequisites
- AWS CLI v2 installed and configured
- IAM permissions: `cloudformation:*`, plus permissions for all resources in the template
- (Optional) `cfn-lint` installed for template validation (`pip install cfn-lint`)
- S3 bucket for storing templates larger than 51,200 bytes
## Template Structure
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Web application stack
Description: Production web application infrastructure
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label: { default: "Environment" }
Parameters: [Environment, InstanceType]
- Label: { default: "Network" }
Parameters: [VpcId, SubnetIds]
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
Default: dev
InstanceType:
Type: String
Default: t3.micro
AllowedValues: [t3.micro, t3.small, t3.medium, t3.large]
VpcId:
Type: AWS::EC2::VPC::Id
Description: VPC to deploy into
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: Subnets for the application
Conditions:
IsProd: !Equals [!Ref Environment, prod]
CreateReadReplica: !Equals [!Ref Environment, prod]
Mappings:
RegionAMI:
us-east-1:
AL2023: ami-0abcdef1234567890
us-west-2:
AL2023: ami-0fedcba9876543210
Resources:
WebServer:
Type: AWS::EC2::Instance
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
ImageId: !Ref AMI
InstanceType: t3.micro
GroupDescription: !Sub '${Environment}-web-sg'
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${Environment}-web-sg'
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: !Sub '${Environment}-web'
LaunchTemplateData:
ImageId: !FindInMap [RegionAMI, !Ref 'AWS::Region', AL2023]
InstanceType: !If [IsProd, t3.large, !Ref InstanceType]
MetadataOptions:
HttpTokens: required
SecurityGroupIds:
- !Ref SecurityGroup
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: !Sub '${Environment}-web-asg'
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
MinSize: !If [IsProd, 2, 1]
MaxSize: !If [IsProd, 10, 3]
DesiredCapacity: !If [IsProd, 4, 1]
VPCZoneIdentifier: !Ref SubnetIds
TargetGroupARNs:
- !Ref TargetGroup
HealthCheckType: ELB
HealthCheckGracePeriod: 300
Tags:
- Key: Name
Value: !Sub '${Environment}-web'
PropagateAtLaunch: true
UpdatePolicy:
AutoScalingRollingUpdate:
MinInstancesInService: !If [IsProd, 2, 0]
MaxBatchSize: 1
PauseTime: PT5M
WaitOnResourceSignals: true
SuspendProcesses:
- HealthCheck
- ReplaceUnhealthy
- AZRebalance
- AlarmNotification
- ScheduledActions
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${Environment}-web-tg'
Port: 8080
Protocol: HTTP
VpcId: !Ref VpcId
TargetType: instance
HealthCheckPath: /health
HealthCheckIntervalSeconds: 30
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Outputs:
InstanceId:
Value: !Ref WebServer
SecurityGroupId:
Description: Web security group ID
Value: !Ref SecurityGroup
Export:
Name: !Sub '${Environment}-WebServerId'
Name: !Sub '${Environment}-WebSecurityGroup'
AutoScalingGroupName:
Description: ASG name
Value: !Ref AutoScalingGroup
Export:
Name: !Sub '${Environment}-WebASG'
```
## Stack Operations
```bash
# Create stack
# Validate a template
aws cloudformation validate-template --template-body file://template.yaml
# Lint with cfn-lint (catches more issues)
cfn-lint template.yaml
# Create a stack
aws cloudformation create-stack \
--stack-name myapp \
--stack-name production-web \
--template-body file://template.yaml \
--parameters ParameterKey=Environment,ParameterValue=prod
--parameters \
ParameterKey=Environment,ParameterValue=prod \
ParameterKey=VpcId,ParameterValue=vpc-abc123 \
ParameterKey=SubnetIds,ParameterValue="subnet-aaa\\,subnet-bbb" \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
--tags Key=Environment,Value=production Key=Team,Value=platform \
--enable-termination-protection \
--on-failure ROLLBACK
# Update stack
aws cloudformation update-stack \
--stack-name myapp \
--template-body file://template.yaml
# Wait for stack creation
aws cloudformation wait stack-create-complete --stack-name production-web
# Delete stack
aws cloudformation delete-stack --stack-name myapp
# Describe stack status and outputs
aws cloudformation describe-stacks \
--stack-name production-web \
--query "Stacks[0].{Status:StackStatus,Outputs:Outputs}" \
--output table
# Detect drift
aws cloudformation detect-stack-drift --stack-name myapp
# List stack resources
aws cloudformation list-stack-resources --stack-name production-web \
--query "StackResourceSummaries[].{Logical:LogicalResourceId,Physical:PhysicalResourceId,Type:ResourceType,Status:ResourceStatus}" \
--output table
# Delete a stack
aws cloudformation delete-stack --stack-name dev-web
aws cloudformation wait stack-delete-complete --stack-name dev-web
```
## Intrinsic Functions
## Change Sets (Safe Updates)
```bash
# Create a change set to preview changes before applying
aws cloudformation create-change-set \
--stack-name production-web \
--change-set-name update-instance-type \
--template-body file://template.yaml \
--parameters \
ParameterKey=Environment,ParameterValue=prod \
ParameterKey=InstanceType,ParameterValue=t3.large \
ParameterKey=VpcId,UsePreviousValue=true \
ParameterKey=SubnetIds,UsePreviousValue=true \
--capabilities CAPABILITY_IAM
# Describe the change set to review planned changes
aws cloudformation describe-change-set \
--stack-name production-web \
--change-set-name update-instance-type \
--query "Changes[].{Action:ResourceChange.Action,Resource:ResourceChange.LogicalResourceId,Type:ResourceChange.ResourceType,Replacement:ResourceChange.Replacement}" \
--output table
# Execute the change set (apply changes)
aws cloudformation execute-change-set \
--stack-name production-web \
--change-set-name update-instance-type
# Wait for update
aws cloudformation wait stack-update-complete --stack-name production-web
# Delete a change set without applying
aws cloudformation delete-change-set \
--stack-name production-web \
--change-set-name update-instance-type
```
## Drift Detection
```bash
# Start drift detection
DRIFT_ID=$(aws cloudformation detect-stack-drift \
--stack-name production-web \
--query 'StackDriftDetectionId' --output text)
# Check drift detection status
aws cloudformation describe-stack-drift-detection-status \
--stack-drift-detection-id $DRIFT_ID
# View drifted resources
aws cloudformation describe-stack-resource-drifts \
--stack-name production-web \
--stack-resource-drift-status-filters MODIFIED DELETED \
--query "StackResourceDrifts[].{Resource:LogicalResourceId,Status:StackResourceDriftStatus,Differences:PropertyDifferences}" \
--output table
# Detect drift on a specific resource
aws cloudformation detect-stack-resource-drift \
--stack-name production-web \
--logical-resource-id SecurityGroup
```
## Nested Stacks
Parent template:
```yaml
# Reference
!Ref MyResource
AWSTemplateFormatVersion: '2010-09-09'
Description: Parent stack - full application
# Get attribute
!GetAtt MyResource.Arn
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
# Substitute
!Sub 'arn:aws:s3:::${BucketName}/*'
Resources:
NetworkStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/my-cfn-templates/network.yaml
Parameters:
Environment: !Ref Environment
VpcCidr: "10.0.0.0/16"
Tags:
- Key: Environment
Value: !Ref Environment
# Conditional
!If [CreateProdResources, 't3.large', 't3.micro']
DatabaseStack:
Type: AWS::CloudFormation::Stack
DependsOn: NetworkStack
Properties:
TemplateURL: https://s3.amazonaws.com/my-cfn-templates/database.yaml
Parameters:
Environment: !Ref Environment
VpcId: !GetAtt NetworkStack.Outputs.VpcId
SubnetIds: !GetAtt NetworkStack.Outputs.PrivateSubnetIds
# Join
!Join ['-', [!Ref Environment, 'app', 'bucket']]
AppStack:
Type: AWS::CloudFormation::Stack
DependsOn: [NetworkStack, DatabaseStack]
Properties:
TemplateURL: https://s3.amazonaws.com/my-cfn-templates/app.yaml
Parameters:
Environment: !Ref Environment
VpcId: !GetAtt NetworkStack.Outputs.VpcId
SubnetIds: !GetAtt NetworkStack.Outputs.PrivateSubnetIds
DbEndpoint: !GetAtt DatabaseStack.Outputs.Endpoint
Outputs:
VpcId:
Value: !GetAtt NetworkStack.Outputs.VpcId
AppUrl:
Value: !GetAtt AppStack.Outputs.LoadBalancerDNS
```
## Best Practices
```bash
# Package nested templates (uploads local references to S3)
aws cloudformation package \
--template-file parent.yaml \
--s3-bucket my-cfn-templates \
--output-template-file packaged.yaml
- Use change sets before updates
- Implement stack policies
- Use nested stacks for modularity
- Enable termination protection
- Use cfn-lint for validation
# Deploy the packaged template
aws cloudformation deploy \
--template-file packaged.yaml \
--stack-name production-app \
--parameter-overrides Environment=prod \
--capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND \
--tags Environment=production
```
## Intrinsic Functions Reference
```yaml
# Ref - reference a parameter or resource
SecurityGroupId: !Ref SecurityGroup
# GetAtt - get an attribute of a resource
SecurityGroupArn: !GetAtt SecurityGroup.GroupId
# Sub - string substitution
BucketName: !Sub '${Environment}-${AWS::AccountId}-data'
# Join - concatenate strings
PolicyArn: !Join ['', ['arn:aws:iam::', !Ref 'AWS::AccountId', ':policy/MyPolicy']]
# Select - pick from a list
FirstSubnet: !Select [0, !Ref SubnetIds]
# Split - split a string
FirstPart: !Select [0, !Split ['-', !Ref 'AWS::StackName']]
# If - conditional value
InstanceSize: !If [IsProd, t3.large, t3.micro]
# Equals - condition definition
Conditions:
IsProd: !Equals [!Ref Environment, prod]
# ImportValue - cross-stack reference
VpcId: !ImportValue production-VpcId
# Cidr - generate CIDR blocks
Subnets: !Cidr [!GetAtt VPC.CidrBlock, 6, 8]
# GetAZs - list availability zones
AZ: !Select [0, !GetAZs '']
```
## Stack Policy (Prevent Accidental Replacements)
```bash
# Apply a stack policy that prevents replacement of the database
aws cloudformation set-stack-policy \
--stack-name production-web \
--stack-policy-body '{
"Statement": [
{
"Effect": "Allow",
"Action": "Update:*",
"Principal": "*",
"Resource": "*"
},
{
"Effect": "Deny",
"Action": "Update:Replace",
"Principal": "*",
"Resource": "LogicalResourceId/Database"
},
{
"Effect": "Deny",
"Action": "Update:Delete",
"Principal": "*",
"Resource": "LogicalResourceId/Database"
}
]
}'
```
## Stack Events and Debugging
```bash
# View stack events (most recent first)
aws cloudformation describe-stack-events \
--stack-name production-web \
--query "StackEvents[?ResourceStatus=='CREATE_FAILED' || ResourceStatus=='UPDATE_FAILED'].{Time:Timestamp,Resource:LogicalResourceId,Status:ResourceStatus,Reason:ResourceStatusReason}" \
--output table
# Continue a rollback that is stuck
aws cloudformation continue-update-rollback \
--stack-name production-web \
--resources-to-skip SecurityGroup
# Cancel an in-progress update
aws cloudformation cancel-update-stack --stack-name production-web
# Get template from an existing stack
aws cloudformation get-template \
--stack-name production-web \
--template-stage Processed \
--query TemplateBody \
--output text > current-template.yaml
```
## Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| CREATE_FAILED on IAM resource | Missing CAPABILITY_IAM | Add `--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM` |
| Stack stuck in UPDATE_ROLLBACK_FAILED | Resource cannot be rolled back | Use `continue-update-rollback` with `--resources-to-skip` |
| Nested stack fails | Template URL wrong or S3 access denied | Use `aws cloudformation package` to upload; check bucket policy |
| Circular dependency error | Two resources reference each other | Break the cycle with a third resource or use `DependsOn` |
| Drift detected | Manual changes made outside CloudFormation | Re-apply the template or update template to match current state |
| Change set shows no changes | Template and parameters identical | Verify the diff; check if the change is parameter-only |
| Template validation error | YAML syntax or invalid resource property | Run `cfn-lint`; check property names against docs |
| Export name already exists | Another stack uses the same export name | Use unique export names with `!Sub '${AWS::StackName}-Name'` |
| Delete fails - resource in use | Dependent resource outside the stack | Remove the dependency first; check for SG references |
## Related Skills
- [terraform-aws](../terraform-aws/) - Alternative IaC
- [aws-iam](../aws-iam/) - IAM resources
- [terraform-aws](../terraform-aws/) - Alternative IaC with Terraform
- [aws-iam](../aws-iam/) - IAM resources in templates
- [aws-vpc](../aws-vpc/) - Network infrastructure templates
- [aws-ec2](../aws-ec2/) - Compute resources in templates
- [aws-s3](../aws-s3/) - Storage resources in templates