Command Palette

Search for a command to run...

Hectal

Mission 0.4 · Stage 0 — Configuration Management with Ansible

Roles, Vault, and the Terraform Handoff

Goal: A reusable shoplite_web role, encrypted secrets in Git, a dynamic AWS inventory fed by Terraform tags, and tests for the role.

60 min Free locally; EC2 part ~$0.02/hr 9 steps 2 break-it drills

By the end of this mission

  • Refactor a playbook into a role with defaults, tasks, templates, and handlers
  • Use Ansible Galaxy collections and roles with a pinned requirements file
  • Encrypt secrets with Ansible Vault and keep them in Git safely
  • Build inventory from AWS tags instead of hardcoded IPs
  • Know where Ansible fits next to Terraform, Packer, Kubernetes, Puppet, Chef, and Salt

Part 1

Understand it first

Roles

A ROLE packages everything needed for one job in a fixed layout: tasks/, handlers/, templates/, files/, defaults/ (weakest variables, meant to be overridden), vars/ (strong, internal), and meta/ (dependencies). The playbook shrinks to 'these hosts get these roles'. Roles are Ansible's version of Terraform modules (Terraform course, Stage 5).

ANSIBLE GALAXY is the public registry for roles and COLLECTIONS (bundles of modules, plugins, and roles, like amazon.aws or community.postgresql). Pin versions in requirements.yml, exactly as you pin providers.

Ansible Vault

Database passwords can't sit in plain group_vars. ansible-vault encrypt turns a file into AES-256 ciphertext that's safe to commit; playbooks decrypt it at runtime with a password (from a prompt, a file, or a script that reads a secret manager). The common pattern is vault.yml holding vault_db_password, and a plain vars.yml saying db_password: "{{ vault_db_password }}", so you can still grep for where a variable is used.

For cloud fleets, reading from AWS Secrets Manager at runtime (amazon.aws.aws_secret lookup) avoids distributing a vault password at all. This is the same idea as the External Secrets Operator in Stage 2.

Terraform creates, Ansible configures

Terraform is good at creating resources and bad at configuring what's inside a server; Ansible is the reverse. The clean handoff is TAGS: Terraform tags instances (Role=web, Env=prod), and Ansible's aws_ec2 inventory plugin queries AWS and builds groups from those tags. No IP ever gets copied by hand.

Where Ansible fits today: use it for VMs, bastions, databases on EC2, network devices, and one-off operational runbooks. For Kubernetes workloads, don't configure containers with Ansible. Bake images with a Dockerfile (Docker course) and deploy them with GitOps (the rest of this course). For golden AMIs, Packer often runs Ansible once at build time, so servers boot already configured (immutable infrastructure).

Puppet, Chef, and Salt

You'll meet these in older estates. PUPPET: declarative DSL, agent pulls from a master every 30 minutes, strong continuous drift correction. CHEF: Ruby-based 'recipes', agent-based, very flexible, popular with developer-heavy teams. SALT (SaltStack): fast event-driven agents (minions) over a message bus, plus an agentless SSH mode. Ansible won most new work because it's agentless and YAML, but agent-based tools enforce state continuously, which Ansible only does if you schedule it (cron, AWX/AAP, or ansible-pull).

Terraform → Ansible handoffdiagram
Rendering diagram…

Part 2

Your project after this mission · 11 files change

shoplite-gitops/
  • ansible/
    • group_vars/
      • prod/
        • vars.ymlnew
        • vault.ymlnew
    • roles/
      • shoplite_web/
        • defaults/
          • main.ymlnew
        • handlers/
          • main.ymlnew
        • molecule/
          • default/
            • molecule.ymlnew
        • tasks/
          • main.ymlnew
        • templates/
          • app.env.j2new
          • shoplite.conf.j2new
    • inventory.aws_ec2.ymlnew
    • requirements.ymlnew
    • site.ymlmodified

Part 3

Build it, step by step

  1. 1

    Scaffold the role

    ansible-galaxy role init creates the standard layout. Move the template into roles/shoplite_web/templates/ and the handler into handlers/main.yml. Anything a user of the role might want to change goes in defaults/main.yml.

    terminal
    $ ansible-galaxy role init roles/shoplite_web
    git mv templates/shoplite.conf.j2 roles/shoplite_web/templates/
    ── expected output ──
    - Role roles/shoplite_web was created successfully
  2. 2

    Role defaults and tasks

    Defaults document every knob the role exposes. Move the Reload nginx handler into handlers/main.yml unchanged.

    ansible/roles/shoplite_web/defaults/main.ymlwhole fileyaml
    listen_port: 80
    client_max_body_size: 10m
    upstreams: [127.0.0.1:8080]
    admin_users: []
  3. 3

    Role tasks

    The tasks are the ones from site.yml, plus a new app.env.j2 template (one line: DB_PASSWORD={{ db_password | default('') }}) for the app's database settings. no_log: true keeps its contents out of the output.

    ansible/roles/shoplite_web/tasks/main.ymlwhole fileyaml
    - name: Install nginx
      ansible.builtin.apt: { name: nginx, state: present, update_cache: true, cache_valid_time: 3600 }
    
    - name: Render the ShopLite site
      ansible.builtin.template:
        src: shoplite.conf.j2
        dest: /etc/nginx/sites-available/default
        mode: "0644"
      notify: Reload nginx
    
    - name: Create admin users
      ansible.builtin.user: { name: "{{ item }}", groups: sudo, append: true, shell: /bin/bash }
      loop: "{{ admin_users }}"
    
    - name: Create the app config directory
      ansible.builtin.file: { path: /etc/shoplite, state: directory, mode: "0750" }
    
    - name: Write the app's DB config
      ansible.builtin.template:
        src: app.env.j2
        dest: /etc/shoplite/app.env
        mode: "0600"
      when: db_password is defined
      no_log: true
    
    - name: Ensure nginx is running
      ansible.builtin.service: { name: nginx, state: started, enabled: true }
  4. 4

    Shrink the playbook

    The playbook now reads like a table of contents. Adding a db group later means one more play with a shoplite_db role.

    ansible/site.ymlwhole fileyaml
    - name: Configure ShopLite web servers
      hosts: web
      become: true
      roles:
        - shoplite_web
  5. 5

    Pin external collections

    Install into the project so every teammate and CI runner uses the same versions.

    ansible/requirements.ymlwhole fileyaml
    collections:
      - name: amazon.aws
        version: "10.1.0"
      - name: community.docker
        version: "4.7.0"
  6. 6

    Encrypt the prod DB password

    Split group_vars/prod.yml into a directory: vars.yml (plain) and vault.yml (encrypted). The vault password file lives OUTSIDE the repo and is listed in .gitignore; in CI it comes from a secret.

    terminal
    $ mkdir -p group_vars/prod && git mv group_vars/prod.yml group_vars/prod/vars.yml
    echo 'vault_db_password: "s3cr3t-prod-pw"' > group_vars/prod/vault.yml
    ansible-vault encrypt group_vars/prod/vault.yml --vault-password-file ~/.shoplite-vault-pass
    head -2 group_vars/prod/vault.yml
    ── expected output ──
    Encryption successful
    $ANSIBLE_VAULT;1.1;AES256
    6231356462663934353738653662333762316335386430623961326134393432...
  7. 7

    Reference the vaulted value from plain vars

    Add this line to group_vars/prod/vars.yml. The vault_ prefix makes it obvious where the real value lives. Run with --vault-password-file (or set vault_password_file in ansible.cfg).

    ansible/group_vars/prod/vars.ymladd to fileyaml
    db_password: "{{ vault_db_password }}"
    terminal
    $ ansible-playbook site.yml --vault-password-file ~/.shoplite-vault-pass
    ansible-vault view group_vars/prod/vault.yml --vault-password-file ~/.shoplite-vault-pass
    ── expected output ──
    TASK [shoplite_web : Write the app's DB config] ***
    skipping: [web1]
    changed: [web2]
    ...
    vault_db_password: "s3cr3t-prod-pw"
  8. 8

    Dynamic inventory from Terraform's tags

    In the Terraform course your instances got Role and Env tags. This inventory file asks EC2 for running instances with those tags and turns them into groups (role_web, env_prod). Connection uses SSM Session Manager or SSH to private IPs through a bastion. No public IPs needed (AWS course, Topic 2.2).

    ansible/inventory.aws_ec2.ymlwhole fileyaml
    plugin: amazon.aws.aws_ec2
    regions: [ap-south-1]
    filters:
      instance-state-name: running
      tag:Project: shoplite
    keyed_groups:
      - key: tags.Role
        prefix: role
      - key: tags.Env
        prefix: env
    hostnames: [tag:Name, private-ip-address]
    compose:
      ansible_host: private_ip_address
    terminal
    $ ansible-galaxy collection install -r requirements.yml
    ansible-inventory -i inventory.aws_ec2.yml --graph
    ── expected output ──
    @all:
    |--@aws_ec2:
    | |--shoplite-prod-web-0
    | |--shoplite-prod-web-1
    |--@role_web:
    | |--shoplite-prod-web-0
    | |--shoplite-prod-web-1
    |--@env_prod:
    | |--shoplite-prod-web-0
    | |--shoplite-prod-web-1
  9. 9

    Test the role with Molecule

    Molecule spins up a throwaway container, applies the role, runs it again to prove idempotency (the run fails if anything reports changed), runs verification, and destroys everything. Put molecule test in CI and roles stay trustworthy, just like terraform test for modules.

    ansible/roles/shoplite_web/molecule/default/molecule.ymlwhole fileyaml
    driver:
      name: docker
    platforms:
      - name: instance
        image: geerlingguy/docker-ubuntu2404-ansible
        pre_build_image: true
    provisioner:
      name: ansible
    verifier:
      name: ansible
    terminal
    $ cd roles/shoplite_web && molecule test
    ── expected output ──
    INFO default ➜ converge: Executed: Successful
    INFO default ➜ idempotence: Idempotence completed successfully.
    INFO default ➜ verify: Executed: Successful
    INFO default ➜ destroy: Executed: Successful

Checkpoint — you should now have

  • ✓site.yml is five lines and still configures both environments.
  • ✓group_vars/prod/vault.yml is ciphertext in Git, and the vault password file is not in the repo.
  • ✓ansible-inventory -i inventory.aws_ec2.yml --graph lists instances by tag (if you ran the EC2 part).
  • ✓molecule test passes, including the idempotence step.

Part 4

Break it on purpose

Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.

Break #1

Leak a secret in the logs

Remove no_log: true from the DB config task and run with -v.

terminal
$ ansible-playbook site.yml -v --vault-password-file ~/.shoplite-vault-pass
── what you'll see ──
changed: [web2] => {"changed": true, "dest": "/etc/shoplite/app.env", ...
"invocation": {"module_args": {... "content": "DB_PASSWORD=s3cr3t-prod-pw" ...}}}

Break #2

Run without the vault password

Run ansible-playbook site.yml without --vault-password-file.

terminal
$ ansible-playbook site.yml
── what you'll see ──
ERROR! Attempting to decrypt but no vault secrets found

Part 5

Interview questions from this mission

01

Ansible vs Terraform: when do you use which?

02

Push vs pull configuration management?

03

How do you manage secrets in Ansible?

Before you stop

Clean up

terminal
$ docker rm -f web1 web2
# if you launched EC2 for the dynamic inventory:
cd ../terraform/envs/dev && terraform destroy
0/4 · 0%