--- name: azure-pipeline-ansible description: Extends azure-devops-pipeline for Ansible playbook runs. Handles syntax check, galaxy install, vault passwords, SSH key injection, check mode on nonprod, and dynamic AWS EC2 inventory. Always load azure-devops-pipeline first. --- ## What I add Type-specific steps for Ansible pipelines. Merge these into the skeleton from `azure-devops-pipeline`. ## Additional required inputs — ask the user 1. **Playbook path** — e.g. `playbooks/site.yml` 2. **Inventory source** — `static` | `dynamic-aws-ec2` 3. **Ansible Vault in use** — `yes` | `no` 4. **ADO secret variable name for vault password** — if vault in use, e.g. `ANSIBLE_VAULT_PASSWORD` 5. **ADO secret variable name for SSH private key** — e.g. `ANSIBLE_SSH_KEY` 6. **Ansible version to pin** — e.g. `9.2.0` 7. **Run --check mode on nonprod before real apply** — `yes` (default) | `no` ## Lint stage steps ```yaml - script: | pip install "ansible==$(ANSIBLE_VERSION)" ansible-lint ansible-lint --profile production displayName: "Lint — ansible-lint" env: ANSIBLE_VERSION: ``` ## Security scan stage steps ```yaml - script: | pip install "ansible==$(ANSIBLE_VERSION)" ansible-lint ansible-lint --profile security \ --sarif-file ansible-lint-security.sarif || true ansible-galaxy install -r requirements.yml --force displayName: "Security scan — ansible-lint security profile" env: ANSIBLE_VERSION: - task: PublishBuildArtifacts@1 inputs: pathToPublish: ansible-lint-security.sarif artifactName: security-scan displayName: "Publish scan results" ``` ## Build stage steps ```yaml - script: | pip install "ansible==$(ANSIBLE_VERSION)" [ -f requirements.yml ] && ansible-galaxy install -r requirements.yml || true ansible-playbook --syntax-check -i displayName: "Validate — syntax check and galaxy install" env: ANSIBLE_VERSION: ``` Note: for dynamic-aws-ec2 inventory, replace `-i ` with `-i aws_ec2.yml` and ensure `aws_ec2.yml` exists in the repo with the `amazon.aws.aws_ec2` plugin configured. ## Deploy stage steps ### Step order — always emit in this order 1. Write SSH key to temp file 2. Write vault password to temp file (if vault in use) 3. Check mode run (nonprod only, if enabled) 4. Real playbook run 5. Clean up SSH key (condition: always) 6. Clean up vault password (condition: always) ### SSH key injection (always include) ```yaml - script: | echo "$(ANSIBLE_SSH_KEY)" > /tmp/ansible_ssh_key chmod 600 /tmp/ansible_ssh_key displayName: "Inject SSH key" env: ANSIBLE_SSH_KEY: $(ANSIBLE_SSH_KEY) ``` ### Vault password file (include only if vault in use) ```yaml - script: | echo "$(ANSIBLE_VAULT_PASSWORD)" > /tmp/vault_pass chmod 600 /tmp/vault_pass displayName: "Write vault password file" env: ANSIBLE_VAULT_PASSWORD: $(ANSIBLE_VAULT_PASSWORD) ``` ### Check mode run (nonprod only, if enabled) ```yaml - script: | VAULT_ARGS="" [ -f /tmp/vault_pass ] && VAULT_ARGS="--vault-password-file /tmp/vault_pass" ansible-playbook \ -i \ --check \ --diff \ --private-key /tmp/ansible_ssh_key \ $VAULT_ARGS displayName: "Dry run — check mode" ``` ### Real run ```yaml - script: | VAULT_ARGS="" [ -f /tmp/vault_pass ] && VAULT_ARGS="--vault-password-file /tmp/vault_pass" ansible-playbook \ -i \ --diff \ --private-key /tmp/ansible_ssh_key \ $VAULT_ARGS displayName: "Apply playbook" ``` ### Cleanup (always at end of deploy steps — condition: always()) ```yaml - script: rm -f /tmp/ansible_ssh_key displayName: "Clean up SSH key" condition: always() - script: rm -f /tmp/vault_pass displayName: "Clean up vault password file" condition: always() ``` ## Hard rules for Ansible - Always pin Ansible version with quoted pip specifier `"ansible==$(ANSIBLE_VERSION)"` — never use `latest`, unquoted `==` may fail in some shells - Always clean up SSH key and vault password files with `condition: always()` — they must be removed even if the playbook fails - Always include `--diff` on real runs so changes are visible in pipeline logs - SSH key file permissions must be `600` — Ansible refuses keys with broader permissions - Use shell variable expansion (`VAULT_ARGS=""`) rather than subshell substitution in the step script to avoid bash syntax issues in ADO agents - For dynamic inventory, AWS credentials come from the OIDC service connection environment — same pattern as Lambda - `requirements.yml` must exist in the repo if galaxy install step is included; if uncertain, wrap with `[ -f requirements.yml ] && ansible-galaxy install -r requirements.yml || true`