AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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.

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home