Ansible for AWS: Complete Guide to Cloud Automation with EC2, S3, RDS, and More
By Luca Berton · Published 2024-01-01 · Category: installation
Automate AWS with Ansible. Provision EC2 instances, manage S3 buckets, configure RDS databases, deploy VPCs, set up ELBs, and orchestrate complete AWS.
Getting Started with Ansible + AWS
# Install AWS collection and dependencies
ansible-galaxy collection install amazon.aws
pip install boto3 botocore
# Configure credentials
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_DEFAULT_REGION="us-east-1"
# Or use ~/.aws/credentials (preferred)
aws configure
See also: Ansible AWS: Complete Guide to Cloud Automation (2026)
VPC and Networking
---
- name: Create AWS VPC infrastructure
hosts: localhost
connection: local
vars:
project: myapp
region: us-east-1
vpc_cidr: 10.0.0.0/16
public_subnets:
- { cidr: "10.0.1.0/24", az: "us-east-1a" }
- { cidr: "10.0.2.0/24", az: "us-east-1b" }
private_subnets:
- { cidr: "10.0.10.0/24", az: "us-east-1a" }
- { cidr: "10.0.11.0/24", az: "us-east-1b" }
tasks:
- name: Create VPC
amazon.aws.ec2_vpc_net:
name: "{{ project }}-vpc"
cidr_block: "{{ vpc_cidr }}"
region: "{{ region }}"
tags:
Project: "{{ project }}"
Environment: production
register: vpc
- name: Create internet gateway
amazon.aws.ec2_vpc_igw:
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ region }}"
tags:
Name: "{{ project }}-igw"
register: igw
- name: Create public subnets
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ vpc.vpc.id }}"
cidr: "{{ item.cidr }}"
az: "{{ item.az }}"
map_public: true
region: "{{ region }}"
tags:
Name: "{{ project }}-public-{{ item.az }}"
Type: 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: "{{ item.az }}"
region: "{{ region }}"
tags:
Name: "{{ project }}-private-{{ item.az }}"
Type: 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: "{{ region }}"
if_exist_do_not_create: true
tags:
Name: "{{ project }}-nat"
register: nat_gw
- name: Create public route table
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ 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 }}-public-rt"
- name: Create private route table
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ 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 }}-private-rt"
Security Groups
- name: Create web security group
amazon.aws.ec2_security_group:
name: "{{ project }}-web-sg"
description: "Web server security group"
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ 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"
tags:
Name: "{{ project }}-web-sg"
register: web_sg
- name: Create database security group
amazon.aws.ec2_security_group:
name: "{{ project }}-db-sg"
description: "Database security group"
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ region }}"
rules:
- proto: tcp
from_port: 5432
to_port: 5432
group_id: "{{ web_sg.group_id }}"
rule_desc: "PostgreSQL from web servers"
tags:
Name: "{{ project }}-db-sg"
register: db_sg
See also: Ansible AWS EC2: Automate Ubuntu Instance Creation & Data Collection
EC2 Instances
- name: Create EC2 key pair
amazon.aws.ec2_key:
name: "{{ project }}-key"
region: "{{ region }}"
register: ec2_key
- name: Save private key
ansible.builtin.copy:
content: "{{ ec2_key.key.private_key }}"
dest: "~/.ssh/{{ project }}-key.pem"
mode: '0600'
when: ec2_key.changed
- name: Launch web server instances
amazon.aws.ec2_instance:
name: "{{ project }}-web-{{ item }}"
instance_type: t3.medium
image_id: ami-0c7217cdde317cfec # Ubuntu 22.04
key_name: "{{ project }}-key"
vpc_subnet_id: "{{ public_subnet_results.results[item | int - 1].subnet.id }}"
security_groups:
- "{{ web_sg.group_id }}"
network:
assign_public_ip: true
volumes:
- device_name: /dev/sda1
ebs:
volume_size: 50
volume_type: gp3
encrypted: true
tags:
Project: "{{ project }}"
Role: web
Environment: production
user_data: |
#!/bin/bash
apt-get update && apt-get install -y python3
wait: true
region: "{{ region }}"
loop: ["1", "2"]
register: ec2_instances
S3 Buckets
- name: Create S3 bucket
amazon.aws.s3_bucket:
name: "{{ project }}-assets-{{ aws_account_id }}"
region: "{{ 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 bucket lifecycle
amazon.aws.s3_lifecycle:
name: "{{ project }}-assets-{{ aws_account_id }}"
rule_id: "archive-old-versions"
noncurrent_version_transition_days: 30
noncurrent_version_storage_class: GLACIER
noncurrent_version_expiration_days: 365
status: enabled
- name: Upload files to S3
amazon.aws.s3_object:
bucket: "{{ project }}-assets-{{ aws_account_id }}"
object: "config/app.yml"
src: files/app-config.yml
mode: put
encryption: AES256
- name: Sync directory to S3
ansible.builtin.command:
cmd: >
aws s3 sync ./static/
s3://{{ project }}-assets-{{ aws_account_id }}/static/
--delete
changed_when: true
See also: Configuring Ansible for AWS: Setup Guide & Playbook
RDS Database
- name: Create RDS subnet group
amazon.aws.rds_subnet_group:
name: "{{ project }}-db-subnet"
description: "Database subnet group"
subnets: "{{ private_subnet_results.results | map(attribute='subnet.id') | list }}"
region: "{{ region }}"
- name: Create RDS PostgreSQL instance
amazon.aws.rds_instance:
id: "{{ project }}-db"
instance_type: db.r6g.large
engine: postgres
engine_version: "16.4"
master_username: dbadmin
master_user_password: "{{ vault_rds_password }}"
allocated_storage: 100
max_allocated_storage: 500
storage_type: gp3
storage_encrypted: true
multi_az: true
db_subnet_group_name: "{{ project }}-db-subnet"
vpc_security_group_ids:
- "{{ db_sg.group_id }}"
backup_retention_period: 14
preferred_backup_window: "03:00-04:00"
preferred_maintenance_window: "sun:04:00-sun:05:00"
auto_minor_version_upgrade: true
publicly_accessible: false
tags:
Project: "{{ project }}"
Environment: production
region: "{{ region }}"
wait: true
register: rds_instance
- name: Create read replica
amazon.aws.rds_instance:
id: "{{ project }}-db-replica"
instance_type: db.r6g.large
source_db_instance_identifier: "{{ project }}-db"
region: "{{ region }}"
tags:
Project: "{{ project }}"
Role: read-replica
Application Load Balancer
- name: Create target group
community.aws.elb_target_group:
name: "{{ project }}-tg"
protocol: http
port: 80
vpc_id: "{{ vpc.vpc.id }}"
health_check_path: /health
health_check_interval: 30
healthy_threshold_count: 2
unhealthy_threshold_count: 3
targets: "{{ ec2_instances.results | map(attribute='instances') | flatten | map(attribute='instance_id') | map('community.general.dict_kv', 'Id') | list }}"
region: "{{ region }}"
state: present
- name: Create ALB
amazon.aws.elb_application_lb:
name: "{{ project }}-alb"
subnets: "{{ public_subnet_results.results | map(attribute='subnet.id') | list }}"
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 }}-tg"
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: redirect
RedirectConfig:
Protocol: HTTPS
Port: "443"
StatusCode: HTTP_301
region: "{{ region }}"
state: present
IAM Roles
- name: Create EC2 instance role
amazon.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
- arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
tags:
Project: "{{ project }}"
- name: Create S3 access policy
amazon.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:ListBucket"],
"Resource": [
"arn:aws:s3:::{{ project }}-assets-*",
"arn:aws:s3:::{{ project }}-assets-*/*"
]
}]
}
state: present
Complete Infrastructure Teardown
---
- name: Destroy all AWS resources
hosts: localhost
connection: local
tasks:
- name: Terminate EC2 instances
amazon.aws.ec2_instance:
filters:
"tag:Project": "{{ project }}"
state: absent
wait: true
- name: Delete RDS instances
amazon.aws.rds_instance:
id: "{{ item }}"
skip_final_snapshot: true
state: absent
wait: true
loop:
- "{{ project }}-db-replica"
- "{{ project }}-db"
- name: Delete ALB
amazon.aws.elb_application_lb:
name: "{{ project }}-alb"
state: absent
wait: true
# Continue with S3, VPC, etc.
FAQ
Should I use Ansible or Terraform for AWS?
Use both. Terraform excels at infrastructure provisioning (VPCs, EC2, RDS) — it tracks state and handles dependencies well. Ansible excels at configuration management (installing software, deploying apps, managing users). Terraform provisions the EC2 instance; Ansible configures what's on it.
How do I use IAM roles instead of access keys?
If running Ansible on an EC2 instance, attach an IAM role to the instance. The amazon.aws collection automatically uses instance metadata credentials. No access keys needed — more secure and no credential rotation.
How do I handle multiple AWS accounts?
Use profile parameter in modules or set AWS_PROFILE environment variable. For cross-account access, use sts_assume_role to assume roles in target accounts:
- amazon.aws.sts_assume_role:
role_arn: "arn:aws:iam::123456789:role/CrossAccountRole"
role_session_name: "ansible"
register: assumed_role
- amazon.aws.ec2_instance:
# ...
aws_access_key: "{{ assumed_role.sts_creds.access_key }}"
aws_secret_key: "{{ assumed_role.sts_creds.secret_key }}"
security_token: "{{ assumed_role.sts_creds.session_token }}"
How do I make Ansible AWS playbooks idempotent?
Most amazon.aws modules are idempotent by default — they check current state before making changes. Use name or tags for identification, not instance IDs. Always use state: present (not create) to ensure idempotency.
Conclusion
Ansible automates the complete AWS stack — VPC networking, EC2 instances, S3 storage, RDS databases, load balancers, IAM, and more. Use amazon.aws collection for native AWS module support, tag everything for identification, use IAM roles instead of access keys, and combine with dynamic inventory for ongoing management. For large-scale AWS infrastructure, pair Ansible with Terraform: Terraform provisions, Ansible configures.
Related Articles
• Ansible vs Terraform Complete Comparison • Ansible Dynamic Inventory Complete Guide • AAP 2.6 Cloud Automation • Ansible for Docker and Podman • Ansible Performance TuningCategory: installation