AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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 status

FAQ

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 AutomationAnsible AWS Complete GuideAnsible Terraform Integration

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home