AWS Systems Manager vs. Ansible
SSM vs. Ansible: Choosing the best fit configuration tool for AWS infrastructure
AWS Systems Manager vs. Ansible
SSM vs. Ansible: Choosing the best fit configuration tool for AWS infrastructure

OK, since I have been using Ansible for some other projects, I wanted to see how it compares to AWS Systems Manager. (see my previous post on SSM: https://medium.com/@gkinthaert/a-case-for-aws-systems-manager-a86aca01c044) Initially I was thinking that Ansible was going to be far superior: You only need a list of your nodes and a playbook, and you are done. More than a few hours into it, however, I wasn’t so sure anymore… A lot of that was caused by some issues I was having with my Ubuntu WSL environment on a windows laptop. More impacting was the fact that despite Ansible modules obfuscating a lot of the code you need to write, you still end up writing quite a bit. But, in the end, you have a reusable template which is kind of what we are shooting for. Anyway — this is what is involved:
Step 1: Install Ansible
Know that Ansible acts as a control node and depends on POSIX APIs (like SSH and native file permissions). However, you can use WSL if you are not natively using Linux
- in WSL run the following:

- confirm the installation

- create an inventory file that lists your fleet of servers. The name of the file or extension does not matter. In this example I used 4 EC2 instances. You can use headers to group them (somewhat equivalent to tags). It is also advisable to provide a ‘friendly’ name. You will need a hostname or public IP. You have the option to group some of common arguments as well

- The playbook YAML file is where the tasks and modules are listed. Again, you can call this file anything but it needs a .yml or .yaml extension. I know this is a lot of code, but remember we are doing several tasks on these EC2 instances: 1. Performing diagnostics, 2. installing a web server, 3. Scanning for patches, 4. Installing patches, 5. Deploying a web page, 6. Verifying that the deployment was done.
---
# ============================================================
# Ansible Playbook: EC2 instance example
# Replicates the AWS Systems Manager lab tasks:
# 1. Diagnostic checks (hostname, uptime, disk, date)
# 2. OS patch scanning and installation
# 3. Nginx web page deployment with per-server hostname
# 4. Deployment verification
#
# Requirements:
# - Ansible 2.9+
# - Target hosts: Amazon Linux 2 or Amazon Linux 2023
# - Python 3 on target hosts (Amazon Linux includes this by default)
# - SSH access via ec2-user and ansible.pem
#
# Usage (from the folder containing this file, inventory, and ansible.pem):
# ansible-playbook -i inventory playbook.yml
# ============================================================
- name: example - Server Diagnostics, Patching, and Web Deployment
hosts: web_servers # matches the [web_servers] group in inventory
become: true # equivalent to sudo in the SSM shell scripts
gather_facts: true # collects ansible_hostname, OS family, etc.
vars:
nginx_webroot: /usr/share/nginx/html
index_file: "{{ nginx_webroot }}/index.html"
page_title: "Systems Engineering Lab"
page_heading: "Deployment Successful"
page_message: "This page was pushed to multiple servers using Ansible"
# ─── HANDLERS ──────────────────────────────────────────────
# Handlers run once at the end of the play if notified.
# nginx reloads only when index.html actually changes.
handlers:
- name: Reload nginx
service:
name: nginx
state: reloaded
# ─── TASKS ────────────────────────────────────────────────
tasks:
# ----------------------------------------------------------
# TASK GROUP 1: Diagnostic Checks
# Equivalent to SSM Run Command with:
# echo "Server check starting"; hostname; date; uptime; df -h
# ----------------------------------------------------------
- name: "DIAG | Print server check banner"
debug:
msg: "Server check starting on {{ inventory_hostname }}"
- name: "DIAG | Capture hostname"
command: hostname
register: hostname_result
changed_when: false # read-only, never marks as changed
- name: "DIAG | Display hostname"
debug:
msg: "Hostname: {{ hostname_result.stdout }}"
- name: "DIAG | Capture current date and time"
command: date
register: date_result
changed_when: false
- name: "DIAG | Display date"
debug:
msg: "Date: {{ date_result.stdout }}"
- name: "DIAG | Capture uptime"
command: uptime
register: uptime_result
changed_when: false
- name: "DIAG | Display uptime"
debug:
msg: "Uptime: {{ uptime_result.stdout }}"
- name: "DIAG | Capture disk usage"
command: df -h
register: df_result
changed_when: false
- name: "DIAG | Display disk usage"
debug:
msg: "Disk usage:\n{{ df_result.stdout }}"
# ----------------------------------------------------------
# TASK GROUP 2: Install Nginx
# Amazon Linux instances do not have nginx pre-installed.
# Amazon Linux 2023 uses 'dnf'; Amazon Linux 2 uses 'yum'.
# The 'package' module is OS-agnostic and picks the right
# package manager automatically. nginx must also be enabled
# so it starts automatically on reboot.
# ----------------------------------------------------------
- name: "NGINX | Install nginx"
package:
name: nginx
state: present
- name: "NGINX | Enable and start nginx"
service:
name: nginx
state: started
enabled: true
# ----------------------------------------------------------
# TASK GROUP 3: Patch Scan and Installation
# Amazon Linux 2 and 2023 are both RedHat family.
# Amazon Linux 2023 uses 'dnf' but the yum module works as
# a shim. The 'package' module is used here for cleaner
# cross-version compatibility.
#
# NOTE: Set 'run_patch_install' to false for scan-only mode.
# ----------------------------------------------------------
- name: "PATCH | Set patch install flag (change to false for scan-only)"
set_fact:
run_patch_install: true
- name: "PATCH | Check for available updates (Amazon Linux)"
command: yum check-update
register: yum_check
changed_when: false
failed_when: yum_check.rc not in [0, 100] # 100 = updates available (normal exit code)
- name: "PATCH | Display available updates"
debug:
msg: "{{ yum_check.stdout_lines }}"
- name: "PATCH | Install all available updates (Amazon Linux)"
package:
name: "*"
state: latest
when: run_patch_install | bool
# Reboot handling: uncomment to allow reboots if patches require them.
# This is equivalent to SSM Patch Manager's "reboot if needed" behavior.
#
# - name: "PATCH | Reboot if required"
# reboot:
# msg: "Rebooting after patch installation"
# connect_timeout: 5
# reboot_timeout: 300
# pre_reboot_delay: 0
# post_reboot_delay: 30
# test_command: uptime
# when: run_patch_install | bool
# ----------------------------------------------------------
# TASK GROUP 4: Deploy Custom Nginx Web Page
# Equivalent to SSM Run Command heredoc script:
#
# HOST=$(hostname)
# sudo bash -c "cat > /usr/share/nginx/html/index.html <<EOF
# ...
# <p>Server Name: $HOST</p>
# ...
# EOF"
#
# Ansible uses the 'copy' module with inline content.
# ansible_hostname is a gathered fact — no subshell needed.
# The handler reloads nginx only when the file changes.
# ----------------------------------------------------------
- name: "DEPLOY | Ensure nginx webroot exists"
file:
path: "{{ nginx_webroot }}"
state: directory
mode: "0755"
owner: root
group: root
- name: "DEPLOY | Write custom index.html to each server"
copy:
dest: "{{ index_file }}"
owner: root
group: root
mode: "0644"
content: |
<!DOCTYPE html>
<html>
<head>
<title>{{ page_title }}</title>
</head>
<body>
<h1>{{ page_heading }}</h1>
<p>{{ page_message }}</p>
<p>Server Name: {{ ansible_hostname }}</p>
</body>
</html>
notify: Reload nginx # triggers handler if file changed
# ----------------------------------------------------------
# TASK GROUP 5: Verify Deployment
# Equivalent to: open each server's IP in a browser.
# Ansible fetches the page from localhost and confirms content.
# ----------------------------------------------------------
- name: "VERIFY | Wait for nginx to be ready on port 80"
wait_for:
host: "127.0.0.1"
port: 80
timeout: 30
- name: "VERIFY | Fetch the deployed index.html via HTTP"
uri:
url: "http://127.0.0.1/index.html"
return_content: true
status_code: 200
register: page_response
- name: "VERIFY | Confirm page contains correct hostname"
assert:
that:
- "'Deployment Successful' in page_response.content"
- "ansible_hostname in page_response.content"
success_msg: "✓ Deployment verified on {{ ansible_hostname }}"
fail_msg: "✗ Deployment verification FAILED on {{ ansible_hostname }}"
- name: "VERIFY | Display page content preview"
debug:
msg: "Page response from {{ ansible_hostname }}:\n{{ page_response.content }}"
- Now you run this one command (I made it easy on myself and had everything in the same folder, but in the real world you would have different folders for templates, variables, inventory files, keys…
ansible-playbook -i inventory playbook.yml
- Next, the tasks are executed. This is where you wait anxiously and go get some more coffee. I had some errors in prior runs and had to make some changes to the playbook, keeping myself honest here…
dev:~/ansible$ ansible-playbook -i inventory playbook.yml
PLAY [SSM Lab - Server Diagnostics, Patching, and Web Deployment] ********************************
TASK [Gathering Facts] ***************************************************************************
[WARNING]: Host 'webserver3' is using the discovered Python interpreter at '/usr/bin/python3.9', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.20/reference_appendices/interpreter_discovery.html for more information.
ok: [webserver3]
[WARNING]: Host 'webserver1' is using the discovered Python interpreter at '/usr/bin/python3.9', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.20/reference_appendices/interpreter_discovery.html for more information.
ok: [webserver1]
[WARNING]: Host 'webserver2' is using the discovered Python interpreter at '/usr/bin/python3.9', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.20/reference_appendices/interpreter_discovery.html for more information.
ok: [webserver2]
[WARNING]: Host 'webserver4' is using the discovered Python interpreter at '/usr/bin/python3.9', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.20/reference_appendices/interpreter_discovery.html for more information.
ok: [webserver4]
TASK [DIAG | Print server check banner] **********************************************************
ok: [webserver1] => {
"msg": "Server check starting on webserver1"
}
ok: [webserver2] => {
"msg": "Server check starting on webserver2"
}
ok: [webserver3] => {
"msg": "Server check starting on webserver3"
}
ok: [webserver4] => {
"msg": "Server check starting on webserver4"
}
TASK [DIAG | Capture hostname] *******************************************************************
ok: [webserver3]
ok: [webserver1]
ok: [webserver2]
ok: [webserver4]
TASK [DIAG | Display hostname] *******************************************************************
ok: [webserver1] => {
"msg": "Hostname: ip-172-31-39-176.us-east-2.compute.internal"
}
ok: [webserver2] => {
"msg": "Hostname: ip-172-31-47-45.us-east-2.compute.internal"
}
ok: [webserver3] => {
"msg": "Hostname: ip-172-31-46-46.us-east-2.compute.internal"
}
ok: [webserver4] => {
"msg": "Hostname: ip-172-31-38-111.us-east-2.compute.internal"
}
TASK [DIAG | Capture current date and time] ******************************************************
ok: [webserver1]
ok: [webserver3]
ok: [webserver2]
ok: [webserver4]
TASK [DIAG | Display date] ***********************************************************************
ok: [webserver1] => {
"msg": "Date: Sat May 30 17:42:15 UTC 2026"
}
ok: [webserver2] => {
"msg": "Date: Sat May 30 17:42:15 UTC 2026"
}
ok: [webserver3] => {
"msg": "Date: Sat May 30 17:42:15 UTC 2026"
}
ok: [webserver4] => {
"msg": "Date: Sat May 30 17:42:15 UTC 2026"
}
TASK [DIAG | Capture uptime] *********************************************************************
ok: [webserver1]
ok: [webserver4]
ok: [webserver3]
ok: [webserver2]
TASK [DIAG | Display uptime] *********************************************************************
ok: [webserver1] => {
"msg": "Uptime: 17:42:16 up 42 min, 2 users, load average: 0.08, 0.02, 0.01"
}
ok: [webserver2] => {
"msg": "Uptime: 17:42:16 up 41 min, 2 users, load average: 0.08, 0.02, 0.01"
}
ok: [webserver3] => {
"msg": "Uptime: 17:42:16 up 42 min, 2 users, load average: 0.16, 0.03, 0.01"
}
ok: [webserver4] => {
"msg": "Uptime: 17:42:16 up 41 min, 2 users, load average: 0.08, 0.02, 0.01"
}
TASK [DIAG | Capture disk usage] *****************************************************************
ok: [webserver1]
ok: [webserver3]
ok: [webserver4]
ok: [webserver2]
TASK [DIAG | Display disk usage] *****************************************************************
ok: [webserver1] => {
"msg": "Disk usage:\nFilesystem Size Used Avail Use% Mounted on\ndevtmpfs 4.0M 0 4.0M 0% /dev\ntmpfs 459M 0 459M 0% /dev/shm\ntmpfs 184M 440K 183M 1% /run\n/dev/nvme0n1p1 8.0G 1.6G 6.4G 20% /\ntmpfs 459M 120K 459M 1% /tmp\n/dev/nvme0n1p128 10M 1.3M 8.7M 13% /boot/efi\ntmpfs 92M 0 92M 0% /run/user/1000"
}
ok: [webserver2] => {
"msg": "Disk usage:\nFilesystem Size Used Avail Use% Mounted on\ndevtmpfs 4.0M 0 4.0M 0% /dev\ntmpfs 459M 0 459M 0% /dev/shm\ntmpfs 184M 440K 183M 1% /run\n/dev/nvme0n1p1 8.0G 1.6G 6.4G 20% /\ntmpfs 459M 120K 459M 1% /tmp\n/dev/nvme0n1p128 10M 1.3M 8.7M 13% /boot/efi\ntmpfs 92M 0 92M 0% /run/user/1000"
}
ok: [webserver3] => {
"msg": "Disk usage:\nFilesystem Size Used Avail Use% Mounted on\ndevtmpfs 4.0M 0 4.0M 0% /dev\ntmpfs 459M 0 459M 0% /dev/shm\ntmpfs 184M 440K 183M 1% /run\n/dev/nvme0n1p1 8.0G 1.6G 6.4G 20% /\ntmpfs 459M 120K 459M 1% /tmp\n/dev/nvme0n1p128 10M 1.3M 8.7M 13% /boot/efi\ntmpfs 92M 0 92M 0% /run/user/1000"
}
ok: [webserver4] => {
"msg": "Disk usage:\nFilesystem Size Used Avail Use% Mounted on\ndevtmpfs 4.0M 0 4.0M 0% /dev\ntmpfs 459M 0 459M 0% /dev/shm\ntmpfs 184M 440K 183M 1% /run\n/dev/nvme0n1p1 8.0G 1.6G 6.4G 20% /\ntmpfs 459M 120K 459M 1% /tmp\n/dev/nvme0n1p128 10M 1.3M 8.7M 13% /boot/efi\ntmpfs 92M 0 92M 0% /run/user/1000"
}
TASK [NGINX | Install nginx] *********************************************************************
changed: [webserver4]
changed: [webserver1]
changed: [webserver2]
changed: [webserver3]
TASK [NGINX | Enable and start nginx] ************************************************************
changed: [webserver3]
changed: [webserver4]
changed: [webserver1]
changed: [webserver2]
TASK [PATCH | Set patch install flag (change to false for scan-only)] ****************************
ok: [webserver1]
ok: [webserver2]
ok: [webserver3]
ok: [webserver4]
TASK [PATCH | Check for available updates (Amazon Linux)] ****************************************
ok: [webserver1]
ok: [webserver3]
ok: [webserver2]
ok: [webserver4]
TASK [PATCH | Display available updates] *********************************************************
ok: [webserver1] => {
"msg": [
"Last metadata expiration check: 0:00:09 ago on Sat May 30 17:42:33 2026."
]
}
ok: [webserver2] => {
"msg": [
"Last metadata expiration check: 0:00:09 ago on Sat May 30 17:42:33 2026."
]
}
ok: [webserver3] => {
"msg": [
"Last metadata expiration check: 0:00:09 ago on Sat May 30 17:42:33 2026."
]
}
ok: [webserver4] => {
"msg": [
"Last metadata expiration check: 0:00:09 ago on Sat May 30 17:42:33 2026."
]
}
TASK [PATCH | Install all available updates (Amazon Linux)] **************************************
ok: [webserver3]
ok: [webserver1]
ok: [webserver2]
ok: [webserver4]
TASK [DEPLOY | Ensure nginx webroot exists] ******************************************************
ok: [webserver1]
ok: [webserver3]
ok: [webserver2]
ok: [webserver4]
TASK [DEPLOY | Write custom index.html to each server] *******************************************
[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: /home/gkinthaert/ansible/playbook.yml:183:18
181 group: root
182 mode: "0644"
183 content: |
^ column 18
Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.
changed: [webserver1]
changed: [webserver4]
changed: [webserver3]
changed: [webserver2]
TASK [VERIFY | Wait for nginx to be ready on port 80] ********************************************
ok: [webserver1]
ok: [webserver4]
ok: [webserver3]
ok: [webserver2]
TASK [VERIFY | Fetch the deployed index.html via HTTP] *******************************************
ok: [webserver4]
ok: [webserver3]
ok: [webserver1]
ok: [webserver2]
TASK [VERIFY | Confirm page contains correct hostname] *******************************************
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: /home/gkinthaert/ansible/playbook.yml:222:19
220 - "ansible_hostname in page_response.content"
221 success_msg: "✓ Deployment verified on {{ ansible_hostname }}"
222 fail_msg: "✗ Deployment verification FAILED on {{ ansible_hostname }}"
^ column 19
Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: /home/gkinthaert/ansible/playbook.yml:221:22
219 - "'Deployment Successful' in page_response.content"
220 - "ansible_hostname in page_response.content"
221 success_msg: "✓ Deployment verified on {{ ansible_hostname }}"
^ column 22
Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: /home/gkinthaert/ansible/playbook.yml:220:13
218 that:
219 - "'Deployment Successful' in page_response.content"
220 - "ansible_hostname in page_response.content"
^ column 13
Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.
ok: [webserver1] => {
"changed": false,
"msg": "✓ Deployment verified on ip-172-31-39-176"
}
ok: [webserver2] => {
"changed": false,
"msg": "✓ Deployment verified on ip-172-31-47-45"
}
ok: [webserver3] => {
"changed": false,
"msg": "✓ Deployment verified on ip-172-31-46-46"
}
ok: [webserver4] => {
"changed": false,
"msg": "✓ Deployment verified on ip-172-31-38-111"
}
TASK [VERIFY | Display page content preview] *****************************************************
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: /home/gkinthaert/ansible/playbook.yml:226:14
224 - name: "VERIFY | Display page content preview"
225 debug:
226 msg: "Page response from {{ ansible_hostname }}:\n{{ page_response.content }}"
^ column 14
Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.
ok: [webserver1] => {
"msg": "Page response from ip-172-31-39-176:\n<!DOCTYPE html>\n<html>\n<head>\n <title>Systems Engineering Lab</title>\n</head>\n<body>\n <h1>Deployment Successful</h1>\n <p>This page was pushed to multiple servers using Ansible</p>\n <p>Server Name: ip-172-31-39-176</p>\n</body>\n</html>\n"
}
ok: [webserver2] => {
"msg": "Page response from ip-172-31-47-45:\n<!DOCTYPE html>\n<html>\n<head>\n <title>Systems Engineering Lab</title>\n</head>\n<body>\n <h1>Deployment Successful</h1>\n <p>This page was pushed to multiple servers using Ansible</p>\n <p>Server Name: ip-172-31-47-45</p>\n</body>\n</html>\n"
}
ok: [webserver3] => {
"msg": "Page response from ip-172-31-46-46:\n<!DOCTYPE html>\n<html>\n<head>\n <title>Systems Engineering Lab</title>\n</head>\n<body>\n <h1>Deployment Successful</h1>\n <p>This page was pushed to multiple servers using Ansible</p>\n <p>Server Name: ip-172-31-46-46</p>\n</body>\n</html>\n"
}
ok: [webserver4] => {
"msg": "Page response from ip-172-31-38-111:\n<!DOCTYPE html>\n<html>\n<head>\n <title>Systems Engineering Lab</title>\n</head>\n<body>\n <h1>Deployment Successful</h1>\n <p>This page was pushed to multiple servers using Ansible</p>\n <p>Server Name: ip-172-31-38-111</p>\n</body>\n</html>\n"
}
RUNNING HANDLER [Reload nginx] *******************************************************************
changed: [webserver4]
changed: [webserver2]
changed: [webserver3]
changed: [webserver1]
PLAY RECAP ***************************************************************************************
webserver1 : ok=23 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
webserver2 : ok=23 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
webserver3 : ok=23 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
webserver4 : ok=23 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
gkinthaert@GeertBiz:~/ansible$
- Success!! All tasks were executed, and the webservers are up and running

Both AWS Systems Manager and Ansible can accomplish every task in my small example (a fleet of a few EC2 instances): patching, command execution, and web page deployment.
But they approach those tasks fundamentally differently, and choosing between them (or combining them) depends on your team’s context, existing toolchain, and longer-term goals.
Task overlap
in my example of running diagnostic commands across multiple servers, applying OS patches, and pushing a file to nginx, both tools are fully capable:
- Ansible can run ad-hoc commands or playbooks targeting multiple hosts simultaneously using its inventory and parallelism model.
- SSM Run Command does the same thing through the AWS console or CLI, targeting by tag or instance ID. The SSM Run Command wins on simplicity — one console action, output per instance. Ansible requires an inventory file or dynamic inventory plugin, then an ad-hoc command or playbook.
- Ansible’s yum/apt modules handle patching; SSM Patch Manager handles it through its native scan-and-install workflow. So SSM Patch Manager wins here for AWS-native teams — compliance reporting and scheduling are built in. Ansible is more flexible but requires more configuration.
- Both tools can write files to remote servers — Ansible via the copy or template module, SSM via Run Command shell scripts. The tools are very comparable and I would just go by your preference: Ansible’s template module is more elegant for file deployment with variable substitution; while SSM’s heredoc shell script works but is less idiomatic configuration management.
Where does AWS Systems Manager have the edge?
For teams operating entirely within AWS, SSM offers several structural advantages:
- No control plane to manage: SSM is serverless from the operator’s perspective. There is no Ansible controller node to provision, secure, or maintain.
- No network connectivity required: SSM Agent communicates outbound over HTTPS. Ansible requires SSH reachability (or WinRM for Windows) from the control node to every managed host.
- Native AWS integration: SSM is tightly integrated with IAM, CloudTrail, CloudWatch, and EventBridge. Audit logging, alerting, and scheduled execution are built-in without additional tooling.
- Patch compliance reporting: Patch Manager provides a compliance dashboard out of the box. Getting equivalent visibility from Ansible requires integrating with a separate reporting tool.
- Lower barrier to entry: For teams already in AWS, SSM requires no additional software installation or configuration on the control side.
Where does Ansible shine?
Ansible’s strengths become apparent in more complex configuration scenarios and multi-cloud or hybrid environments:
- Idempotent configuration management: Ansible playbooks describe desired state. Running a playbook twice produces the same result with no side effects — a property SSM Run Command shell scripts do not inherently have.
- Rich module library: Ansible has thousands of modules covering application configuration, database management, network devices, cloud APIs, and more — far beyond what SSM’s shell-script model supports natively.
- Multi-cloud and on-premises: Ansible works identically against AWS, Azure, GCP, VMware, and bare metal. SSM is AWS-native (though hybrid activations exist for on-prem, they add complexity).
- Version-controlled infrastructure: Playbooks are YAML files that live in Git, making change history, code review, and rollback natural parts of the workflow.
- Dynamic inventory: Ansible can dynamically query AWS, Azure, or other sources for its inventory, supporting sophisticated host grouping and variable management.
The verdict?
For teams running workloads exclusively in AWS with relatively straightforward configuration needs, SSM is the best choice as it is already there, it is free, and it requires no additional infrastructure.
For teams with complex application configuration requirements, multi-cloud infrastructure, or existing Ansible expertise and workflows, Ansible is the more powerful and portable option. It also pairs well with SSM: many organizations use SSM for access and patching while using Ansible (or Terraform, or CloudFormation) for application-layer configuration.
These tools are not mutually exclusive. You should treat them as complementary, using each where it excels rather than forcing one tool to do everything.
메타데이터
- post_id
- 8cdd386914eb
- slug
- aws-systems-manager-vs-ansible-8cdd386914eb
- url
- https://medium.com/@gkinthaert/aws-systems-manager-vs-ansible-8cdd386914eb
- canonical_url
- https://medium.com/@gkinthaert/aws-systems-manager-vs-ansible-8cdd386914eb
- author_url
- https://medium.com/@gkinthaert
- status
- ok
- fetched_at
- 2026-06-09 15:37:30