Ansible for Cloud Migration: Lift-and-Shift, Re-Platform, and Re-Factor Strategies
By Luca Berton · Published 2024-01-01 · Category: installation
Automate cloud migration with Ansible. Execute lift-and-shift, re-platforming, and re-factoring strategies across AWS, Azure, and GCP with migration playbooks.
Introduction
Cloud migration is complex, risky, and time-consuming when done manually. Ansible automates the repeatable parts — server inventory discovery, application dependency mapping, workload migration, configuration adaptation, and validation testing. Whether you're doing lift-and-shift, re-platforming, or re-factoring, Ansible provides a consistent framework for migrating hundreds of workloads.
See also: Ansible Automation Mesh: Scalable Automation Across Hybrid Cloud Environments
Migration Strategies
| Strategy | Complexity | Downtime | Ansible Role | |----------|-----------|----------|-------------| | Lift-and-Shift | Low | Medium | Replicate server configs in cloud | | Re-Platform | Medium | Medium | Adapt to managed services | | Re-Factor | High | Low | Containerize and deploy to K8s | | Re-Purchase | Low | High | Migrate data to SaaS | | Retire | Trivial | None | Decommission servers |
Phase 1: Discovery and Assessment
Inventory Current Infrastructure
---
- name: Discover on-premises infrastructure
hosts: all
become: true
tasks:
- name: Gather comprehensive facts
ansible.builtin.setup:
gather_subset: all
- name: Collect installed packages
ansible.builtin.package_facts:
- name: Collect running services
ansible.builtin.service_facts:
- name: Collect listening ports
ansible.builtin.shell: ss -tlnp | awk 'NR>1 {print $4, $6}'
register: listening_ports
changed_when: false
- name: Collect disk usage
ansible.builtin.shell: df -h --output=target,size,used,pcent | tail -n+2
register: disk_usage
changed_when: false
- name: Collect crontabs
ansible.builtin.shell: crontab -l 2>/dev/null || echo "none"
register: crontabs
changed_when: false
- name: Generate migration assessment
ansible.builtin.template:
src: assessment-report.j2
dest: "/tmp/assessment-{{ inventory_hostname }}.json"
delegate_to: localhost
vars:
server_data:
hostname: "{{ inventory_hostname }}"
os: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
kernel: "{{ ansible_kernel }}"
cpus: "{{ ansible_processor_vcpus }}"
memory_mb: "{{ ansible_memtotal_mb }}"
disk_total_gb: "{{ ansible_mounts | map(attribute='size_total') | map('int') | sum // (1024**3) }}"
packages: "{{ ansible_facts.packages | length }}"
services: "{{ ansible_facts.services | dict2items | selectattr('value.state', 'eq', 'running') | list | length }}"
ports: "{{ listening_ports.stdout_lines }}"
Dependency Mapping
- name: Check database connections
ansible.builtin.shell: |
ss -tnp | grep -E ':3306|:5432|:27017|:6379' | awk '{print $5}'
register: db_connections
changed_when: false
- name: Check external API dependencies
ansible.builtin.shell: |
ss -tnp state established | awk '{print $5}' | cut -d: -f1 | sort -u
register: external_connections
changed_when: false
- name: Build dependency map
ansible.builtin.set_fact:
dependencies:
databases: "{{ db_connections.stdout_lines | unique }}"
external_apis: "{{ external_connections.stdout_lines | unique }}"
internal_services: "{{ ansible_facts.services | dict2items | selectattr('value.state', 'eq', 'running') | map(attribute='key') | list }}"
See also: Meeting Rajveer Singh, Red Hat Consultant, at Delhi Book Festival
Phase 2: Lift-and-Shift
Provision Cloud Targets
- name: Provision cloud equivalents
hosts: localhost
connection: local
tasks:
- name: Read assessment reports
ansible.builtin.find:
paths: /tmp/
patterns: "assessment-*.json"
register: assessments
- name: Create matching EC2 instances
amazon.aws.ec2_instance:
name: "cloud-{{ item.server_data.hostname }}"
instance_type: "{{ item | instance_type_map }}"
image_id: "{{ ami_map[item.server_data.os] }}"
vpc_subnet_id: "{{ migration_subnet }}"
security_group: "{{ migration_sg }}"
key_name: migration-key
volumes:
- device_name: /dev/sda1
ebs:
volume_size: "{{ item.server_data.disk_total_gb | int + 20 }}"
volume_type: gp3
tags:
Source: "{{ item.server_data.hostname }}"
Migration: lift-and-shift
state: running
loop: "{{ assessments.files | map(attribute='path') | map('file') | map('from_json') | list }}"
register: cloud_instances
Replicate Configuration
- name: Replicate server configuration
hosts: cloud_targets
become: true
vars:
source_host: "{{ hostvars[inventory_hostname].migration_source }}"
tasks:
- name: Install matching packages
ansible.builtin.package:
name: "{{ hostvars[source_host].ansible_facts.packages.keys() | list }}"
state: present
ignore_errors: true # Some packages may not exist in cloud AMI
- name: Sync configuration files
ansible.posix.synchronize:
src: "{{ item }}"
dest: "{{ item }}"
rsync_opts:
- "--exclude=*.pid"
- "--exclude=*.sock"
loop:
- /etc/nginx/
- /etc/systemd/system/
- /etc/cron.d/
- /opt/myapp/
delegate_to: "{{ source_host }}"
- name: Enable and start services
ansible.builtin.systemd:
name: "{{ item }}"
state: started
enabled: true
loop: "{{ source_services }}"
Data Migration
- name: Migrate data
hosts: cloud_targets
become: true
tasks:
- name: Sync application data
ansible.posix.synchronize:
src: /data/
dest: /data/
archive: true
compress: true
rsync_opts:
- "--bwlimit=50000" # 50MB/s to avoid saturating network
delegate_to: "{{ source_host }}"
- name: Migrate database
block:
- name: Dump source database
ansible.builtin.command: >
pg_dump -Fc -h {{ source_db_host }} {{ db_name }}
-f /tmp/migration-dump.sql
delegate_to: "{{ source_host }}"
- name: Transfer dump
ansible.builtin.fetch:
src: /tmp/migration-dump.sql
dest: /tmp/
flat: true
delegate_to: "{{ source_host }}"
- name: Restore to cloud database
ansible.builtin.command: >
pg_restore -h {{ cloud_db_endpoint }} -d {{ db_name }}
/tmp/migration-dump.sql
Phase 3: Validation
- name: Validate migrated workloads
hosts: cloud_targets
tasks:
- name: Health check — HTTP endpoints
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}:{{ item.port }}{{ item.path }}"
status_code: "{{ item.expected_code }}"
loop:
- { port: 80, path: "/", expected_code: 200 }
- { port: 8080, path: "/health", expected_code: 200 }
- { port: 8080, path: "/api/status", expected_code: 200 }
- name: Verify services running
ansible.builtin.systemd:
name: "{{ item }}"
register: svc_check
loop: "{{ required_services }}"
- name: Assert all services active
ansible.builtin.assert:
that: item.status.ActiveState == 'active'
fail_msg: "Service {{ item.item }} not running on {{ inventory_hostname }}"
loop: "{{ svc_check.results }}"
- name: Compare source vs cloud response
ansible.builtin.uri:
url: "http://{{ item }}:8080/api/status"
return_content: true
loop:
- "{{ source_host }}"
- "{{ inventory_hostname }}"
register: comparison
- name: Validate responses match
ansible.builtin.assert:
that: comparison.results[0].content == comparison.results[1].content
fail_msg: "Response mismatch between source and cloud"
See also: Ansible vs GitHub Actions: Key Differences & When to Use Each (2026)
Phase 4: Cutover
- name: DNS cutover
hosts: localhost
tasks:
- name: Update DNS records
amazon.aws.route53:
zone: example.com
record: "{{ item.record }}"
type: A
value: "{{ item.cloud_ip }}"
ttl: 60
overwrite: true
state: present
loop: "{{ dns_cutover_records }}"
- name: Verify DNS propagation
ansible.builtin.command: "dig +short {{ item.record }}"
register: dns_check
retries: 30
delay: 10
until: item.cloud_ip in dns_check.stdout
loop: "{{ dns_cutover_records }}"
- name: Decommission source servers
hosts: source_servers
become: true
tasks:
- name: Stop services (keep data for rollback period)
ansible.builtin.systemd:
name: "{{ item }}"
state: stopped
enabled: false
loop: "{{ app_services }}"
- name: Mark as decommissioned
ansible.builtin.copy:
content: "Decommissioned: {{ ansible_date_time.iso8601 }}\nMigrated to: cloud equivalent"
dest: /etc/DECOMMISSIONED
Re-Platform Pattern
- name: Re-platform to managed services
hosts: localhost
tasks:
- name: Create RDS instance (replaces self-managed PostgreSQL)
amazon.aws.rds_instance:
id: "{{ app_name }}-db"
engine: postgres
engine_version: "16"
db_instance_class: db.r6g.large
allocated_storage: 100
master_username: "{{ db_admin_user }}"
master_user_password: "{{ vault_db_admin_pass }}"
vpc_security_group_ids: ["{{ db_sg_id }}"]
multi_az: true
backup_retention_period: 30
storage_encrypted: true
register: rds
- name: Create ElastiCache (replaces self-managed Redis)
amazon.aws.elasticache:
name: "{{ app_name }}-cache"
engine: redis
node_type: cache.r6g.large
num_nodes: 3
security_group_ids: ["{{ cache_sg_id }}"]
register: elasticache
- name: Update application config
ansible.builtin.template:
src: app-config-cloud.j2
dest: /etc/myapp/config.yml
delegate_to: "{{ item }}"
loop: "{{ groups['cloud_app_servers'] }}"
vars:
db_host: "{{ rds.endpoint.address }}"
redis_host: "{{ elasticache.endpoint }}"
notify: restart application
Best Practices
Discover before migrating — Full assessment of every server's config, dependencies, and data Migrate in waves — Start with low-risk, stateless workloads; databases last Keep rollback path — Don't decommission sources until cloud is validated (2-4 weeks) Test, test, test — Automated validation comparing source vs cloud behavior Low TTL DNS before cutover — Reduce DNS TTL to 60s days before migration Bandwidth planning — Large data transfers need dedicated network paths or AWS DataSync Security group mirroring — Cloud security groups should match on-prem firewall rules exactly Track progress — Migration dashboard with per-workload statusFAQ
How long does a typical migration take?
Per server: 2-4 hours for lift-and-shift (excluding data transfer). Per organization: 3-12 months for 100+ servers. Ansible reduces per-server time by 60-80%.
What about licensing?
Track OS and application licenses during discovery. Some on-prem licenses don't transfer to cloud — you may need cloud-specific licensing (BYOL or marketplace).
Hybrid during migration?
Yes — most migrations run hybrid for weeks/months. Use VPN/Direct Connect between on-prem and cloud. Ansible manages both sides with separate inventory groups.
Conclusion
Ansible transforms cloud migration from a risky manual project into a repeatable, automated process. By codifying discovery, provisioning, configuration replication, and validation as playbooks, you reduce migration risk and time while ensuring nothing gets missed.
Related Articles
• Ansible Multi-Cloud Automation • Ansible AWS Complete Guide • Ansible Terraform IntegrationCategory: installation