Introduction
AWS provides 200+ services — and Ansible's amazon.aws and community.aws collections provide modules for the ones that matter most: EC2 for compute, RDS for databases, S3 for storage, VPC for networking, and IAM for security. While Terraform excels at provisioning cloud resources with state tracking, Ansible shines at combining provisioning with configuration — create an EC2 instance AND install your application in the same playbook.
Setup
# Install AWS collections
ansible-galaxy collection install amazon.aws community.aws
# Python dependencies
pip install boto3 botocore
Authentication
# Option 1: Environment variables (recommended for CI/CD)
# export AWS_ACCESS_KEY_ID=AKIA...
# export AWS_SECRET_ACCESS_KEY=...
# export AWS_REGION=us-east-1
# Option 2: Playbook variables (for AAP credential injection)
# group_vars/all/aws.yml
aws_region: us-east-1
# aws_access_key and aws_secret_key from Vault/AAP credentials
# Option 3: AWS CLI profile
# export AWS_PROFILE=production
Pattern 1: Complete VPC with Subnets
---
- name: Create production VPC infrastructure
hosts: localhost
connection: local
vars:
vpc_cidr: 10.0.0.0/16
project: myapp
environment: production
az_count: 3
public_subnets:
- { cidr: "10.0.1.0/24", az: "a" }
- { cidr: "10.0.2.0/24", az: "b" }
- { cidr: "10.0.3.0/24", az: "c" }
private_subnets:
- { cidr: "10.0.11.0/24", az: "a" }
- { cidr: "10.0.12.0/24", az: "b" }
- { cidr: "10.0.13.0/24", az: "c" }
tasks:
- name: Create VPC
amazon.aws.ec2_vpc_net:
name: "{{ project }}-{{ environment }}-vpc"
cidr_block: "{{ vpc_cidr }}"
region: "{{ aws_region }}"
tags:
Project: "{{ project }}"
Environment: "{{ environment }}"
ManagedBy: ansible
register: vpc
- name: Create Internet Gateway
amazon.aws.ec2_vpc_igw:
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ aws_region }}"
tags:
Name: "{{ project }}-{{ environment }}-igw"
register: igw
- name: Create public subnets
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ vpc.vpc.id }}"
cidr: "{{ item.cidr }}"
az: "{{ aws_region }}{{ item.az }}"
region: "{{ aws_region }}"
map_public: true
tags:
Name: "{{ project }}-{{ environment }}-public-{{ item.az }}"
Tier: public
loop: "{{ public_subnets }}"
register: public_subnet_results
- name: Create private subnets
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ vpc.vpc.id }}"
cidr: "{{ item.cidr }}"
az: "{{ aws_region }}{{ item.az }}"
region: "{{ aws_region }}"
tags:
Name: "{{ project }}-{{ environment }}-private-{{ item.az }}"
Tier: private
loop: "{{ private_subnets }}"
register: private_subnet_results
- name: Create NAT Gateway
amazon.aws.ec2_vpc_nat_gateway:
subnet_id: "{{ public_subnet_results.results[0].subnet.id }}"
region: "{{ aws_region }}"
if_exist_do_not_create: true
wait: true
tags:
Name: "{{ project }}-{{ environment }}-nat"
register: nat_gw
- name: Create public route table
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ aws_region }}"
subnets: "{{ public_subnet_results.results | map(attribute='subnet.id') | list }}"
routes:
- dest: 0.0.0.0/0
gateway_id: "{{ igw.gateway_id }}"
tags:
Name: "{{ project }}-{{ environment }}-public-rt"
- name: Create private route table
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ aws_region }}"
subnets: "{{ private_subnet_results.results | map(attribute='subnet.id') | list }}"
routes:
- dest: 0.0.0.0/0
nat_gateway_id: "{{ nat_gw.nat_gateway_id }}"
tags:
Name: "{{ project }}-{{ environment }}-private-rt"
Pattern 2: EC2 Fleet with Auto Scaling
---
- name: Deploy EC2 web fleet
hosts: localhost
connection: local
vars:
instance_type: t3.medium
ami_id: ami-0c55b159cbfafe1f0
key_name: deploy-key
min_size: 2
max_size: 10
desired_size: 3
tasks:
- name: Create security group
amazon.aws.ec2_security_group:
name: "{{ project }}-web-sg"
description: "Web server security group"
vpc_id: "{{ vpc_id }}"
region: "{{ aws_region }}"
rules:
- proto: tcp
from_port: 80
to_port: 80
cidr_ip: 0.0.0.0/0
rule_desc: "HTTP"
- proto: tcp
from_port: 443
to_port: 443
cidr_ip: 0.0.0.0/0
rule_desc: "HTTPS"
- proto: tcp
from_port: 22
to_port: 22
cidr_ip: 10.0.0.0/16
rule_desc: "SSH from VPC"
rules_egress:
- proto: all
cidr_ip: 0.0.0.0/0
register: web_sg
- name: Create launch template
community.aws.ec2_launch_template:
name: "{{ project }}-web-lt"
region: "{{ aws_region }}"
image_id: "{{ ami_id }}"
instance_type: "{{ instance_type }}"
key_name: "{{ key_name }}"
security_group_ids:
- "{{ web_sg.group_id }}"
user_data: "{{ lookup('file', 'scripts/user-data.sh') | b64encode }}"
instance_initiated_shutdown_behavior: terminate
block_device_mappings:
- device_name: /dev/xvda
ebs:
volume_size: 50
volume_type: gp3
encrypted: true
tag_specifications:
- resource_type: instance
tags:
Name: "{{ project }}-web"
Environment: "{{ environment }}"
- name: Create Application Load Balancer
community.aws.elb_application_lb:
name: "{{ project }}-web-alb"
region: "{{ aws_region }}"
subnets: "{{ public_subnet_ids }}"
security_groups:
- "{{ web_sg.group_id }}"
listeners:
- Protocol: HTTPS
Port: 443
SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
Certificates:
- CertificateArn: "{{ acm_cert_arn }}"
DefaultActions:
- Type: forward
TargetGroupName: "{{ project }}-web-tg"
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: redirect
RedirectConfig:
Protocol: HTTPS
Port: "443"
StatusCode: HTTP_301
state: present
register: alb
- name: Create Auto Scaling Group
community.aws.autoscaling_group:
name: "{{ project }}-web-asg"
region: "{{ aws_region }}"
launch_template:
launch_template_name: "{{ project }}-web-lt"
version: "$Latest"
min_size: "{{ min_size }}"
max_size: "{{ max_size }}"
desired_capacity: "{{ desired_size }}"
vpc_zone_identifier: "{{ private_subnet_ids }}"
target_group_arns:
- "{{ target_group_arn }}"
health_check_type: ELB
health_check_grace_period: 300
tags:
- key: Name
value: "{{ project }}-web"
propagate_at_launch: true
Pattern 3: RDS Database
---
- name: Provision RDS PostgreSQL
hosts: localhost
connection: local
tasks:
- name: Create DB subnet group
amazon.aws.rds_subnet_group:
name: "{{ project }}-db-subnet"
description: "Database subnet group"
subnets: "{{ private_subnet_ids }}"
region: "{{ aws_region }}"
- name: Create DB security group
amazon.aws.ec2_security_group:
name: "{{ project }}-db-sg"
description: "Database security group"
vpc_id: "{{ vpc_id }}"
region: "{{ aws_region }}"
rules:
- proto: tcp
from_port: 5432
to_port: 5432
group_id: "{{ web_sg_id }}"
rule_desc: "PostgreSQL from web servers"
- name: Create RDS instance
amazon.aws.rds_instance:
db_instance_identifier: "{{ project }}-{{ environment }}-db"
region: "{{ aws_region }}"
engine: postgres
engine_version: "16.1"
db_instance_class: db.r6g.xlarge
allocated_storage: 100
max_allocated_storage: 500
storage_type: gp3
storage_encrypted: true
master_username: "{{ db_master_user }}"
master_user_password: "{{ db_master_password }}"
db_name: appdb
db_subnet_group_name: "{{ project }}-db-subnet"
vpc_security_group_ids:
- "{{ db_sg_id }}"
multi_az: true
backup_retention_period: 30
preferred_backup_window: "03:00-04:00"
preferred_maintenance_window: "sun:04:00-sun:05:00"
auto_minor_version_upgrade: true
copy_tags_to_snapshot: true
deletion_protection: true
tags:
Project: "{{ project }}"
Environment: "{{ environment }}"
register: rds
- name: Output connection info
ansible.builtin.debug:
msg: "DB endpoint: {{ rds.endpoint.address }}:{{ rds.endpoint.port }}"
Pattern 4: S3 with Lifecycle Policies
---
- name: Configure S3 buckets
hosts: localhost
connection: local
tasks:
- name: Create application assets bucket
amazon.aws.s3_bucket:
name: "{{ project }}-{{ environment }}-assets"
region: "{{ aws_region }}"
versioning: true
encryption: AES256
public_access:
block_public_acls: true
block_public_policy: true
ignore_public_acls: true
restrict_public_buckets: true
tags:
Project: "{{ project }}"
- name: Configure lifecycle policy
community.aws.s3_lifecycle:
name: "{{ project }}-{{ environment }}-assets"
region: "{{ aws_region }}"
rules:
- id: archive-old-versions
status: enabled
noncurrent_version_transitions:
- transition_days: 30
storage_class: STANDARD_IA
- transition_days: 90
storage_class: GLACIER
noncurrent_version_expiration_days: 365
- id: abort-incomplete-uploads
status: enabled
prefix: ""
abort_incomplete_multipart_upload_days: 7
- name: Create backup bucket with cross-region replication
amazon.aws.s3_bucket:
name: "{{ project }}-{{ environment }}-backups"
region: "{{ aws_region }}"
versioning: true
encryption: AES256
public_access:
block_public_acls: true
block_public_policy: true
ignore_public_acls: true
restrict_public_buckets: true
Pattern 5: IAM Roles and Policies
---
- name: Configure IAM for application
hosts: localhost
connection: local
tasks:
- name: Create EC2 instance role
community.aws.iam_role:
name: "{{ project }}-ec2-role"
assume_role_policy_document: |
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
managed_policies:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
tags:
Project: "{{ project }}"
- name: Create custom S3 access policy
community.aws.iam_policy:
iam_type: role
iam_name: "{{ project }}-ec2-role"
policy_name: "{{ project }}-s3-access"
policy_json: |
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::{{ project }}-{{ environment }}-assets",
"arn:aws:s3:::{{ project }}-{{ environment }}-assets/*"
]
}]
}
- name: Create instance profile
community.aws.iam_instance_profile:
name: "{{ project }}-ec2-profile"
role: "{{ project }}-ec2-role"
AWS Dynamic Inventory
# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
- eu-west-1
filters:
tag:ManagedBy: ansible
instance-state-name: running
keyed_groups:
- key: tags.Environment
prefix: env
- key: tags.Role
prefix: role
- key: placement.region
prefix: region
hostnames:
- private-ip-address
compose:
ansible_user: "'ec2-user'"
Cost Optimization Playbook
---
- name: AWS cost optimization checks
hosts: localhost
connection: local
tasks:
- name: Find unattached EBS volumes
amazon.aws.ec2_vol_info:
region: "{{ aws_region }}"
filters:
status: available
register: unattached_volumes
- name: Report unattached volumes
ansible.builtin.debug:
msg: "Unattached volume {{ item.id }}: {{ item.size }}GB {{ item.volume_type }} ({{ item.tags.Name | default('unnamed') }})"
loop: "{{ unattached_volumes.volumes }}"
when: unattached_volumes.volumes | length > 0
- name: Find stopped instances running > 7 days
amazon.aws.ec2_instance_info:
region: "{{ aws_region }}"
filters:
instance-state-name: stopped
register: stopped_instances
- name: Report stopped instances
ansible.builtin.debug:
msg: "Stopped instance {{ item.instance_id }} ({{ item.instance_type }}): {{ item.tags.Name | default('unnamed') }}"
loop: "{{ stopped_instances.instances }}"
Troubleshooting
Credential Errors
# Verify AWS credentials
aws sts get-caller-identity
# Should return account ID and ARN
# Check boto3 version
python3 -c "import boto3; print(boto3.__version__)"
Timeout on Large Operations
# Increase module timeout
- name: Create large RDS instance
amazon.aws.rds_instance:
# ...
wait: true
wait_timeout: 1800 # 30 minutes
Related Articles
- AWS EC2 AMI Module
- Ansible vs Terraform
- Ansible + Terraform Pipeline
- Ansible Best Practices Guide
- Managing Credentials with Vault
Conclusion
Use amazon.aws for core services (EC2, VPC, RDS, S3, IAM) and community.aws for extended services (ALB, Auto Scaling, launch templates). Authenticate via environment variables in CI/CD or AAP credential injection in production. For enterprise deployments, combine VPC networking, security groups, EC2 fleets with auto scaling, encrypted RDS with multi-AZ, and least-privilege IAM roles — all version-controlled and deployed through a single Ansible playbook or AAP workflow.