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

StrategyComplexityDowntimeAnsible Role
Lift-and-ShiftLowMediumReplicate server configs in cloud
Re-PlatformMediumMediumAdapt to managed services
Re-FactorHighLowContainerize and deploy to K8s
Re-PurchaseLowHighMigrate data to SaaS
RetireTrivialNoneDecommission 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 }}"

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

See also: Meeting Rajveer Singh, Red Hat Consultant, at Delhi Book Festival

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"

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

See also: Ansible vs GitHub Actions: Key Differences & When to Use Each (2026)

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

  1. Discover before migrating — Full assessment of every server's config, dependencies, and data
  2. Migrate in waves — Start with low-risk, stateless workloads; databases last
  3. Keep rollback path — Don't decommission sources until cloud is validated (2-4 weeks)
  4. Test, test, test — Automated validation comparing source vs cloud behavior
  5. Low TTL DNS before cutover — Reduce DNS TTL to 60s days before migration
  6. Bandwidth planning — Large data transfers need dedicated network paths or AWS DataSync
  7. Security group mirroring — Cloud security groups should match on-prem firewall rules exactly
  8. 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.

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home