mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
V2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user