100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Infrastructure as Code — Terraform & Ansible
25 minintermediate

Combining Terraform and Ansible — Provision-Then-Configure Pattern

Terraform and Ansible are complementary tools that address different phases of infrastructure automation. Terraform's declarative model excels at provisioning and managing cloud infrastructure resources — VPCs, EC2 instances, RDS databases, S3 buckets, IAM roles, security groups — resources that are created and deleted through cloud provider APIs. Ansible's procedural model excels at configuring the operating system and software running on those resources — installing packages, writing configuration files, managing services, creating users, and deploying application code. Attempting to do everything in one tool produces worse results than using each tool for its strength: Terraform-based configuration management is possible (using user_data scripts, remote-exec provisioners) but brittle and difficult to iterate on; Ansible-based infrastructure provisioning is possible (using the AWS collection) but lacks Terraform's state management and drift detection.

The provision-then-configure pattern has three integration points that must be carefully designed. First, the handoff mechanism: how does Ansible know which hosts Terraform created? The cleanest answer is tags — Terraform tags EC2 instances with Project, Environment, and Role tags; the Ansible dynamic EC2 inventory plugin discovers those instances by tag. This decoupling means Terraform does not need to call Ansible, and Ansible does not need Terraform — they share only the tagging convention. Second, the ordering and dependencies: Terraform must complete provisioning before Ansible runs; EC2 instances must be fully initialised and SSM-registered before Ansible can connect. CI pipelines enforce this ordering with explicit 'terraform apply' → 'wait for SSM' → 'ansible-playbook' steps. Third, ongoing lifecycle management: after initial provisioning and configuration, how are application updates, OS patches, and configuration changes applied? Ansible handles day-two operations (deploying new app versions, applying security patches, rotating credentials) while Terraform handles infrastructure changes (scaling, resizing, networking changes). Keeping each tool responsible for its domain prevents the operational confusion of having two tools fight over the same resource.

Analogy🏏Cricket
🏏 Think of it like cricket: Terraform Cloud is the ICC's centralised match management platform — instead of each national board (team) maintaining its own scoring system, umpire assignment software and results database (self-managed CI/CD + S3 backend), the ICC platform handles all of this centrally. When a board member proposes a rule change (pull request), the platform automatically simulates the match under the new rules (speculative plan on PR), shows the referees the impact (plan output in PR comment), and requires the match committee to approve (policy gates) before the rule takes effect. The audit log records every change, every approval, and who made each decision — providing the governance and traceability that serious tournament operations require.
yaml
# complete CI/CD pipeline — provision (Terraform) then configure (Ansible)
# .github/workflows/full-deploy.yml

name: Full Infrastructure Deployment

on:
  push:
    branches: [main]

jobs:
  # ── Phase 1: Provision Infrastructure ─────────────────────────────────────
  terraform:
    name: Provision with Terraform
    runs-on: ubuntu-latest
    outputs:
      instance_ids: ${{ steps.outputs.outputs.instance_ids }}
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/cricket-terraform-role
          aws-region: ap-south-1

      - uses: hashicorp/setup-terraform@v3
        with: { terraform_version: '1.6.4' }

      - name: Terraform Init and Apply
        working-directory: environments/production
        run: |
          terraform init -input=false
          terraform apply -input=false -auto-approve -no-color

      - name: Capture Terraform Outputs
        id: outputs
        working-directory: environments/production
        run: |
          INSTANCE_IDS=$(terraform output -json instance_ids | jq -r '.[]' | tr '\n' ',')
          echo "instance_ids=${INSTANCE_IDS}" >> $GITHUB_OUTPUT

  # ── Wait for SSM registration ──────────────────────────────────────────────
  wait_for_ssm:
    name: Wait for SSM Agent Registration
    runs-on: ubuntu-latest
    needs: terraform
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/cricket-deploy-role
          aws-region: ap-south-1

      - name: Wait for all instances to register with SSM
        run: |
          INSTANCE_IDS=($(echo '${{ needs.terraform.outputs.instance_ids }}' | tr ',' ' '))
          echo "Waiting for ${#INSTANCE_IDS[@]} instances to register with SSM..."
          for INSTANCE_ID in "${INSTANCE_IDS[@]}"; do
            echo "Checking ${INSTANCE_ID}..."
            timeout 300 bash -c "
              until aws ssm describe-instance-information \
                --filters Key=InstanceIds,Values=${INSTANCE_ID} \
                --query 'InstanceInformationList[0].PingStatus' \
                --output text 2>/dev/null | grep -q Online; do
                echo 'Waiting for SSM...'
                sleep 10
              done
            "
            echo "${INSTANCE_ID} is online"
          done

  # ── Phase 2: Configure with Ansible ───────────────────────────────────────
  ansible:
    name: Configure with Ansible
    runs-on: ubuntu-latest
    needs: [terraform, wait_for_ssm]
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/cricket-deploy-role
          aws-region: ap-south-1

      - name: Install Ansible and dependencies
        run: |
          pip install ansible boto3 botocore
          ansible-galaxy collection install amazon.aws community.aws
          ansible-galaxy install -r requirements.yml --roles-path vendor/roles

      - name: Write vault password
        run: |
          echo '${{ secrets.ANSIBLE_VAULT_PASSWORD }}' > /tmp/vault-pass
          chmod 600 /tmp/vault-pass

      - name: Run Ansible playbook
        run: |
          ansible-playbook \
            -i inventory/aws_ec2.yml \
            --vault-password-file /tmp/vault-pass \
            -e app_version=${{ github.sha }} \
            -e environment=production \
            playbooks/deploy_cricket_api.yml

      - name: Clean up
        if: always()
        run: rm -f /tmp/vault-pass
hcl
# Tag conventions for Terraform → Ansible handoff
# In Terraform configuration:
resource "aws_instance" "api" {
  count = var.instance_count
  tags = {
    Name        = "cricket-api-${count.index + 1}"
    Project     = "cricket-analytics"   # Ansible ec2 plugin: 'tag:Project': cricket-analytics
    Environment = var.environment       # Ansible group: tag_Environment_production
    Role        = "api-server"          # Ansible group: tag_Role_api_server
    AnsibleManaged = "true"            # Easy filter for all Ansible-managed instances
  }
}

# In Ansible aws_ec2.yml:
# filters:
#   'tag:AnsibleManaged': 'true'
#   'tag:Project': cricket-analytics
# groups:
#   api_servers: "tags.Role == 'api-server'"

# ansible/ansible.cfg — project configuration
[defaults]
host_key_checking    = False
roles_path           = vendor/roles
stdout_callback      = yaml
collections_path     = ~/.ansible/collections

[inventory]
enable_plugins = amazon.aws.ec2, yaml, ini

[privilege_escalation]
become      = True
become_method = sudo
Analogy🏏Cricket
🏏 Think of it like cricket: The two-job pipeline structure — provision must complete before configure begins — is the immutable sequencing of preparing a venue and then rehearsing the team on it: you cannot run fielding drills on a ground that has not been built, and no amount of coaching enthusiasm changes the order. The CI pipeline encodes this the way a tournament schedule does — as an explicit dependency between stages ('needs: provision' in the workflow), not as a polite convention: the configure job physically cannot start until the provision job reports success, so the failure modes partition cleanly. If the provision stage fails, no configuration was ever attempted (the venue build stalled; the squad never travelled — nothing half-coached exists), and if the configure stage fails, the infrastructure is up but unconfigured (the ground stands complete but the team is not match-ready) — two distinct, diagnosable states instead of one entangled mess. The tags applied during provisioning are the venue's signage going up as part of construction, and they are what the second stage navigates by (the dynamic inventory discovers hosts by the tags Terraform wrote) — which is why the pipeline treats tagging not as decoration but as the load-bearing interface between its two halves: mis-tag the venue and the coaching staff drive to the wrong ground.
Lesson 26 of 33
0% complete