How to deploy letsencrypt-ready dockerized haproxy using Ansible

Ansible is configuration automation tool that does not require client application like Chef. You only need Python running on the server (which comes with most linux distribution) and everything is done through SSH. Here, I am going to show you how to write ansible yaml file to deploy dockerized HAProxy with letsencrypt support.

There are a number of ways to verify your domain with letsencrypt. Most common (recommended) way is to expose letsencrypt endpoint on your server. Another popular way is to use DNS. Unfortunately, not every domain name provider provides an API to configure your DNS.

In order to use SSL certificate with HAProxy, it needs to be put into HAProxy configuration. Unfortunately HAProxy will not be able to start up if the specific certificate file is missing. We can use Ansible to run certbot docker image to create an initial certificate if it doesn’t exist. And HAProxy image can be created and started afterwards. I am going to expect docker is already installed through another task/role.

Create certificate using Certbot Docker image

Certificate is created if the certificate does not exist. Certificate file is defined below:

- stat: path=/etc/certbot/certs/{{cert_domain}}.pem
  register: cert_file
  become: yes

We pass cleanup option to ensure the container is deleted after the command exits. During this step, we bind 443 to this command for certbot to be able to listen to tls challenge. If the certificate exists, this is task is skipped.

- name: create initial certificate
  docker_container:
    name: certbot
    image: certbot/certbot:latest
    state: started
    cleanup: yes
    command: certonly --standalone --preferred-challenges tls-sni -n --agree-tos -m {{ cert_email }} -d {{ cert_domain }}
    ports:
      - 443:443
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
  when: cert_file.stat.exists == False

Combine certificates for HAProxy

Certbot creates private and public certificates. These need to be combined for HAProxy to use.

#!/bin/sh

# move to the correct let's encrypt directory
cd /etc/letsencrypt/live/{{cert_domain}}

# cat files to make combined .pem for haproxy
cat fullchain.pem privkey.pem > /etc/certbot/certs/{{cert_domain}}.pem

This script is run through Ansible.

- name: run post-cert script
  command: /etc/certbot/combine_cert.sh
  become: yes

Automatic Renewal

Automatic renewal is done by creating permanent certbot image and register cronjob to execute cerbot renewal command. cerbot_nw is used to connect cerbot container to haproxy container.

- name: start renewal certbot image
  docker_container:
    name: certbot
    image: certbot/certbot:latest
    state: started
    recreate: yes
    interactive: yes
    tty: yes
    purge_networks: yes
    entrypoint: "/bin/sh"
    networks:
      - name: certbot_nw
    volumes:
      - /etc/certbot:/etc/certbot
      - /etc/letsencrypt:/etc/letsencrypt
- name: create renewal cronjob
  cron:
    name: certbot renewal
    special_time: hourly
    job: "docker exec -i -t certbot /usr/local/bin/certbot renew --http-01-port 54321 --renew-hook \"/etc/certbot/renew_cert.sh\" | awk '{ print strftime(\"%c: \"), $0; fflush(); }' >> /etc/certbot/le-renewal.log"

Starting HAProxy using Docker

HAProxy configuration template points to combined certificate generated by certbot and our script.

bind *:443 ssl crt /etc/certbot/certs/{{ cert_domain }}.pem

Start HAProxy image with certbot_nw network to ensure cerbot can use haproxy to validate domain name.

- name: start haproxy image
  docker_container:
    name: haproxy
    image: haproxy:alpine
    state: started
    recreate: yes
    purge_networks: yes
    networks:
      - name: certbot_nw
    ports:
      - 80:80
      - 443:443
    volumes:
      - /etc/haproxy:/usr/local/etc/haproxy
      - /etc/certbot/certs:/etc/certbot/certs
      - /dev/log:/dev/log

That’s it! We have fully functional dockerized certbot and haproxy with automatic renewal.

Tags:

ansible docker haproxy certbot