Intro

As I dive deeper into learning Puppet, I’m aiming to use it more in my homelab for managing configurations. By using a central mono-repository, I can keep things in one place and easily tweak stuff without the hassle of connecting to each machine separately. Lately I’ve been trying to puppetize the way my compose-stacks are configured. I wanted to set it up myself as a learning experience so this is probably not the most optimized way of doing it.

Puppet manifest

First off I created a puppet manifest which will make a docker-compose file according to a pre-made .epp template. This will create a general directory to place the stacks in. Every stack will get it’s own directory with a dedicated docker-compose.yaml file.

class profile_docker::compose_files (
  String                    $compose_version  = '3',
  String                    $directory        = '/docker_stacks',
  Hash[String, Array[Hash]] $stacks           = {},
) {
  file { $directory:
    ensure => directory,
    owner  => 'root',
    group  => 'root',
    mode   => '0755',
  }

  $stacks.each |$stack_name, $containers| {
    $stack_directory  = "${directory}/${stack_name}"

    file { $stack_directory:
      ensure => directory,
      owner  => 'rein',
      group  => 'rein',
      mode   => '0755',
    }

    file { "${stack_directory}/docker-compose.yaml":
      content => epp("${module_name}/docker-compose.yaml.epp", {
          compose_version => $compose_version,
          stack_name      => $stack_name,
          containers      => $containers,
      }),
      owner   => 'rein',
      group   => 'rein',
    }
  }
}

The .epp file

The docker-compose.yaml.epp file is created to be as modular as possible. Some of my containers use docker volumes while others use local binds. This was circomvented with some simple conditionals.

<% | $compose_version, $stack_name, $containers | -%>
---
# File managed by puppet.
version: "<%= $compose_version %>"
<% if $containers.any |$container| { $container['volume_names'] } { -%>
volumes:
<% } -%>
<% $containers.each |$container| { -%>
<% if $container['volume_names'] { -%>
<% $container['volume_names'].each |$volume| { -%>
  <%= $volume %>:
<% } -%>
<% } -%>
<% } -%>

services:
<% $containers.each |$container| { -%>
  <%= $container['name'] %>:
    image: <%= $container['image'] %>
    container_name: <%= $container['container_name'] %>
<% if $container['environment'] { -%>
    environment:
<% $container['environment'].each |$env| { -%>
      <%= $env %>
<% } -%>
<% } -%>
<% if $container['ports'] { -%>
    ports:
<% $container['ports'].each |$port| { -%>
      - <%= $port %>
<% } -%>
<% } -%>
<% if $container['volumes'] { -%>
    volumes:
<% $container['volumes'].each |$volume| { -%>
      - <%= $volume %>
<% } -%>
<% } -%>
<% if $container['devices'] { -%>
    devices:
<% $container['devices'].each |$device| { -%>
      - <%= $device %>
<% } -%>
<% } -%>
<% if $container['healthcheck'] { -%>
    healthcheck:
<% $container['healthcheck'].each |$check| { -%>
      <%= $check %>
<% } -%>
<% } -%>
<% if $container['command'] { -%>
    command: <%= $container['command'] %>
<% } -%>
<% if $container['stdin_open'] { -%>
    stdin_open: <%= $container['stdin_open'] %>
<% } -%>
<% if $container['tty'] { -%>
    tty: <%= $container['tty'] %>
<% } -%>
<% if $container['env_file'] { -%>
    env_file:
<% $container['env_file'].each |$env_file| { -%>
      - <%= $env_file %>
<% } -%>
<% } -%>
<% if $container['depends_on'] { -%>
    depends_on:
<% $container['depends_on'].each |$depends_on| { -%>
      - <%= $depends_on %>
<% } -%>
<% } -%>
<% if $container['cap_add'] { -%>
    cap_add:
<% $container['cap_add'].each |$cap_add| { -%>
      - <%= $cap_add %>
<% } -%>
<% } -%>
<% if $container['sysctls'] { -%>
    sysctls:
<% $container['sysctls'].each |$sysctls| { -%>
      - <%= $sysctls %>
<% } -%>
<% } -%>
    restart: <%= $container['restart'] %>
<% } -%>

Puppet hiera

The most important part is, of course, the parameters needed for the .epp file. These parameters are defined like this: Hash[String, Array[Hash]] $stacks = {}. When looping through this parameter one by one, it gathers all the options and puts them in the .epp file.

For example:

profile_docker::compose_files::stacks:
  container_maintenance:
    - name: cadvisor
      image: gcr.io/cadvisor/cadvisor-arm64:0.99-porterdavid
      container_name: cadvisorzzz
      ports:
        - 8055:8080
      volumes:
        - /:/rootfs:ro
        - /var/run:/var/run:rw
        - /sys:/sys:ro
        - /var/lib/docker/:/var/lib/docker:ro
      restart: unless-stopped
    - name: watchtower
      image: containrrr/watchtower
      container_name: watchtower
      ports:
        - 8080:8080
      volumes:
        - /var/run/docker.sock:/var/run/docker.sock
      restart: unless-stopped

This new way to create my stacks makes it easier for me to quickly adjust a stack and redeploy it. The redeploy step still needs to be implemented and tested but then I’m set!