For a while now, I’ve used bash scripts called by systemd unit files to control containers I want running at all times. And while it works well, I decided to look into Ansible for a more repeatable and organized approach that would also hopefully have the benefit of easy container host migrations should the need arise. Having spent a couple days re-tooling, I thought I’d share a quick example using Hauk - a super cool self-hosted location sharing app.

Pre-Ansible Setup

Using Hauk as an example..

1. Create bash scripts for starting and stopping the container

START:

#!/bin/bash
#
/usr/bin/podman run -d -ti --name hauk --hostname hauk.cthudson.com \
	-v /containers/hauk:/etc/hauk:z \
	-l traefik.enable="true" \
 	-l traefik.http.routers.hauk.rule=Host\(\`hauk.cthudson.com\`\) \
 	-l traefik.http.middlewares.hauk-https-redirect.redirectscheme.scheme="https" \
 	-l traefik.http.routers.hauk.middlewares="hauk-https-redirect" \
 	-l traefik.http.routers.hauk-secure.entrypoints="websecure" \
 	-l traefik.http.routers.hauk-secure.rule=Host\(\`hauk.cthudson.com\`\) \
	-l traefik.http.routers.hauk-secure.tls="true" \
	-l traefik.http.routers.hauk-secure.tls.certresolver=le \
 	-l traefik.http.services.hauk.loadbalancer.server.port="80" \
	-l traefik.http.routers.hauk-admin.rule='(Host(`hauk.cthudson.com`) && PathPrefix(`/index.html`))' \
	-l traefik.http.routers.hauk-admin.entrypoints=websecure \
	-l traefik.http.middlewares.hauk-admin-ipwhitelist.ipwhitelist.sourcerange='127.0.0.1/32, 192.168.5.0/24, 10.8.0.0/24' \
	-l traefik.http.routers.hauk-admin.middlewares=hauk-admin-ipwhitelist \
	docker.io/bilde2910/hauk

STOP:

#!/bin/bash

/usr/bin/podman stop hauk; /usr/bin/podman rm hauk

2. Create a systemd unit file to control and automatically run the container

[Unit]
Description=Hauk Podman Container
After=network.target containers.mount
ExecStartPre=-/bin/bash /usr/local/sbin/stop_hauk.sh

[Service]
Type=simple
TimeoutStartSec=5m

ExecStart=/bin/bash /usr/local/sbin/start_hauk.sh
RemainAfterExit=true
ExecStop=-/bin/bash /usr/local/sbin/stop_hauk.sh

ExecReload=-/bin/bash /usr/local/sbin/stop_hauk.sh
ExecReload=-/bin/bash /usr/local/sbin/start_hauk.sh

Restart=always
RestartSec=60

[Install]
WantedBy=multi-user.target

While this is simple enough, it’s a bit painful if the host dies and I need to head over to my backups and place everything back in the right place on a new host, etc, etc. So now let’s see what Ansible brings to the table.

With Ansible Setup

Continuing with the Hauk example..

1. Create Ansible playbook (hauk.yml in my case)

Change the hosts value to your host or a variable that you can specify during playbook execution.

---
- hosts: localhost

  tasks:
    
  - name: Create hauk container
    containers.podman.podman_container:
      name: hauk
      hostname: hauk.cthudson.com
      image: docker.io/bilde2910/hauk
      rm: true
      state: started
      volume:
        - "/containers/hauk:/etc/hauk:z"
      label: 
        {
        'traefik.enable': 'true',
        'traefik.http.routers.hauk.rule': 'Host(`hauk.cthudson.com`)',
        'traefik.http.middlewares.hauk-https-redirect.redirectscheme.scheme': 'https',
        'traefik.http.routers.hauk.middlewares': 'hauk-https-redirect',
        'traefik.http.routers.hauk-secure.entrypoints': 'websecure',
        'traefik.http.routers.hauk-secure.rule': 'Host(`hauk.cthudson.com`)',
        'traefik.http.routers.hauk-secure.tls': 'true',
        'traefik.http.routers.hauk-secure.tls.certresolver': 'le',
        'traefik.http.services.hauk.loadbalancer.server.port': '80',
        'traefik.http.routers.hauk-admin.rule': '(Host(`hauk.cthudson.com`) && PathPrefix(`/index.html`))',
        'traefik.http.routers.hauk-admin.entrypoints': 'websecure',
        'traefik.http.middlewares.hauk-admin-ipwhitelist.ipwhitelist.sourcerange': '127.0.0.1/32, 192.168.5.0/24, 10.8.0.0/24',
        'traefik.http.routers.hauk-admin.middlewares': 'hauk-admin-ipwhitelist'
        }

  - name: Create systemd unit file for Hauk container
    containers.podman.podman_generate_systemd:
      name: hauk
      new: true
      requires: containers.mount
      restart_policy: always
      no_header: true
      dest: /etc/systemd/system

  - name: Ensure hauk container is started and enabled
    ansible.builtin.systemd:
      name: container-hauk
      daemon_reload: true
      state: started
      enabled: true

And that’s really it. Put these playbooks in source control and you can deploy at will just about anywhere. As for what’s happening here, the name of each task gives a good hint:

1. Create hauk container

This task is using the podman_container module from the containers.podman collection to create the container itself. You’ll notice the similarities to the bash script I noted earlier, which made the shift quite painless. There aren’t many callouts aside from maybe familiarizing yourself with lists and dictionaries and looking at the docs to understand what each parameter expects. You can see here that labels expect dictionaries (as do container environmnet variables) and volumes expect lists.

2. Create systemd unit file for Hauk container

This task is using the podman_generate_systemd module from the same containers.podman collection and generates a systemd unit file within /etc/systemd/system. In this case, it would be named container-hauk.service (NOTE: If container_prefix is not specified, it prepends container- to your specified unit filename - similarly, if creating a pod it would prepend pod-). As for the unit file, here it is:

# container-hauk.service

[Unit]
Description=Podman container-hauk.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

# User-defined dependencies
Requires=containers.mount

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
ExecStart=/usr/bin/podman container run \
	--cidfile=%t/%n.ctr-id \
	--cgroups=no-conmon \
	--rm \
	--sdnotify=conmon \
	--replace \
	--name hauk \
	--hostname hauk.cthudson.com \
	--volume /containers/hauk:/etc/hauk:z \
	--label traefik.enable=true \
	--label traefik.http.routers.hauk.rule=Host(`hauk.cthudson.com`) \
	--label traefik.http.middlewares.hauk-https-redirect.redirectscheme.scheme=https \
	--label traefik.http.routers.hauk.middlewares=hauk-https-redirect \
	--label traefik.http.routers.hauk-secure.entrypoints=websecure \
	--label traefik.http.routers.hauk-secure.rule=Host(`hauk.cthudson.com`) \
	--label traefik.http.routers.hauk-secure.tls=true \
	--label traefik.http.routers.hauk-secure.tls.certresolver=le \
	--label traefik.http.services.hauk.loadbalancer.server.port=80 \
	--label "traefik.http.routers.hauk-admin.rule=(Host(`hauk.cthudson.com`) && PathPrefix(`/index.html`))" \
	--label traefik.http.routers.hauk-admin.entrypoints=websecure \
	--label "traefik.http.middlewares.hauk-admin-ipwhitelist.ipwhitelist.sourcerange=127.0.0.1/32, 192.168.5.0/24, 10.8.0.0/24" \
	--label traefik.http.routers.hauk-admin.middlewares=hauk-admin-ipwhitelist \
	--detach=True docker.io/bilde2910/hauk
ExecStop=/usr/bin/podman stop \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
	-f \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

Pretty slick, eh?

3. Ensure hauk container is started and enabled

This final task is calling on Ansible’s built-in systemd module to start and enable (ie: start on boot) the new container-hauk service.

Running the playbook

Once you have ansible installed on whatever you are running (in my case, this is Fedora and I ran dnf install ansible), you just need to make sure you also install the podman collection via ansible-galaxy. At the time of this writing, you can do so by running: ansible-galaxy collection install containers.podman.

After that, you can fire off the playbook via:

# ansible-playbook hauk.yml

And you should see something similar to:

# ansible-playbook hauk.yml 
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] ****************************************************************************************************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Create hauk container] ****************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Create systemd unit file for Hauk container] ******************************************************************************************************************************************************************
ok: [localhost]

TASK [Ensure hauk container is started and enabled] *****************************************************************************************************************************************************************
ok: [localhost]

PLAY RECAP **********************************************************************************************************************************************************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

With my container already running, everything was OK and nothing changed. But it is that simple!

Moving Forward

As I get everything migrated over to these playbooks, I will likely start refining things by bringing in an inventory and variabalizing anything that makes sense. But this was a cool 24hr exploration into how Ansible can make my container deployments a little more streamlined and consistent. I hope others find it interesting as well. Cheers!