# How to run Ansible in CI/CD

Source: https://docs.quake.ai/docs/automation/how-to/ansible-cicd
Markdown: https://docs.quake.ai/docs/automation/how-to/ansible-cicd.md

---

# How to run Ansible in CI/CD

This guide runs Ansible playbooks against Quake AI from GitHub Actions and GitLab CI. The same pipeline patterns drive scheduled patching, release deploys, and the Day 1 configuration step that follows an OpenTofu provision.

For the OpenTofu equivalent, see [How to integrate OpenTofu with CI/CD](/docs/automation/how-to/cicd-integration). For the combined provision-and-configure flow, see [How to use Ansible with OpenTofu on Quake AI](/docs/automation/how-to/ansible-opentofu-workflow).

## Prerequisites

- A working playbook that runs locally against Quake AI (see [getting-started](/docs/automation/how-to/getting-started-ansible))
- Application credentials available as CI secrets, either as `clouds.yaml` content or as the discrete `OS_*` variables. The same credentials drive the modules and the dynamic inventory plugin.
- A GitHub or GitLab repository with the playbook and inventory committed

## Secrets configuration

Both CI platforms need the Quake AI authentication values stored as secrets. The minimum set:

| Variable | Purpose |
|---|---|
| `OS_AUTH_URL` | Quake AI Keystone endpoint |
| `OS_APPLICATION_CREDENTIAL_ID` | Application credential id |
| `OS_APPLICATION_CREDENTIAL_SECRET` | Application credential secret |
| `OS_REGION_NAME` | Region (for example `us-east-1`) |
| `OS_INTERFACE` | `public` for most projects |
| `OS_IDENTITY_API_VERSION` | `3` |
| `ANSIBLE_VAULT_PASSWORD` | Optional: only if your playbook reads `ansible-vault` files |
| `SSH_PRIVATE_KEY` | The key Ansible uses to reach managed hosts (PEM format) |



Application credentials are scoped, rotatable, and do not embed your account password. Generate a dedicated credential per pipeline and rotate them on a schedule. The full flow is documented in [Generate app credentials](/docs/tools/generate-app-credentials).



The `OS_*` variables shown above match the [authentication chain](/docs/automation/concepts/ansible#authentication-on-quake-ai) the modules and the inventory plugin both read. CI runners typically prefer environment variables over a checked-in `clouds.yaml`.

## GitHub actions

### Single-job playbook run

The minimal pipeline installs Ansible plus `openstacksdk`, writes the SSH key to disk, and runs the playbook. Trigger on `workflow_dispatch` so production runs require an explicit click:

```yaml
name: Configure infrastructure

on:
  workflow_dispatch:
  push:
    branches: [main]
    paths:
      - "playbooks/**"
      - "inventory/**"

jobs:
  configure:
    runs-on: ubuntu-latest
    env:
      OS_AUTH_URL: ${{ secrets.OS_AUTH_URL }}
      OS_APPLICATION_CREDENTIAL_ID: ${{ secrets.OS_APPLICATION_CREDENTIAL_ID }}
      OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
      OS_REGION_NAME: ${{ secrets.OS_REGION_NAME }}
      OS_INTERFACE: public
      OS_IDENTITY_API_VERSION: "3"
      ANSIBLE_HOST_KEY_CHECKING: "False"
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Ansible and openstacksdk
        run: |
          pip install "ansible-core>=2.13" "openstacksdk>=1.0.0"
          ansible-galaxy collection install openstack.cloud

      - name: Write SSH key
        run: |
          mkdir -p ~/.ssh
          chmod 700 ~/.ssh
          printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519

      - name: Run playbook
        run: |
          ansible-playbook \
            -i inventory/openstack.yml \
            --private-key ~/.ssh/id_ed25519 \
            playbooks/site.yml
```

`ANSIBLE_HOST_KEY_CHECKING: "False"` disables strict host key prompts that would otherwise hang in a non-interactive runner. For environments where strict checking is required, prepopulate `~/.ssh/known_hosts` from a checked-in fixture instead.

### Sequenced provision-then-configure

The canonical Day 0 plus Day 1 pattern runs OpenTofu in one job and Ansible in a downstream job. The configure job depends on `needs: provision`, which guarantees order:

```yaml
name: Deploy

on:
  workflow_dispatch:

jobs:
  provision:
    runs-on: ubuntu-latest
    env:
      OS_AUTH_URL: ${{ secrets.OS_AUTH_URL }}
      OS_APPLICATION_CREDENTIAL_ID: ${{ secrets.OS_APPLICATION_CREDENTIAL_ID }}
      OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
      OS_REGION_NAME: ${{ secrets.OS_REGION_NAME }}
    steps:
      - uses: actions/checkout@v4
      - uses: opentofu/setup-opentofu@v1
      - run: tofu -chdir=infra init
      - run: tofu -chdir=infra apply -auto-approve

  configure:
    needs: provision
    runs-on: ubuntu-latest
    env:
      OS_AUTH_URL: ${{ secrets.OS_AUTH_URL }}
      OS_APPLICATION_CREDENTIAL_ID: ${{ secrets.OS_APPLICATION_CREDENTIAL_ID }}
      OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
      OS_REGION_NAME: ${{ secrets.OS_REGION_NAME }}
      ANSIBLE_HOST_KEY_CHECKING: "False"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }
      - run: |
          pip install "ansible-core>=2.13" "openstacksdk>=1.0.0"
          ansible-galaxy collection install openstack.cloud
      - name: Write SSH key
        run: |
          mkdir -p ~/.ssh && chmod 700 ~/.ssh
          printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
      - run: |
          ansible-playbook \
            -i inventory/openstack.yml \
            --private-key ~/.ssh/id_ed25519 \
            playbooks/site.yml
```

The configure job uses the [dynamic inventory plugin](/docs/automation/how-to/ansible-dynamic-inventory), which queries Quake AI directly. The provision job does not need to pass an inventory file forward: Ansible discovers the new instance through the metadata OpenTofu set on it. The full handoff (including the alternative `tofu output -json` path) is documented in [How to use Ansible with OpenTofu on Quake AI](/docs/automation/how-to/ansible-opentofu-workflow).

## GitLab CI

GitLab pipelines fit the same shape. Use a Python or Ansible image, pull credentials from masked CI/CD variables, and gate production with `when: manual`:

```yaml
stages:
  - configure

variables:
  ANSIBLE_HOST_KEY_CHECKING: "False"
  OS_INTERFACE: public
  OS_IDENTITY_API_VERSION: "3"

.ansible_base:
  image: python:3.11-slim
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends openssh-client
    - pip install "ansible-core>=2.13" "openstacksdk>=1.0.0"
    - ansible-galaxy collection install openstack.cloud
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
    - chmod 600 ~/.ssh/id_ed25519

configure_staging:
  extends: .ansible_base
  stage: configure
  script:
    - ansible-playbook -i inventory/openstack.yml --private-key ~/.ssh/id_ed25519 playbooks/site.yml --limit env_staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

configure_production:
  extends: .ansible_base
  stage: configure
  script:
    - ansible-playbook -i inventory/openstack.yml --private-key ~/.ssh/id_ed25519 playbooks/site.yml --limit env_production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
  environment:
    name: production
```

In GitLab, mark the secret variables (`OS_*`, `SSH_PRIVATE_KEY`) as **Masked** and **Protected** so they never appear in logs and only run on protected branches. Configure them under **Settings > CI/CD > Variables** in the project.

If you prefer a pre-baked Ansible runtime, swap `python:3.11-slim` for `quay.io/ansible/awx-ee` or another community Ansible image. The trade-off is image size against having `ansible-core` and `openstacksdk` already installed.

## Secret management

A few rules keep the pipeline credible:

- Never commit vault passwords, application credential secrets, or SSH private keys to the repository. Pipelines load them from the platform's secrets store.
- If a playbook reads `ansible-vault`-encrypted files, supply the password through `--vault-password-file <(echo "$ANSIBLE_VAULT_PASSWORD")` rather than echoing the value into a file the runner does not clean up.
- Rotate the application credential and the SSH private key on a schedule (every 90 days is a common cadence). Generate new ones, replace the secret values, and retire the old credential.
- Scope each pipeline to its own application credential. A leaked secret in a staging pipeline should not have permission to apply changes in production.
- Restrict who can trigger `workflow_dispatch` runs and who can approve manual GitLab gates. The platform's secrets store is only as strong as the access control on the people who can call it.

## Tear down

The pipeline configurations themselves do not create persistent infrastructure. Removing them stops future runs but does not change anything on Quake AI. To roll back the configuration applied by the playbook, write a teardown playbook with `state: absent` tasks (the [provisioning guide](/docs/automation/how-to/ansible-provision-instance#author-the-teardown-playbook) shows the pattern) and run it through the same pipeline.

## See also

- [Ansible on Quake AI](/docs/automation/concepts/ansible)
- [How to use Ansible with OpenTofu on Quake AI](/docs/automation/how-to/ansible-opentofu-workflow)
- [How to use Ansible dynamic inventory on Quake AI](/docs/automation/how-to/ansible-dynamic-inventory)
- [How to integrate OpenTofu with CI/CD](/docs/automation/how-to/cicd-integration)
- [How to deploy an application to a Quake AI VM from CI](/docs/automation/how-to/app-cicd-vm)
- [How to deploy to a Quake AI Kubernetes cluster from CI](/docs/kubernetes/how-to/deploy-from-ci)
- [How to provision a Quake AI instance with Ansible](/docs/automation/how-to/ansible-provision-instance)
- [Generate app credentials](/docs/tools/generate-app-credentials)
