Cloud AI Campus
  • Career paths
  • Learning paths
  • Hands-on Labs
Log in Sign up

๐Ÿงช Hands-on lab · 60 min

Ansible โ€” Cloud Services & Containers

  1. 1. Pulling images
  2. 2. Running a container
  3. 3. docker-compose from Ansible
  4. 4. Talking to a REST API

Pulling images

The sandbox's control container has the Docker daemon you've been using throughout the labs โ€” docker ps already shows the six worker containers. Now drive it via Ansible.

community.docker.docker_image is the declarative wrapper. Set its state / source and re-run it as many times as you like โ€” only the first run actually does work.

Write pull.yml:

---
- name: Make sure the alpine image is present locally
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Pull alpine:3.20
      community.docker.docker_image:
        name: alpine
        tag: '3.20'
        source: pull
        state: present
      register: alpine_pull

    - name: Report
      debug:
        msg: "alpine:3.20 changed={{ alpine_pull.changed }}"
...

The first run pulls (โ‰ˆ 3MB compressed), the second is a no-op:

ansible-playbook pull.yml
ansible-playbook pull.yml

You should see changed=true the first time and changed=false the second โ€” that's idempotence.

Click Verify step.

Hint

Use `community.docker.docker_image` with `source: pull` against `localhost`.

Running a container

docker_container accepts most docker run flags. The key field is state โ€” started, stopped, present, absent.

Write run.yml:

---
- name: Run an ephemeral utility container
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Start a long-lived alpine
      community.docker.docker_container:
        name: cac-sleeper
        image: alpine:3.20
        state: started
        restart_policy: 'no'
        command: ["sleep", "300"]

    - name: Inspect it
      community.docker.docker_container_info:
        name: cac-sleeper
      register: info

    - name: Show the IP
      debug:
        msg: "cac-sleeper running, ID={{ info.container.Id[:12] }}"
...

Apply:

ansible-playbook run.yml
docker ps --filter name=cac-sleeper

You should see the container listed under docker ps.

To stop it via Ansible:

ansible localhost -c local -m community.docker.docker_container \
  -a 'name=cac-sleeper state=absent'

That's the kind of one-liner the community.docker collection gives you for free. Click Verify step.

Hint

`docker_container` is declarative โ€” set `state: started`, pass `image`, `ports`, `env`.

docker-compose from Ansible

community.docker.docker_compose_v2 applies a compose file the same way docker compose up -d would, but with idempotence and the rest of the playbook's variables in scope.

Create compose/docker-compose.yml:

services:
  cache:
    image: alpine:3.20
    command: ["sh", "-c", "sleep 600"]
    container_name: cac-compose-cache

  worker:
    image: alpine:3.20
    command: ["sh", "-c", "sleep 600"]
    container_name: cac-compose-worker
    depends_on:
      - cache

Then compose.yml:

---
- name: Stand up the compose stack
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Apply docker-compose
      community.docker.docker_compose_v2:
        project_src: ./compose
        state: present
      register: result

    - name: List started containers
      debug:
        msg: "{{ result.containers | map(attribute='Name') | list }}"
...
mkdir -p compose
# (paste docker-compose.yml content above into compose/docker-compose.yml)
ansible-playbook compose.yml
docker ps --filter name=cac-compose

To tear it back down:

ansible localhost -c local -m community.docker.docker_compose_v2 \
  -a 'project_src=./compose state=absent'

Click Verify step while both cac-compose-cache and cac-compose-worker are running.

Hint

`community.docker.docker_compose_v2` consumes a compose YAML on the controller.

Talking to a REST API

Cloud-provider modules (amazon.aws.ec2, google.cloud.gcp_compute_instance, azure.azcollection.azure_rm_*) all wrap HTTP REST APIs underneath. The uri module is the same machinery, exposed directly.

Write api.yml โ€” talk to the public httpbin echo service:

---
- name: REST as a first-class module
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: GET an echo endpoint
      uri:
        url: https://httpbin.org/json
        method: GET
        return_content: true
        body_format: json
      register: echo

    - name: Show one field from the response
      debug:
        msg: "first slide title: {{ echo.json.slideshow.title }}"

    - name: POST a payload
      uri:
        url: https://httpbin.org/post
        method: POST
        body_format: json
        body:
          deployed_by: ansible
          host: "{{ inventory_hostname }}"
        return_content: true
      register: posted

    - name: Confirm the server echoed our body
      debug:
        msg: "server saw deployed_by={{ posted.json.json.deployed_by }}"
...
ansible-playbook api.yml

That's the same pattern every cloud module uses internally:

  • Build a request body from variables.
  • POST/PUT/GET against a cloud API.
  • Register the response into a variable.
  • Branch on it (when:).
  • Re-run the task in check mode to preview without applying.

To plug in real AWS / GCP / Azure modules, install the matching collection (ansible-galaxy collection install amazon.aws) and set credentials via environment variables or a Vault file.

Click Verify step.

Hint

Most cloud modules wrap REST. `uri:` shows the underlying mechanic โ€” use `return_content: yes` + `body_format: json`.

© 2026 Cloud AI Campus