Cloud Modules¶
- In this lab we use Ansible’s cloud modules to provision and manage infrastructure on AWS, Azure, and GCP.
- Ansible can replace Terraform for simpler provisioning tasks, or complement it as the configuration layer after infrastructure is created.
- Dynamic inventory plugins allow Ansible to auto-discover cloud resources by tags.
What will we learn?¶
- Provisioning EC2 instances with
amazon.aws - Managing Azure resources with
azure.azcollection - Using cloud inventories for dynamic host discovery
- Idempotent cloud resource management
Prerequisites¶
- Complete Lab 018 and Lab 019 in order to have a working knowledge of Galaxy collections and Ansible Vault.
01. Install Cloud Collections¶
# Install cloud collections individually
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-galaxy collection install amazon.aws"
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-galaxy collection install azure.azcollection"
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-galaxy collection install google.cloud"
# Or via requirements.yml
docker exec ansible-controller sh -c "cd /labs-scripts && cat > requirements.yml << 'EOF'
collections:
- name: amazon.aws
version: \">=6.0.0\"
- name: azure.azcollection
version: \">=1.19.0\"
- name: community.aws
version: \">=7.0.0\"
EOF
ansible-galaxy collection install -r requirements.yml"
02. AWS Credentials Setup¶
# Method 1: Environment variables
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="us-east-1"
# Method 2: AWS credentials file
mkdir -p ~/.aws
cat > ~/.aws/credentials << 'EOF'
[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
EOF
# Method 3: Ansible Vault (recommended for CI/CD)
# Store credentials in a vault-encrypted variable file
NOTE: For production use, prefer IAM roles or managed identities over long-lived access keys.
03. AWS EC2 Instances¶
---
- name: Provision AWS Infrastructure
hosts: localhost
gather_facts: false
collections:
- amazon.aws
vars:
aws_region: us-east-1
instance_type: t3.micro
ami_id: ami-0c55b159cbfafe1f0 # Ubuntu 22.04 us-east-1
key_name: my-ansible-key
tasks:
# Create a VPC
- name: Create VPC
amazon.aws.ec2_vpc_net:
name: ansible-vpc
cidr_block: "10.0.0.0/16"
region: "{{ aws_region }}"
tags:
Environment: lab
ManagedBy: ansible
register: vpc
# Create a subnet
- name: Create public subnet
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ vpc.vpc.id }}"
cidr: "10.0.1.0/24"
az: "{{ aws_region }}a"
region: "{{ aws_region }}"
map_public: true
tags:
Name: ansible-public-subnet
register: subnet
# Create security group
- name: Create security group
amazon.aws.ec2_security_group:
name: ansible-sg
description: Security group for Ansible lab
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ aws_region }}"
rules:
- proto: tcp
ports: [22]
cidr_ip: 0.0.0.0/0
rule_desc: SSH access
- proto: tcp
ports: [80, 443]
cidr_ip: 0.0.0.0/0
rule_desc: HTTP/HTTPS access
tags:
ManagedBy: ansible
register: sg
# Launch EC2 instances
- name: Launch EC2 instances
amazon.aws.ec2_instance:
name: "web-server-{{ item }}"
key_name: "{{ key_name }}"
instance_type: "{{ instance_type }}"
image_id: "{{ ami_id }}"
region: "{{ aws_region }}"
vpc_subnet_id: "{{ subnet.subnet.id }}"
security_group: "{{ sg.group_id }}"
network:
assign_public_ip: true
state: running
wait: true
tags:
Name: "web-server-{{ item }}"
Environment: lab
Role: webserver
ManagedBy: ansible
loop:
- "01"
- "02"
register: ec2_instances
- name: Show instance IPs
ansible.builtin.debug:
msg: "Instance {{ item.item }}: {{ item.instances[0].public_ip_address }}"
loop: "{{ ec2_instances.results }}"
04. AWS S3 Buckets¶
tasks:
- name: Create S3 bucket
amazon.aws.s3_bucket:
name: "my-ansible-bucket-{{ ansible_date_time.epoch }}"
region: "{{ aws_region }}"
versioning: true
encryption: AES256
tags:
ManagedBy: ansible
state: present
- name: Upload file to S3
amazon.aws.aws_s3:
bucket: my-ansible-bucket
object: /configs/app.conf
src: /local/app.conf
mode: put
region: "{{ aws_region }}"
- name: Download file from S3
amazon.aws.aws_s3:
bucket: my-ansible-bucket
object: /configs/app.conf
dest: /etc/app/app.conf
mode: get
region: "{{ aws_region }}"
05. AWS EC2 Dynamic Inventory¶
# aws_ec2.yml - Dynamic inventory for AWS
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
filters:
instance-state-name: running
tag:ManagedBy: ansible
keyed_groups:
- key: tags.Environment
prefix: env
- key: tags.Role
prefix: role
compose:
ansible_host: public_ip_address
ansible_user: "'ubuntu'"
# Use the dynamic inventory
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-inventory -i aws_ec2.yml --graph"
# Run against all web servers tagged in AWS
docker exec ansible-controller sh -c "cd /labs-scripts && ansible -i aws_ec2.yml role_webserver -m ping"
06. Azure Resources¶
---
- name: Provision Azure Infrastructure
hosts: localhost
gather_facts: false
vars:
resource_group: ansible-lab-rg
location: eastus
vnet_name: ansible-vnet
tasks:
- name: Create resource group
azure.azcollection.azure_rm_resourcegroup:
name: "{{ resource_group }}"
location: "{{ location }}"
tags:
ManagedBy: ansible
Environment: lab
- name: Create virtual network
azure.azcollection.azure_rm_virtualnetwork:
resource_group: "{{ resource_group }}"
name: "{{ vnet_name }}"
address_prefixes: "10.0.0.0/16"
- name: Create subnet
azure.azcollection.azure_rm_subnet:
resource_group: "{{ resource_group }}"
virtual_network: "{{ vnet_name }}"
name: default
address_prefix: "10.0.1.0/24"
- name: Create VM
azure.azcollection.azure_rm_virtualmachine:
resource_group: "{{ resource_group }}"
name: ansible-vm-01
vm_size: Standard_B1s
admin_username: azureuser
ssh_password_enabled: false
ssh_public_keys:
- path: /home/azureuser/.ssh/authorized_keys
key_data: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
image:
offer: UbuntuServer
publisher: Canonical
sku: "22.04-LTS"
version: latest
tags:
ManagedBy: ansible
07. Terminate and Clean Up Resources¶
tasks:
# Terminate EC2 instances by tag
- name: Terminate lab instances
amazon.aws.ec2_instance:
state: terminated
region: "{{ aws_region }}"
filters:
tag:Environment: lab
tag:ManagedBy: ansible
# Delete security group
- name: Delete security group
amazon.aws.ec2_security_group:
name: ansible-sg
region: "{{ aws_region }}"
state: absent
# Delete S3 bucket (with all objects)
- name: Delete S3 bucket
amazon.aws.s3_bucket:
name: my-ansible-bucket
state: absent
force: true # Delete non-empty bucket

08. Hands-on¶
- Simulate a cloud provisioning workflow locally - create a playbook that loops over a list of instances and generates a simulated inventory file:
??? success “Solution”
docker exec ansible-controller sh -c "cd /labs-scripts && cat > lab035-cloud-sim.yml << 'EOF'
---
- name: Cloud Infrastructure Simulation
hosts: localhost
gather_facts: false
vars:
instances:
- { name: web-01, role: webserver, zone: us-east-1a }
- { name: web-02, role: webserver, zone: us-east-1b }
- { name: db-01, role: database, zone: us-east-1a }
tasks:
- name: Simulate instance creation
ansible.builtin.debug:
msg: \"Creating instance {{ item.name }} ({{ item.role }}) in {{ item.zone }}\"
loop: \"{{ instances }}\"
- name: Create simulated inventory
ansible.builtin.copy:
content: |
[webservers]
{% for i in instances | selectattr('role', 'eq', 'webserver') %}
{{ i.name }}.example.com
{% endfor %}
[databases]
{% for i in instances | selectattr('role', 'eq', 'database') %}
{{ i.name }}.example.com
{% endfor %}
[all:vars]
ansible_user=ec2-user
ansible_connection=local
dest: /tmp/cloud-inventory
mode: \"0644\"
- name: Show inventory structure
ansible.builtin.command:
cmd: cat /tmp/cloud-inventory
register: inv_content
changed_when: false
- name: Print inventory
ansible.builtin.debug:
var: inv_content.stdout_lines
EOF
ansible-playbook lab035-cloud-sim.yml"
- Install the
amazon.awscollection and view the documentation for theec2_instanceands3_bucketmodules:
??? success “Solution”
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-galaxy collection install amazon.aws --upgrade"
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-doc amazon.aws.ec2_instance | head -40"
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-doc amazon.aws.s3_bucket | head -30"
- Create a playbook that generates a simulated AWS inventory report - loops over instances and prints a formatted table with name, role, zone, and simulated IP:
??? success “Solution”
docker exec ansible-controller sh -c "cd /labs-scripts && cat > lab035-inventory-report.yml << 'EOF'
---
- name: Cloud Inventory Report
hosts: localhost
gather_facts: false
vars:
cloud_instances:
- { name: web-01, role: webserver, zone: us-east-1a, ip: \"10.0.1.10\", state: running }
- { name: web-02, role: webserver, zone: us-east-1b, ip: \"10.0.1.11\", state: running }
- { name: api-01, role: api, zone: us-east-1a, ip: \"10.0.2.10\", state: running }
- { name: db-01, role: database, zone: us-east-1a, ip: \"10.0.3.10\", state: running }
- { name: db-02, role: database, zone: us-east-1b, ip: \"10.0.3.11\", state: stopped }
tasks:
- name: Print inventory header
ansible.builtin.debug:
msg: \"{{ '%-12s %-12s %-14s %-15s %s' | format('NAME', 'ROLE', 'ZONE', 'IP', 'STATE') }}\"
- name: Print each instance
ansible.builtin.debug:
msg: \"{{ '%-12s %-12s %-14s %-15s %s' | format(item.name, item.role, item.zone, item.ip, item.state) }}\"
loop: \"{{ cloud_instances }}\"
- name: Count by role
ansible.builtin.set_fact:
role_counts: \"{{ cloud_instances | groupby('role') | map('list') | map(attribute=1) | map('length') | list }}\"
- name: Summary stats
ansible.builtin.debug:
msg:
- \"Total instances: {{ cloud_instances | length }}\"
- \"Running: {{ cloud_instances | selectattr('state', 'eq', 'running') | list | length }}\"
- \"Stopped: {{ cloud_instances | selectattr('state', 'eq', 'stopped') | list | length }}\"
- \"Webservers: {{ cloud_instances | selectattr('role', 'eq', 'webserver') | list | length }}\"
- \"Databases: {{ cloud_instances | selectattr('role', 'eq', 'database') | list | length }}\"
EOF
ansible-playbook lab035-inventory-report.yml"
- Write a playbook that generates Terraform-like destroy plan output - lists all resources that would be deleted before actually deleting them:
??? success “Solution”
docker exec ansible-controller sh -c "cd /labs-scripts && cat > lab035-destroy-plan.yml << 'EOF'
---
- name: Cloud Resource Destroy Plan
hosts: localhost
gather_facts: false
vars:
environment: lab
resources_to_destroy:
- { type: ec2_instance, name: web-01, id: i-0abc123, env: lab }
- { type: ec2_instance, name: web-02, id: i-0abc124, env: lab }
- { type: security_group, name: ansible-sg, id: sg-0abc125, env: lab }
- { type: s3_bucket, name: my-ansible-bucket, id: my-ansible-bucket, env: lab }
tasks:
- name: Show destroy plan header
ansible.builtin.debug:
msg:
- \"=== DESTROY PLAN (environment={{ environment }}) ===\"
- \"The following resources will be PERMANENTLY DELETED:\"
- name: List resources to be destroyed
ansible.builtin.debug:
msg: \" [-] {{ item.type }}: {{ item.name }} ({{ item.id }})\"
loop: \"{{ resources_to_destroy | selectattr('env', 'eq', environment) | list }}\"
- name: Confirm destruction count
ansible.builtin.debug:
msg: \"Total resources to destroy: {{ resources_to_destroy | length }}\"
- name: Simulate destroy (dry-run)
ansible.builtin.debug:
msg: \"Would terminate: {{ item.type }} {{ item.name }} with state: absent\"
loop: \"{{ resources_to_destroy }}\"
when: not ansible_check_mode
- name: Reminder to use check mode first
ansible.builtin.debug:
msg: \"TIP: Always run with --check first: ansible-playbook destroy.yml -e 'env=lab' --check\"
EOF
ansible-playbook lab035-destroy-plan.yml"
- Demonstrate dynamic inventory by creating a static YAML inventory file in AWS dynamic inventory format, then use
ansible-inventoryto query it:
??? success “Solution”
docker exec ansible-controller sh -c "cd /labs-scripts && cat > lab035-dynamic-inv.yml << 'EOF'
---
# Simulated AWS EC2 dynamic inventory (YAML format)
all:
children:
env_lab:
children:
role_webserver:
hosts:
web-01:
ansible_host: 10.0.1.10
ansible_user: ec2-user
ec2_instance_type: t3.micro
ec2_region: us-east-1
tags:
Environment: lab
Role: webserver
ManagedBy: ansible
web-02:
ansible_host: 10.0.1.11
ansible_user: ec2-user
ec2_instance_type: t3.micro
ec2_region: us-east-1
tags:
Environment: lab
Role: webserver
ManagedBy: ansible
role_database:
hosts:
db-01:
ansible_host: 10.0.3.10
ansible_user: ec2-user
ec2_instance_type: t3.small
ec2_region: us-east-1
tags:
Environment: lab
Role: database
ManagedBy: ansible
EOF"
# Query the dynamic inventory
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-inventory -i lab035-dynamic-inv.yml --graph"
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-inventory -i lab035-dynamic-inv.yml --list | python3 -m json.tool | head -40"
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-inventory -i lab035-dynamic-inv.yml --host web-01"
09. Summary¶
amazon.aws,azure.azcollection, andgoogle.cloudprovide comprehensive cloud module coverage- Cloud resources should be tagged with
ManagedBy: ansibleandEnvironmentfor easy targeting and cleanup - Dynamic inventory plugins (
aws_ec2.yml) auto-discover instances and group them by tags - Always clean up cloud resources using
state: absentorstate: terminatedto avoid costs - Store cloud credentials in Ansible Vault or use IAM roles/managed identities instead of long-lived keys
- Use
registerto capture created resource IDs (VPC, subnet, SG) for use in subsequent tasks