Post

Using Ansible with GitHub Actions to Deploy Web Services to Azure (Without Dedicated Servers)

Using Ansible with GitHub Actions to Deploy Web Services to Azure

In this comprehensive guide, you’ll learn how to use Ansible with GitHub Actions to deploy a simple web service to Azure without needing any dedicated Ansible control servers. GitHub Actions runners themselves will act as Ansible control nodes, making this a serverless approach to infrastructure automation.

Why Ansible with GitHub Actions?

Benefits of this approach:

  1. No dedicated servers: GitHub Actions runners serve as ephemeral Ansible control nodes
  2. Cost-effective: No need to maintain separate automation servers
  3. Version-controlled: All playbooks and workflows stored in Git
  4. Integrated CI/CD: Deployment automation as part of your development workflow
  5. Scalable: GitHub Actions handles scaling automatically
  6. Secure: Uses GitHub secrets for credentials and Azure service principals

Prerequisites

Before starting, ensure you have:

  1. GitHub Account with access to GitHub Actions
  2. Azure Account with an active subscription
  3. Azure Service Principal for authentication
  4. Basic knowledge of Ansible playbooks and YAML
  5. SSH key pair for connecting to Azure VMs (if using VMs)

Architecture Overview

Here’s how the components work together:

1
GitHub Repository → GitHub Actions Workflow → Ansible Playbook → Azure Resources

Flow:

  1. Developer pushes code to GitHub
  2. GitHub Actions workflow triggers
  3. Workflow installs Ansible on the runner
  4. Ansible playbook executes from the runner
  5. Ansible provisions/configures Azure resources
  6. Web service deployed to Azure

Part 1: Setting Up Azure Prerequisites

Create Azure Service Principal

First, create a service principal for GitHub Actions to authenticate with Azure:

1
2
3
4
5
6
7
8
# Login to Azure CLI
az login

# Create a service principal
az ad sp create-for-rbac --name "github-actions-ansible" \
  --role contributor \
  --scopes /subscriptions/{subscription-id} \
  --sdk-auth

Output (save this - you’ll need it for GitHub secrets):

1
2
3
4
5
6
{
  "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "clientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "subscriptionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Generate SSH Keys

Generate an SSH key pair for connecting to Azure VMs:

1
2
3
4
5
6
7
8
# Generate SSH key pair (don't use a passphrase for automation)
ssh-keygen -t rsa -b 4096 -f ~/.ssh/azure_vm_key -N ""

# Display public key (save this)
cat ~/.ssh/azure_vm_key.pub

# Display private key (save this securely)
cat ~/.ssh/azure_vm_key

Part 2: Configure GitHub Repository Secrets

Add the following secrets to your GitHub repository (Settings → Secrets and variables → Actions):

  1. AZURE_CREDENTIALS: Paste the entire JSON output from service principal creation
  2. AZURE_CLIENT_ID: The clientId from the JSON
  3. AZURE_CLIENT_SECRET: The clientSecret from the JSON
  4. AZURE_SUBSCRIPTION_ID: The subscriptionId from the JSON
  5. AZURE_TENANT_ID: The tenantId from the JSON
  6. SSH_PRIVATE_KEY: The content of ~/.ssh/azure_vm_key (private key)
  7. SSH_PUBLIC_KEY: The content of ~/.ssh/azure_vm_key.pub (public key)

Part 3: Create Ansible Inventory

Create an inventory file for Azure dynamic inventory:

File: ansible/inventory/azure_rm.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Azure dynamic inventory configuration
plugin: azure.azcollection.azure_rm
auth_source: env

# Filter by resource group
include_vm_resource_groups:
  - "ansible-demo-rg"

# Group by various attributes
keyed_groups:
  - key: tags.environment
    prefix: env
  - key: tags.role
    prefix: role

# Conditional groups
conditional_groups:
  webservers: "'web' in tags.role"
  databases: "'db' in tags.role"

Part 4: Create Ansible Configuration

File: ansible/ansible.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[defaults]
inventory = ./inventory/azure_rm.yml
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_cache
fact_caching_timeout = 7200

[inventory]
enable_plugins = azure.azcollection.azure_rm

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
pipelining = True

Part 5: Create Ansible Playbook for Azure VM and Web Service

File: ansible/playbooks/deploy_web_service.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
---
- name: Deploy Web Service Infrastructure to Azure
  hosts: localhost
  connection: local
  gather_facts: no
  
  vars:
    resource_group: "ansible-demo-rg"
    location: "eastus"
    vm_name: "webserver-vm"
    admin_username: "azureuser"
    
  tasks:
    - name: Create Resource Group
      azure.azcollection.azure_rm_resourcegroup:
        name: ""
        location: ""
        tags:
          environment: production
          project: ansible-demo
    
    - name: Create Virtual Network
      azure.azcollection.azure_rm_virtualnetwork:
        resource_group: ""
        name: "-vnet"
        address_prefixes_cidr:
          - "10.0.0.0/16"
        tags:
          environment: production
    
    - name: Create Subnet
      azure.azcollection.azure_rm_subnet:
        resource_group: ""
        virtual_network_name: "-vnet"
        name: "-subnet"
        address_prefix_cidr: "10.0.1.0/24"
    
    - name: Create Public IP Address
      azure.azcollection.azure_rm_publicipaddress:
        resource_group: ""
        name: "-public-ip"
        allocation_method: Static
        sku: Standard
        tags:
          environment: production
      register: public_ip
    
    - name: Create Network Security Group
      azure.azcollection.azure_rm_securitygroup:
        resource_group: ""
        name: "-nsg"
        tags:
          environment: production
        rules:
          - name: AllowSSH
            protocol: Tcp
            destination_port_range: 22
            access: Allow
            priority: 1000
            direction: Inbound
          - name: AllowHTTP
            protocol: Tcp
            destination_port_range: 80
            access: Allow
            priority: 1001
            direction: Inbound
          - name: AllowHTTPS
            protocol: Tcp
            destination_port_range: 443
            access: Allow
            priority: 1002
            direction: Inbound
    
    - name: Create Network Interface
      azure.azcollection.azure_rm_networkinterface:
        resource_group: ""
        name: "-nic"
        virtual_network: "-vnet"
        subnet_name: "-subnet"
        public_ip_name: "-public-ip"
        security_group: "-nsg"
        tags:
          environment: production
    
    - name: Create Virtual Machine
      azure.azcollection.azure_rm_virtualmachine:
        resource_group: ""
        name: ""
        vm_size: Standard_B1s
        admin_username: ""
        ssh_password_enabled: false
        ssh_public_keys:
          - path: "/home//.ssh/authorized_keys"
            key_data: ""
        network_interfaces: "-nic"
        image:
          offer: 0001-com-ubuntu-server-jammy
          publisher: Canonical
          sku: '22_04-lts-gen2'
          version: latest
        os_disk_caching: ReadWrite
        managed_disk_type: Standard_LRS
        tags:
          environment: production
          role: web
      register: vm
    
    - name: Display VM Public IP
      debug:
        msg: "VM created with public IP: "
    
    - name: Add VM to inventory
      add_host:
        name: ""
        groups: webservers
        ansible_user: ""
        ansible_ssh_private_key_file: /tmp/ssh_key
    
    - name: Wait for SSH to be available
      wait_for:
        host: ""
        port: 22
        delay: 10
        timeout: 300
        state: started

- name: Configure Web Server
  hosts: webservers
  become: yes
  gather_facts: yes
  
  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
    
    - name: Install required packages
      apt:
        name:
          - nginx
          - python3-pip
          - git
        state: present
    
    - name: Create web application directory
      file:
        path: /var/www/myapp
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'
    
    - name: Deploy simple HTML page
      copy:
        content: |
          <!DOCTYPE html>
          <html>
          <head>
              <title>Ansible Deployed Web Service</title>
              <style>
                  body {
                      font-family: Arial, sans-serif;
                      margin: 50px;
                      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                      color: white;
                  }
                  .container {
                      background: rgba(255, 255, 255, 0.1);
                      padding: 30px;
                      border-radius: 10px;
                      backdrop-filter: blur(10px);
                  }
                  h1 { font-size: 2.5em; }
                  .info { background: rgba(0,0,0,0.2); padding: 15px; border-radius: 5px; margin-top: 20px; }
              </style>
          </head>
          <body>
              <div class="container">
                  <h1>🚀 Successfully Deployed!</h1>
                  <p>This web service was deployed using:</p>
                  <ul>
                      <li><strong>GitHub Actions</strong> for CI/CD automation</li>
                      <li><strong>Ansible</strong> for configuration management</li>
                      <li><strong>Azure</strong> for cloud infrastructure</li>
                  </ul>
                  <div class="info">
                      <p><strong>Deployment Time:</strong> </p>
                      <p><strong>Server:</strong> </p>
                      <p><strong>OS:</strong>  </p>
                  </div>
              </div>
          </body>
          </html>
        dest: /var/www/myapp/index.html
        owner: www-data
        group: www-data
        mode: '0644'
    
    - name: Configure Nginx site
      copy:
        content: |
          server {
              listen 80 default_server;
              listen [::]:80 default_server;
              
              root /var/www/myapp;
              index index.html;
              
              server_name _;
              
              location / {
                  try_files $uri $uri/ =404;
              }
              
              # Security headers
              add_header X-Frame-Options "SAMEORIGIN" always;
              add_header X-Content-Type-Options "nosniff" always;
              add_header X-XSS-Protection "1; mode=block" always;
          }
        dest: /etc/nginx/sites-available/myapp
        owner: root
        group: root
        mode: '0644'
      notify: Restart Nginx
    
    - name: Enable Nginx site
      file:
        src: /etc/nginx/sites-available/myapp
        dest: /etc/nginx/sites-enabled/myapp
        state: link
      notify: Restart Nginx
    
    - name: Remove default Nginx site
      file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: Restart Nginx
    
    - name: Ensure Nginx is running
      service:
        name: nginx
        state: started
        enabled: yes
  
  handlers:
    - name: Restart Nginx
      service:
        name: nginx
        state: restarted

Part 6: Create GitHub Actions Workflow

File: .github/workflows/deploy-to-azure.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
name: Deploy Web Service to Azure with Ansible

on:
  push:
    branches:
      - main
      - develop
  workflow_dispatch:
    inputs:
      action:
        description: 'Action to perform'
        required: true
        default: 'deploy'
        type: choice
        options:
          - deploy
          - destroy

env:
  ANSIBLE_VERSION: '2.15.0'
  AZURE_COLLECTION_VERSION: '2.1.0'

jobs:
  deploy:
    name: Deploy to Azure
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      
      - name: Install Ansible and Azure dependencies
        run: |
          python -m pip install --upgrade pip
          pip install ansible==$
          pip install ansible[azure]
          ansible-galaxy collection install azure.azcollection:$
          pip install -r ~/.ansible/collections/ansible_collections/azure/azcollection/requirements-azure.txt
      
      - name: Verify Ansible installation
        run: |
          ansible --version
          ansible-galaxy collection list
      
      - name: Set up SSH key
        run: |
          echo "$" > /tmp/ssh_key
          chmod 600 /tmp/ssh_key
          eval $(ssh-agent -s)
          ssh-add /tmp/ssh_key
      
      - name: Set Azure credentials
        run: |
          echo "AZURE_CLIENT_ID=$" >> $GITHUB_ENV
          echo "AZURE_SECRET=$" >> $GITHUB_ENV
          echo "AZURE_SUBSCRIPTION_ID=$" >> $GITHUB_ENV
          echo "AZURE_TENANT=$" >> $GITHUB_ENV
          echo "SSH_PUBLIC_KEY=$" >> $GITHUB_ENV
      
      - name: Run Ansible playbook
        working-directory: ./ansible
        run: |
          ansible-playbook playbooks/deploy_web_service.yml \
            -e "ansible_ssh_private_key_file=/tmp/ssh_key" \
            -v
      
      - name: Clean up SSH key
        if: always()
        run: |
          rm -f /tmp/ssh_key
      
      - name: Deployment summary
        if: success()
        run: |
          echo "### ✅ Deployment Successful!" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "Web service deployed to Azure successfully." >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Next Steps:**" >> $GITHUB_STEP_SUMMARY
          echo "1. Check Azure Portal for resource group: ansible-demo-rg" >> $GITHUB_STEP_SUMMARY
          echo "2. Access the web service using the public IP address" >> $GITHUB_STEP_SUMMARY
          echo "3. Monitor the deployment in Azure Monitor" >> $GITHUB_STEP_SUMMARY
      
      - name: Deployment failed
        if: failure()
        run: |
          echo "### ❌ Deployment Failed" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "Please check the logs above for error details." >> $GITHUB_STEP_SUMMARY

Part 7: Optional - Cleanup Playbook

File: ansible/playbooks/cleanup_azure.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---
- name: Cleanup Azure Resources
  hosts: localhost
  connection: local
  gather_facts: no
  
  vars:
    resource_group: "ansible-demo-rg"
  
  tasks:
    - name: Delete Resource Group
      azure.azcollection.azure_rm_resourcegroup:
        name: ""
        state: absent
        force_delete_nonempty: yes
      register: rg_delete
    
    - name: Display cleanup status
      debug:
        msg: "Resource group  has been deleted"
      when: rg_delete.changed

Part 8: Repository Structure

Your repository should have this structure:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── .github/
│   └── workflows/
│       └── deploy-to-azure.yml
├── ansible/
│   ├── ansible.cfg
│   ├── inventory/
│   │   └── azure_rm.yml
│   └── playbooks/
│       ├── deploy_web_service.yml
│       └── cleanup_azure.yml
└── README.md

Testing the Deployment

1. Local Testing (Optional)

Test Ansible playbook syntax locally:

1
2
3
cd ansible
ansible-playbook playbooks/deploy_web_service.yml --syntax-check
ansible-lint playbooks/deploy_web_service.yml

2. Trigger GitHub Actions Workflow

Option A: Push to main branch

1
2
3
git add .
git commit -m "Add Ansible deployment workflow"
git push origin main

Option B: Manual trigger via GitHub UI

  1. Go to Actions tab in your repository
  2. Select “Deploy Web Service to Azure with Ansible”
  3. Click “Run workflow”
  4. Select branch and action (deploy)
  5. Click “Run workflow”

3. Monitor Deployment

Watch the workflow execution in the Actions tab:

  1. Click on the running workflow
  2. Click on the “Deploy to Azure” job
  3. Expand each step to see detailed logs
  4. Wait for completion (typically 5-10 minutes)

4. Verify Deployment

Once complete, verify the deployment:

1
2
3
4
5
# Get the public IP from Azure Portal or CLI
az vm show -d -g ansible-demo-rg -n webserver-vm --query publicIps -o tsv

# Test the web service
curl http://<public-ip>

You should see the deployed HTML page in your browser.

Best Practices

1. Security

  • Never commit secrets: Always use GitHub Secrets
  • Use service principals: Don’t use personal credentials
  • Rotate credentials: Regularly rotate Azure service principal secrets
  • Limit permissions: Use least-privilege access for service principals
  • Enable Azure Key Vault: Store sensitive data in Azure Key Vault

2. Idempotency

  • Design for reruns: Ensure playbooks can run multiple times safely
  • Use check mode: Test with ansible-playbook --check before applying
  • Handle errors gracefully: Use ignore_errors and failed_when appropriately

3. Version Control

  • Tag releases: Use Git tags for playbook versions
  • Document changes: Maintain a CHANGELOG
  • Use branches: Develop in feature branches, deploy from main
  • Code review: Require PR reviews before merging

4. Performance

  • Use caching: Enable fact caching to speed up runs
  • Parallel execution: Use serial or forks for controlled parallelism
  • Optimize tasks: Combine tasks where possible
  • Use dynamic inventory: Avoid static inventory files

5. Monitoring

  • Enable logging: Configure Ansible logging
  • Use Azure Monitor: Set up monitoring and alerts
  • Check workflow status: Monitor GitHub Actions workflow runs
  • Cost tracking: Tag resources for cost allocation

Troubleshooting

Common Issues

1. Authentication Failures

1
Error: Unable to authenticate with Azure

Solution: Verify Azure credentials in GitHub Secrets

1
2
3
4
5
# Test credentials locally
az login --service-principal \
  -u $AZURE_CLIENT_ID \
  -p $AZURE_CLIENT_SECRET \
  --tenant $AZURE_TENANT_ID

2. SSH Connection Failures

1
Error: Failed to connect to host

Solution: Check NSG rules and SSH key configuration

1
2
3
4
5
# Verify NSG allows SSH
az network nsg rule show -g ansible-demo-rg --nsg-name webserver-vm-nsg -n AllowSSH

# Test SSH connection
ssh -i /tmp/ssh_key azureuser@<public-ip>

3. Ansible Module Not Found

1
Error: The module azure.azcollection.azure_rm_resourcegroup was not found

Solution: Install Azure collection

1
2
ansible-galaxy collection install azure.azcollection
pip install -r requirements-azure.txt

4. Timeout Errors

1
Error: Timeout waiting for SSH

Solution: Increase wait timeout or check Azure region availability

5. Quota Exceeded

1
Error: Quota exceeded for VM size

Solution: Request quota increase or use smaller VM size

Advanced Scenarios

1. Deploy Multiple Environments

Use variables to deploy to dev/staging/prod:

1
2
3
4
5
6
7
8
# In workflow
- name: Deploy to environment
  env:
    ENVIRONMENT: $
  run: |
    ansible-playbook playbooks/deploy_web_service.yml \
      -e "environment=$ENVIRONMENT" \
      -e "resource_group=ansible-$ENVIRONMENT-rg"

2. Blue-Green Deployment

Implement blue-green deployment strategy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- name: Blue-Green Deployment
  tasks:
    - name: Deploy to green environment
      include_tasks: deploy_green.yml
    
    - name: Verify green environment
      uri:
        url: "http://"
        status_code: 200
    
    - name: Switch traffic to green
      include_tasks: switch_traffic.yml
    
    - name: Cleanup blue environment
      include_tasks: cleanup_blue.yml

3. Ansible Vault for Secrets

Use Ansible Vault for additional secrets:

1
2
3
4
5
6
7
8
# Create vault password file
echo "$" > .vault_pass

# Encrypt sensitive files
ansible-vault encrypt vars/secrets.yml --vault-password-file .vault_pass

# Use in playbook
ansible-playbook playbooks/deploy.yml --vault-password-file .vault_pass

4. Dynamic Scaling

Scale based on load:

1
2
3
4
5
6
- name: Scale web servers
  azure.azcollection.azure_rm_virtualmachine:
    resource_group: ""
    name: "webserver-"
    # ... other params
  loop: ""

Cost Optimization

Tips to minimize Azure costs:

  1. Use B-series VMs: Burstable VMs for dev/test (Standard_B1s)
  2. Auto-shutdown: Schedule VM shutdown during off-hours
  3. Reserved instances: Commit for discounts on production
  4. Spot instances: Use for non-critical workloads
  5. Cleanup resources: Delete resources when not needed

Cleanup command:

1
2
# Delete all resources
ansible-playbook ansible/playbooks/cleanup_azure.yml

Extending the Solution

1. Add Application Deployment

Deploy a Python Flask app instead of static HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- name: Install Python application
  tasks:
    - name: Clone application repository
      git:
        repo: https://github.com/yourusername/flask-app
        dest: /opt/myapp
    
    - name: Install Python dependencies
      pip:
        requirements: /opt/myapp/requirements.txt
    
    - name: Configure systemd service
      template:
        src: myapp.service.j2
        dest: /etc/systemd/system/myapp.service

2. Add Database Support

Include Azure Database for PostgreSQL:

1
2
3
4
5
6
7
8
9
10
- name: Create PostgreSQL server
  azure.azcollection.azure_rm_postgresqlserver:
    resource_group: ""
    name: ""
    sku:
      name: B_Gen5_1
      tier: Basic
    storage_mb: 5120
    version: '11'
    enforce_ssl: true

3. Add Monitoring

Include Azure Monitor and Application Insights:

1
2
3
4
5
- name: Create Log Analytics workspace
  azure.azcollection.azure_rm_loganalyticsworkspace:
    resource_group: ""
    name: ""
    location: ""

4. Add Load Balancer

Use Azure Load Balancer for high availability:

1
2
3
4
5
6
7
- name: Create Load Balancer
  azure.azcollection.azure_rm_loadbalancer:
    resource_group: ""
    name: ""
    frontend_ip_configurations:
      - name: frontendIPConfig
        public_ip_address: ""

Conclusion

You’ve successfully learned how to use Ansible with GitHub Actions to deploy web services to Azure without maintaining dedicated automation servers. This approach provides a powerful, cost-effective, and scalable solution for infrastructure automation.

Key Takeaways:

  1. Serverless automation: GitHub Actions runners act as Ansible control nodes
  2. No infrastructure overhead: No need for dedicated Ansible servers
  3. Version-controlled IaC: All automation code in Git
  4. Azure integration: Native Azure module support in Ansible
  5. CI/CD pipeline: Automated deployment on code changes
  6. Secure: Uses service principals and GitHub Secrets
  7. Scalable: Handles multiple environments and resources

Next Steps:

  • Implement role-based access control (RBAC) in Azure
  • Add comprehensive monitoring and alerting
  • Create reusable Ansible roles for common tasks
  • Implement automated testing with Molecule
  • Explore Terraform + Ansible hybrid approach
  • Add container orchestration with Azure Kubernetes Service

This pattern works for any cloud provider (AWS, GCP, DigitalOcean) with appropriate Ansible modules and service authentication. The principles remain the same: use ephemeral CI/CD runners as control nodes for agentless infrastructure automation.

This post is licensed under CC BY 4.0 by the author.