Ansible AWX vs AAP, Terraform Integration & CI/CD Pipelines
1. Ansible AWX vs AAP
What is AWX?
AWX is the open-source upstream project for Ansible Automation Platform — think of it like Fedora is to RHEL. It provides a web UI, REST API, and job scheduling on top of Ansible.
What is AAP?
AAP (Ansible Automation Platform) is the Red Hat enterprise product built from AWX — adds support, stability, Automation Hub, EDA, and a hardened release cycle.
Comparison Table
| Feature | AWX | AAP |
|---|---|---|
| Cost | Free / open source | Paid subscription |
| Support | Community only | Red Hat support |
| Release cycle | Rapid / unstable | Stable / tested |
| Automation Hub | No | Yes (certified content) |
| Event Driven Ansible | Limited | Full EDA component |
| Execution Environments | Yes | Yes + certified EEs |
| Upgrade path | Manual / complex | Supported upgrade |
| RBAC | Basic | Advanced |
| Analytics | No | Yes (Automation Analytics) |
| Air-gap install | Difficult | Supported |
| Best for | Labs / dev / learning | Enterprise production |
AAP Architecture — Full Picture
┌─────────────────────────────────────────────────────────────┐│ Ansible Automation Platform ││ ││ ┌─────────────────┐ ┌──────────────────────────────┐ ││ │ Automation Hub │ │ Automation Controller │ ││ │ │ │ │ ││ │ - Certified │ │ ┌────────────────────────┐ │ ││ │ Collections │ │ │ Job Templates │ │ ││ │ - Private repo │◄────┤ │ Workflows │ │ ││ │ - Sync from │ │ │ Schedules │ │ ││ │ Galaxy │ │ │ Credentials │ │ ││ └─────────────────┘ │ │ Inventories │ │ ││ │ │ Projects (SCM) │ │ ││ ┌─────────────────┐ │ └────────────────────────┘ │ ││ │ Event Driven │ └──────────────────────────────┘ ││ │ Ansible (EDA) │ │ ││ │ │ ┌─────────────▼────────────────┐ ││ │ - Rulebooks │ │ Execution Environments │ ││ │ - Event sources │ │ (containerized Ansible) │ ││ │ - Kafka/webhook │ └──────────────────────────────┘ ││ └─────────────────┘ ││ ││ ┌──────────────────────────────────────────────────────┐ ││ │ Automation Analytics (optional) │ ││ │ Job history, savings calculator, host metrics │ ││ └──────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────┘
AWX / AAP Key Concepts Deep Dive
Organizations & RBAC
Organization├── Teams│ ├── Team: Developers│ │ └── Role: Execute (Job Templates only)│ └── Team: Ops│ └── Role: Admin (all resources)├── Users├── Inventories├── Projects├── Job Templates└── Credentials
Credential Types
# Built-in credential types:Machine # SSH key/password for managed nodesSource Control # Git username/token/SSH keyVault # Ansible Vault passwordAWS # Access key + secretGCP # Service account JSONAzure # Client ID + secretOpenShift/K8s # Kubeconfig / bearer tokenHashiCorp Vault # AppRole / tokenCyberArk # API credentials
Job Template — Key Fields
Name: Deploy AppInventory: Production InventoryProject: App Deployment (Git)Playbook: deploy.ymlCredentials: - Machine: prod-ssh-key - Vault: prod-vault-pass - AWS: prod-aws-credsExtra Vars: env: production version: "{{ tower_job_id }}"Verbosity: 1 (Verbose)Job Tags: deploy,configLimit: webservers # Run only on webservers groupConcurrent: false # Prevent parallel runsTimeout: 3600Survey: enabled # Runtime input form
Workflow Job Templates
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐│ Run Tests │────►│ Deploy App │────►│ Smoke Tests │└─────────────┘ └──────────────┘ └────────┬────────┘ │ │ on_failure on_success │ │ ┌──────▼──────┐ ┌────────▼────────┐ │ Rollback │ │ Notify Slack │ └─────────────┘ └─────────────────┘
# Workflow node conditions:on_success # Run next node if this one succeedson_failure # Run next node if this one failsalways # Run next node regardless of result
Surveys — Runtime Input
# Survey fields on a Job Template- variable: app_version question_name: "App Version to Deploy" type: text required: true default: "latest"- variable: environment question_name: "Target Environment" type: multiplechoice choices: - dev - staging - production required: true- variable: replica_count question_name: "Number of Replicas" type: integer min: 1 max: 10 default: 2
AAP REST API — Automation
# Launch a job template via APIcurl -X POST \ https://aap.example.com/api/v2/job_templates/42/launch/ \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "extra_vars": { "env": "production", "version": "2.1.0" }, "limit": "webservers" }'# Check job statuscurl -X GET \ https://aap.example.com/api/v2/jobs/1234/ \ -H "Authorization: Bearer $TOKEN"# Get job outputcurl -X GET \ https://aap.example.com/api/v2/jobs/1234/stdout/?format=txt \ -H "Authorization: Bearer $TOKEN"
2. Ansible + Terraform Integration
Why Combine Ansible and Terraform?
┌────────────────────────────────────────────────────────┐│ Infrastructure Lifecycle ││ ││ ┌───────────────┐ ┌──────────────────────┐ ││ │ Terraform │ │ Ansible │ ││ │ │ │ │ ││ │ - Provision │─────────►│ - Configure OS │ ││ │ VMs/cloud │ │ - Install software │ ││ │ - Networks │ │ - Deploy apps │ ││ │ - Storage │ │ - Manage users │ ││ │ - Kubernetes │ │ - Day-2 operations │ ││ │ │ │ │ ││ │ "Build it" │ │ "Configure it" │ ││ └───────────────┘ └──────────────────────┘ ││ │ │ ││ └────────────────────────────┘ ││ Works together │└────────────────────────────────────────────────────────┘
Pattern 1 — Terraform Provisions, Ansible Configures
# main.tf — Terraform creates EC2 instancesresource "aws_instance" "web" { count = 3 ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.medium" tags = { Name = "web-${count.index + 1}" Role = "webserver" Environment = "production" } # Run Ansible after instance is created provisioner "local-exec" { command = <<-EOT sleep 30 # Wait for SSH ansible-playbook \ -i '${self.public_ip},' \ -u ec2-user \ --private-key ~/.ssh/prod-key.pem \ playbooks/configure-webserver.yml \ --extra-vars "env=production" EOT }}# Output IPs for Ansible dynamic inventoryoutput "web_ips" { value = aws_instance.web[*].public_ip}
Pattern 2 — Terraform Output → Ansible Inventory
# outputs.tfoutput "web_servers" { value = { for idx, instance in aws_instance.web : "web-${idx + 1}" => { ip = instance.public_ip private = instance.private_ip az = instance.availability_zone } }}
#!/usr/bin/env python3# dynamic_inventory.py — reads Terraform state for Ansible inventoryimport jsonimport subprocessimport sysdef get_terraform_output(): result = subprocess.run( ["terraform", "output", "-json"], capture_output=True, text=True ) return json.loads(result.stdout)def build_inventory(): tf_output = get_terraform_output() web_servers = tf_output["web_servers"]["value"] inventory = { "webservers": { "hosts": list(web_servers.keys()), }, "_meta": { "hostvars": { name: { "ansible_host": data["ip"], "private_ip": data["private"], "az": data["az"], "ansible_user": "ec2-user" } for name, data in web_servers.items() } } } return inventoryprint(json.dumps(build_inventory()))
# Use dynamic inventoryansible-playbook -i dynamic_inventory.py configure.yml
Pattern 3 — Ansible Calling Terraform
# playbook — use Ansible to run Terraform- name: Provision infrastructure with Terraform hosts: localhost gather_facts: false tasks: - name: Terraform init community.general.terraform: project_path: ./terraform state: present force_init: true backend_config: bucket: my-tf-state key: prod/terraform.tfstate region: us-east-1 variables: environment: production instance_count: "3" instance_type: t3.medium register: tf_result - name: Show outputs debug: var: tf_result.outputs - name: Add new hosts to inventory add_host: name: "{{ item.key }}" ansible_host: "{{ item.value.ip }}" groups: webservers loop: "{{ tf_result.outputs.web_servers.value | dict2items }}"- name: Configure newly provisioned servers hosts: webservers become: true roles: - common - webserver
Pattern 4 — Shared State via Terraform Remote State
# data.tf — read existing Terraform state in another playbookdata "terraform_remote_state" "network" { backend = "s3" config = { bucket = "my-tf-state" key = "network/terraform.tfstate" region = "us-east-1" }}# Use VPC ID from network stateresource "aws_instance" "web" { subnet_id = data.terraform_remote_state.network.outputs.subnet_id}
# Ansible reading Terraform state directly- name: Read Terraform state hosts: localhost tasks: - name: Get TF state from S3 amazon.aws.s3_object: bucket: my-tf-state object: prod/terraform.tfstate dest: /tmp/terraform.tfstate mode: get - name: Parse state set_fact: tf_state: "{{ lookup('file', '/tmp/terraform.tfstate') | from_json }}" - name: Extract outputs set_fact: web_ips: >- {{ tf_state.outputs.web_ips.value }}
3. CI/CD Pipelines with Ansible
Pipeline Patterns
┌──────────────────────────────────────────────────────────────┐│ CI/CD Pipeline ││ ││ Code Push ││ │ ││ ▼ ││ ┌──────┐ ┌────────┐ ┌──────────┐ ┌────────────────┐ ││ │ SCM │──►│ CI │──►│ Ansible │──►│ Production │ ││ │(Git) │ │(build/ │ │(deploy/ │ │ (live env) │ ││ │ │ │ test) │ │ config) │ │ │ ││ └──────┘ └────────┘ └──────────┘ └────────────────┘ ││ │└──────────────────────────────────────────────────────────────┘
Jenkins + Ansible Pipeline
// Jenkinsfilepipeline { agent any environment { ANSIBLE_HOST_KEY_CHECKING = 'False' VAULT_PASSWORD_FILE = credentials('ansible-vault-pass') } parameters { choice( name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'], description: 'Target environment' ) string( name: 'APP_VERSION', defaultValue: 'latest', description: 'Version to deploy' ) } stages { stage('Checkout') { steps { git branch: 'main', url: 'https://github.com/myorg/ansible-playbooks.git', credentialsId: 'github-token' } } stage('Lint & Syntax Check') { steps { sh ''' pip install ansible-lint ansible-lint playbooks/deploy.yml ansible-playbook playbooks/deploy.yml --syntax-check ''' } } stage('Molecule Tests') { when { branch 'main' } steps { sh ''' pip install molecule molecule-docker cd roles/myapp molecule test ''' } } stage('Deploy to Dev') { when { expression { params.ENVIRONMENT == 'dev' } } steps { sh """ ansible-playbook playbooks/deploy.yml \ -i inventories/dev/hosts.yml \ --vault-password-file ${VAULT_PASSWORD_FILE} \ -e app_version=${params.APP_VERSION} \ -e env=dev """ } } stage('Deploy to Staging') { when { expression { params.ENVIRONMENT == 'staging' } } steps { sh """ ansible-playbook playbooks/deploy.yml \ -i inventories/staging/hosts.yml \ --vault-password-file ${VAULT_PASSWORD_FILE} \ -e app_version=${params.APP_VERSION} \ -e env=staging """ } post { success { // Trigger smoke tests sh 'ansible-playbook playbooks/smoke-tests.yml -i inventories/staging/hosts.yml' } } } stage('Approval') { when { expression { params.ENVIRONMENT == 'production' } } steps { timeout(time: 1, unit: 'HOURS') { input message: 'Deploy to Production?', ok: 'Deploy', submitter: 'ops-team' } } } stage('Deploy to Production') { when { expression { params.ENVIRONMENT == 'production' } } steps { sh """ ansible-playbook playbooks/deploy.yml \ -i inventories/production/hosts.yml \ --vault-password-file ${VAULT_PASSWORD_FILE} \ -e app_version=${params.APP_VERSION} \ -e env=production \ --diff """ } } } post { success { slackSend channel: '#deployments', message: "✅ Deploy ${params.APP_VERSION} to ${params.ENVIRONMENT} succeeded" } failure { slackSend channel: '#deployments', message: "❌ Deploy ${params.APP_VERSION} to ${params.ENVIRONMENT} FAILED" // Auto rollback on production failure script { if (params.ENVIRONMENT == 'production') { sh 'ansible-playbook playbooks/rollback.yml -i inventories/production/hosts.yml' } } } }}
GitLab CI + Ansible
# .gitlab-ci.ymlstages: - lint - test - deploy-dev - deploy-staging - deploy-prodvariables: ANSIBLE_HOST_KEY_CHECKING: "false" PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"# Reusable anchor for Ansible setup.ansible_setup: &ansible_setup image: python:3.11-slim before_script: - pip install ansible ansible-lint molecule[docker] --quiet - ansible-galaxy collection install -r requirements.yml - echo "$VAULT_PASSWORD" > /tmp/vault_pass# Lint stageansible-lint: <<: *ansible_setup stage: lint script: - ansible-lint - ansible-playbook site.yml --syntax-check -i inventories/dev/# Molecule testsmolecule-test: <<: *ansible_setup stage: test services: - docker:dind variables: DOCKER_HOST: tcp://docker:2376 script: - | for role in roles/*/; do echo "Testing role: $role" cd "$role" molecule test cd - done only: - merge_requests - main# Deploy to devdeploy-dev: <<: *ansible_setup stage: deploy-dev script: - | ansible-playbook deploy.yml \ -i inventories/dev/hosts.yml \ --vault-password-file /tmp/vault_pass \ -e app_version=$CI_COMMIT_SHA \ -e env=dev environment: name: development url: https://dev.example.com only: - main# Deploy to stagingdeploy-staging: <<: *ansible_setup stage: deploy-staging script: - | ansible-playbook deploy.yml \ -i inventories/staging/hosts.yml \ --vault-password-file /tmp/vault_pass \ -e app_version=$CI_COMMIT_TAG \ -e env=staging environment: name: staging url: https://staging.example.com only: - tags# Deploy to production — manual gatedeploy-prod: <<: *ansible_setup stage: deploy-prod script: - | ansible-playbook deploy.yml \ -i inventories/production/hosts.yml \ --vault-password-file /tmp/vault_pass \ -e app_version=$CI_COMMIT_TAG \ -e env=production \ --diff environment: name: production url: https://example.com when: manual # Manual trigger allow_failure: false only: - tags
GitHub Actions + Ansible
# .github/workflows/deploy.ymlname: Deploy with Ansibleon: push: branches: [main] workflow_dispatch: inputs: environment: description: 'Environment' required: true type: choice options: [dev, staging, production] version: description: 'Version to deploy' required: true default: 'latest'jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install Ansible run: pip install ansible ansible-lint - name: Run ansible-lint run: ansible-lint - name: Syntax check run: ansible-playbook deploy.yml --syntax-check -i inventories/dev/ molecule-test: runs-on: ubuntu-latest needs: lint steps: - uses: actions/checkout@v4 - name: Install dependencies run: pip install ansible molecule[docker] - name: Run Molecule tests run: | for role in roles/*/; do cd "$role" && molecule test && cd - done deploy: runs-on: ubuntu-latest needs: molecule-test environment: ${{ github.event.inputs.environment || 'dev' }} steps: - uses: actions/checkout@v4 - name: Install Ansible + collections run: | pip install ansible ansible-galaxy collection install -r requirements.yml - name: Write vault password run: echo "${{ secrets.VAULT_PASSWORD }}" > /tmp/vault_pass - name: Write SSH key run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa - name: Deploy run: | ansible-playbook deploy.yml \ -i inventories/${{ github.event.inputs.environment }}/hosts.yml \ --vault-password-file /tmp/vault_pass \ --private-key ~/.ssh/id_rsa \ -e app_version=${{ github.event.inputs.version }} \ -e env=${{ github.event.inputs.environment }} - name: Cleanup secrets if: always() run: | rm -f /tmp/vault_pass ~/.ssh/id_rsa
Ansible + AAP in CI/CD Pipeline
# aap_trigger.py — trigger AAP job from CI pipelineimport requestsimport timeimport sysAAP_URL = "https://aap.example.com"TOKEN = os.environ["AAP_TOKEN"]JOB_TEMPLATE_ID = 42def launch_job(extra_vars): response = requests.post( f"{AAP_URL}/api/v2/job_templates/{JOB_TEMPLATE_ID}/launch/", headers={"Authorization": f"Bearer {TOKEN}"}, json={"extra_vars": extra_vars}, verify=True ) response.raise_for_status() return response.json()["id"]def wait_for_job(job_id, timeout=3600): start = time.time() while time.time() - start < timeout: response = requests.get( f"{AAP_URL}/api/v2/jobs/{job_id}/", headers={"Authorization": f"Bearer {TOKEN}"} ) job = response.json() status = job["status"] print(f"Job {job_id} status: {status}") if status == "successful": return True elif status in ["failed", "error", "canceled"]: return False time.sleep(15) raise TimeoutError(f"Job {job_id} timed out")# Usage in CIjob_id = launch_job({ "env": "production", "version": os.environ["APP_VERSION"]})success = wait_for_job(job_id)sys.exit(0 if success else 1)
Deployment Strategies with Ansible
Rolling Deploy
- name: Rolling deployment hosts: webservers serial: 1 # One host at a time # serial: "25%" # Or 25% at a time max_fail_percentage: 0 # Stop on any failure pre_tasks: - name: Remove from load balancer uri: url: "http://lb.example.com/api/drain/{{ inventory_hostname }}" method: POST delegate_to: localhost roles: - deploy-app post_tasks: - name: Health check uri: url: "http://{{ ansible_host }}:8080/health" status_code: 200 retries: 5 delay: 10 - name: Re-add to load balancer uri: url: "http://lb.example.com/api/enable/{{ inventory_hostname }}" method: POST delegate_to: localhost
Blue-Green Deploy
- name: Blue-Green deployment hosts: localhost vars: active_env: "{{ lookup('file', '/tmp/active_env') | default('blue') }}" new_env: "{{ 'green' if active_env == 'blue' else 'blue' }}" tasks: - name: Deploy to inactive environment include_role: name: deploy-app vars: target_hosts: "{{ new_env }}_servers" app_version: "{{ deploy_version }}" - name: Smoke test new environment uri: url: "http://{{ new_env }}.internal/health" status_code: 200 - name: Switch load balancer to new environment uri: url: http://lb.example.com/api/switch method: POST body_format: json body: active: "{{ new_env }}" - name: Record new active environment copy: content: "{{ new_env }}" dest: /tmp/active_env
Interview Quick-Fire
- AWX vs AAP main difference? AWX is open-source/community; AAP is enterprise with Red Hat support, Automation Hub, and EDA.
- How does AAP store credentials securely? Credentials are encrypted at rest and never exposed in job output — only injected into the execution environment at runtime.
- What is an Execution Environment? A container image bundling Ansible, collections, and Python dependencies — ensures consistent runtime across nodes.
- How do you trigger AAP from a CI pipeline? Via the REST API — POST to
/api/v2/job_templates/<id>/launch/with a bearer token. - Terraform vs Ansible — which for provisioning? Terraform for cloud infrastructure (stateful, plan/apply). Ansible for configuration management (agentless, push-based). Use both together.
- What is the Terraform provisioner risk? Provisioners are a last resort — they break idempotency. Prefer separate Ansible invocation after
terraform apply. - How do you pass Terraform outputs to Ansible? Via dynamic inventory script reading
terraform output -json, oradd_hostmodule aftercommunity.general.terraformtask. - What is
serialin a playbook? Controls rolling update batch size —serial: 1updates one host at a time;serial: "25%"does 25% at a time. - How do you prevent secrets appearing in CI logs? Use Ansible Vault, store passwords in CI secret variables, use
no_log: trueon sensitive tasks.
# no_log example- name: Set database password mysql_user: name: appuser password: "{{ db_password }}" no_log: true # Suppresses task output in logs