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

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.