In the blog post What's new in cloud automation: Red Hat Ansible Certified Content Collection for amazon.aws 10.0.0, we introduced version 10.0.0 of the Red Hat Ansible Certified Content Collection for amazon.aws. We highlighted key enhancements and full support for amazon.aws.aws_ssm, a security-focused, agent-based connection plug-in that represents a significant leap forward in how we automate Amazon EC2 infrastructure.
In this follow-up, we'll move from concepts to execution by showcasing a real-world use case where we combine the following:
- HashiCorp Terraform for provisioning private EC2 instances
- AWS Systems Manager (SSM) as the connection mechanism
- Ansible automation using the new
amazon.aws.aws_ssm
connection plug-in and theamazon.aws.aws_ec2
dynamic inventory plug-in
We'll explore a scenario that demonstrates the power of this plug-in in the context of Day 0, Day 1, and Day 2 operations. We'll walk through provisioning infrastructure with Terraform, then managing configuration and lifecycle tasks using Red Hat Ansible Automation Platform, all without opening a single inbound port. This flow is illustrated in Figure 1.
This scenario aligns with modern cloud architecture trends, including zero trust networking, no-SSH environments, and compliance-driven infrastructure management.

Embracing zero trust: Why amazon.aws.aws_ssm?
Traditionally, managing EC2 instances has required juggling key pairs, managing SSH access, and configuring security groups—all of which introduce security complexity and operational overhead. The amazon.aws.aws_ssm
connection plug-in, newly supported in the amazon.aws 10.0.0 certified collection, changes that paradigm entirely.
By using the AWS Systems Manager (SSM) agent pre-installed on supported EC2 AMIs, this plug-in allows Ansible Automation Platform to connect over the AWS-managed control plane, eliminating the need for SSH access or public IP addresses. That means:
- No open inbound ports, including port 22
- No SSH keys to manage or rotate
- No bastion hosts
- Cloud-native logging of session activity via CloudTrail and SSM
- Better alignment with compliance frameworks like CIS, HIPAA, or ISO 27001
This drastically reduces your attack surface and simplifies your operational model.
Real-world scenario: Private web servers, fully automated
Let's consider a common scenario: Suppose your security team mandates that all EC2 instances must reside in private subnets, without public IP addresses or SSH access. You need to provision a fleet of web servers and automate their configuration—all without compromising this zero trust security posture.
In this scenario, we'll:
- Provision EC2 resources with Terraform.
- Enable remote access via SSM.
- Automate software installation and configuration using Ansible over SSM.
- Manage both Day 1 and Day 2 tasks.
Day 0 Operations: Security-focused provisioning with Terraform
Our journey begins with infrastructure provisioning. Using Terraform, we define a virtual private cloud (VPC), deploy private subnets, restrict all inbound traffic via security groups, and launch EC2 instances with IAM roles that enable SSM access.
Here's a simplified Terraform configuration:
# Configure the AWS Provider
provider "aws" {
region = var.aws_region
}
# --- VPC and Networking Setup ---
# Create a new VPC
resource "aws_vpc" "zero_trust_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "${var.project_name}-vpc"
Project = var.project_name
}
}
# Create a public subnet for the NAT Gateway
resource "aws_subnet" "public_subnet" {
vpc_id = aws_vpc.zero_trust_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a" # Use a specific AZ for simplicity
tags = {
Name = "${var.project_name}-public-subnet"
Project = var.project_name
}
}
# Create a private subnet for the EC2 instance
resource "aws_subnet" "private_subnet" {
vpc_id = aws_vpc.zero_trust_vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.aws_region}a" # Use a specific AZ for simplicity
tags = {
Name = "${var.project_name}-private-subnet"
Project = var.project_name
}
}
# Create an Internet Gateway for the public subnet
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.zero_trust_vpc.id
tags = {
Name = "${var.project_name}-igw"
Project = var.project_name
}
}
# Create an Elastic IP for the NAT Gateway
resource "aws_eip" "nat_eip" {
vpc = true
depends_on = [aws_internet_gateway.igw] # Ensure IGW exists before EIP
tags = {
Name = "${var.project_name}-nat-eip"
Project = var.project_name
}
}
# Create a NAT Gateway in the public subnet
resource "aws_nat_gateway" "nat_gateway" {
allocation_id = aws_eip.nat_eip.id
subnet_id = aws_subnet.public_subnet.id
tags = {
Name = "${var.project_name}-nat-gateway"
Project = var.project_name
}
}
# Create a public route table
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.zero_trust_vpc.id
tags = {
Name = "${var.project_name}-public-rt"
Project = var.project_name
}
}
# Add route to Internet Gateway for public route table
resource "aws_route" "public_internet_route" {
route_table_id = aws_route_table.public_rt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
# Associate public route table with public subnet
resource "aws_route_table_association" "public_subnet_association" {
subnet_id = aws_subnet.public_subnet.id
route_table_id = aws_route_table.public_rt.id
}
# Create a private route table
resource "aws_route_table" "private_rt" {
vpc_id = aws_vpc.zero_trust_vpc.id
tags = {
Name = "${var.project_name}-private-rt"
Project = var.project_name
}
}
# Add route to NAT Gateway for private route table
resource "aws_route" "private_nat_route" {
route_table_id = aws_route_table.private_rt.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat_gateway.id
}
# Associate private route table with private subnet
resource "aws_route_table_association" "private_subnet_association" {
subnet_id = aws_subnet.private_subnet.id
route_table_id = aws_route_table.private_rt.id
}
# --- Security Group for EC2 Instance ---
# Create a security group for the EC2 instance
resource "aws_security_group" "ec2_sg" {
name = "${var.project_name}-ec2-sg"
description = "Allow outbound traffic for private EC2"
vpc_id = aws_vpc.zero_trust_vpc.id
# Allow all outbound traffic (needed for updates, SSM endpoints, etc.)
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
# No inbound rules needed for SSH/HTTP as we are using SSM
# If you need specific inbound rules are needed within the VPC, add them here.
tags = {
Name = "${var.project_name}-ec2-sg"
Project = var.project_name
}
}
# --- S3 Bucket ---
resource "aws_s3_bucket" "zero_trust_bucket" {
bucket = "${var.project_name}-ssm-bucket" # S3 bucket names must be globally unique
tags = {
Name = "${var.project_name}-ssm-bucket"
Project = var.project_name
}
}
# --- IAM Role for EC2 (SSM Permissions) ---
# Create an IAM role for the EC2 instance
resource "aws_iam_role" "ssm_ec2_role" {
name = "${var.project_name}-ssm-ec2-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
tags = {
Name = "${var.project_name}-ssm-ec2-role"
Project = var.project_name
}
}
# Attach the AmazonSSMManagedInstanceCore policy to the role
resource "aws_iam_role_policy_attachment" "ssm_policy_attachment" {
role = aws_iam_role.ssm_ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# Create an instance profile for the EC2 instance
resource "aws_iam_instance_profile" "ssm_instance_profile" {
name = "${var.project_name}-ssm-instance-profile"
role = aws_iam_role.ssm_ec2_role.name
}
# --- EC2 Instance (Private) ---
# Find the latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux_2" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
}
# Create the private EC2 instance
resource "aws_instance" "private_webserver" {
ami = data.aws_ami.amazon_linux_2.id
instance_type = var.instance_type
subnet_id = aws_subnet.private_subnet.id
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
associate_public_ip_address = false # Crucial for zero-trust (no public IP)
iam_instance_profile = aws_iam_instance_profile.ssm_instance_profile.name
tags = {
Name = "${var.project_name}-webserver"
Project = var.project_name
}
}
To bring this zero trust automation workflow to life, you can start by provisioning your infrastructure using the scripts provided in this GitHub repository. You can get started by cloning the repository and applying the Terraform configuration:
git clone https://github.com/alinabuzachis/aws-ansible-zero-trust-automation.git
cd terraform
terraform init
terraform apply
This configuration ensures security-focused, SSM-enabled infrastructure is provisioned on Day 1, with no exposure to the public internet.
Day 1 and Day 2 operations: Lifecycle automation with Ansible Automation Platform
With the infrastructure in place and SSM connectivity configured, we now shift into Day 1 and Day 2 operations—the critical tasks that define how systems are configured, updated, and maintained over their lifecycle. These operations are where the real value of automation emerges: you're not just provisioning servers, but keeping them consistent, security-focused, and production-ready at all times.
What sets this workflow apart is that all Ansible automation is executed via the amazon.aws.aws_ssm
connection plug-in, without ever relying on SSH, key pairs, or opening inbound ports. Connections are tunneled through AWS Systems Manager (SSM), and all actions are traceable through CloudTrail and SSM session logs.
Rather than relying on static inventory files, we'll use the amazon.aws.aws_ec2
dynamic inventory plug-in to automatically discover and group EC2 instances based on AWS tags or other metadata. This allows your Ansible Automation Platform environment to stay aligned with your cloud infrastructure—no manual updates required.
Dynamic inventory with amazon.aws.aws_ec2: Let Ansible Automation Platform discover your infrastructure
While you can manually track EC2 instance IDs in a static inventory, dynamic environments benefit greatly from Ansible's amazon.aws.aws_ec2
inventory plug-in. With this plug-in, Ansible Automation Platform can automatically discover and group your EC2 instances based on tags, regions, or instance attributes. This eliminates the need to update your inventory manually.
Let's configure a dynamic inventory (aws_ec2.yml
) that targets all EC2 instances tagged with Project=zero-trust-demo
:
plugin: amazon.aws.aws_ec2
regions:
- "eu-central-1"
keyed_groups:
- prefix: tags
key: tag_
filters:
instance-state-name: running
tag:Project: zero-trust-demo
compose:
ansible_host: instance_id
ansible_connection: "'aws_ssm'"
ansible_aws_ssm_bucket_name: '"zero-trust-demo-ssm-bucket"'
ansible_aws_ssm_region: '"eu-central-1"'
This configuration tells Ansible Automation Platform to:
- Discover EC2 instances in the
eu-central-1
region. - Target only instances tagged with
Project=zero-trust-demo
. - Use the EC2 instance ID as the hostname (required for SSM).
- Automatically assign the
amazon.aws.aws_ssm
connection method and region per host.
This makes your inventory flexible and fully automatic—perfect for modern infrastructure that changes frequently or spans multiple environments. This dynamic inventory adapts as your infrastructure evolves. Tag a new instance with Environment=dev
, and Ansible Automation Platform will pick it up automatically. No need to manually update your inventory.
Day 1 Ansible Playbook: Configure Apache via SSM
Now that the infrastructure is discoverable, we can automate common configuration tasks using Ansible Automation Platform—reliably, flexibly, and at scale.
Here's a simple example playbook (configure_web.yml
) that installs and configures Apache on all matching instances:
---
- name: Configure Apache on EC2 via SSM
hosts: tag__Name_zero_trust_demo_webserver
gather_facts: false
tasks:
- name: Install Apache HTTP Server
ansible.builtin.package:
name: httpd
state: present
- name: Enable and start Apache service
ansible.builtin.service:
name: httpd
state: started
enabled: true
- name: Create a sample homepage
ansible.builtin.copy:
dest: /var/www/html/index.html
content: "<h1>Welcome to Ansible over SSM!</h1>"
This playbook runs against all instances dynamically grouped under tag__Name_zero_trust_demo_webserver
, thanks to our inventory plug-in.
You can execute this playbook in Ansible Automation Platform, which ensures that all necessary software is installed and configured on your infrastructure without requiring command-line intervention.
Looking ahead: Security, scalability, and compliance
This is a complete, security-focused configuration workflow, with no SSH keys, no exposed ports, and no infrastructure outside your control. You gain the full power of Ansible Automation Platform for post-deployment automation while aligning with cloud-native and zero trust principles.
Typical Day 2 operations now become seamless:
- Updating packages across private subnets
- Rotating secrets without exposing endpoints
- Applying compliance patches across fleets
- Enforcing configuration drift remediation
- Automating routine administrative tasks via cron jobs or scheduled playbooks
Additionally, because all actions are routed through SSM, they are logged, monitored, and centrally auditable, which gives you better governance and compliance tracking without sacrificing agility.
This approach scales effortlessly. Add new EC2 instances? Tag them and let your dynamic inventory detect them. Rotate credentials? Let IAM and SSM handle the heavy lifting. Need audit trails? SSM integrates with AWS CloudTrail for full session logging. Everything happens within your existing AWS IAM and logging architecture.
Most importantly, this model aligns with evolving security expectations. Whether you're working in finance, healthcare, or any highly regulated industry, moving away from open SSH access toward network-isolated, centrally audited control is imperative.
Final thoughts
With the release of version 10.0.0 of the Red Hat Ansible Certified Content Collection for amazon.aws, we see a firm shift toward security-focused, cloud-native automation. Now fully supported, the amazon.aws.aws_ssm connection plug-in empowers users to automate in ways that are scalable, compliant, and operationally resilient.
By combining HashiCorp Terraform, Red Hat Ansible Automation Platform, and SSM, you can confidently deliver Day 0, Day 1, and Day 2 operations for AWS environments that meet the highest standards of automation maturity and infrastructure security.
Where to go next
- Explore the latest articles about Ansible automation on Red Hat Developer and the Red Hat Blog.
- Check out this YouTube playlist for everything about Ansible Collections.
- If you're new to Ansible automation, download the Red Hat Certified Engineer (RHCE) Ansible Automation Study Guide (O'Reilly).
- To learn more about Ansible Automation Platform, check out our getting started guide.