Deploying vault via docker and ansible

Adding to my in-promptu series on automating docker containers with ansible, this time I'm looking at Hashicorp's Vault. This is slightly different, in that it required a binary to be installed on the ansible controller (a raspberry pi). Once vault is deployed, we need to unseal the containter.

As usual, we start by setting up the variables. If this is the first time you have set up vault, the unseal key and root token will not be generated yet. They can be found in the container's logs, once it has been initially deployed.

---
  - name: "Vault on Docker Playbook"
    hosts: docker.host
    become: yes
    become_method: sudo

    vars:
    - container: vault
    - domain: 'docker.host'
    - ports: "8200"
    - unseal_key: UNSEALKEY
    - root_token: ROOTTOKEN
    - vault_token: ROOTTOKEN
    - vault_addr: "http://{{ container }}.{{ domain }}:{{ ports }}"
    - openldap_url: "freeipa.host:389"

    tasks:

The next stage is to set up the volumes we require. For this, we need to set up three volumes - config, logs and files. This is similar to creating volumes that we have done before, except this time we are using an items loop to create all three.

    - name: Create the homepage configuration volume
      docker_volume:
        name: "{{ container }}-{{ item }}"
      with_items:
      - config
      - logs
      - file
      tags: volumecreate

Again, as we have done before, we set up the labels to use traefik.

    - name: Create Traefik labels's dictionary
      set_fact:
        my_labels: "{{ my_labels | default({}) | combine ({ item.key : item.value }) }}"
      with_items:
      - { 'key': 'traefik.enable' , 'value': 'true'}
      - { 'key': 'traefik.docker.network', 'value': "traefik-public"}
      - { 'key': "traefik.http.middlewares.{{ container }}-https-redirect.redirectscheme.scheme", 'value': "https"}
      - { 'key': "traefik.http.routers.{{ container }}-secure.entrypoints",'value': "https"}
      - { 'key': "traefik.http.routers.{{ container }}-secure.rule",'value': "Host(`{{ container }}.{{ domain }}`)"}
      - { 'key': "traefik.http.routers.{{ container }}-secure.service",'value': "{{ container }}"}
      - { 'key': "traefik.http.routers.{{ container }}-secure.tls",'value': "true"}
      - { 'key': "traefik.http.routers.{{ container }}.entrypoints",'value': "http"}
      - { 'key': "traefik.http.routers.{{ container }}.middlewares",'value': "{{ container }}-https-redirect"}
      - { 'key': "traefik.http.routers.{{ container }}.rule",'value': "Host(`{{ container }}.{{ domain }}`)"}
#      - { 'key': "traefik.http.services.{{ container }}.loadbalancer.server.port", 'value': "8200"}
      - { 'key': "traefik.http.services.{{ container }}.loadbalancer.server.port", 'value': "{{ ports }}"}

Now the labels are set up, next we deploy the container.

At this stage, the container can be deployed, get the root token and unseal token, to populate the playbook. The next stages check require the vault binary. I followed this guide: https://austineosuide.medium.com/installing-hashicorp-vault-on-a-raspberry-pi-4-1b1716f3cf51 These are the steps that were followed:

# Download Hashicorp Vault
wget https://releases.hashicorp.com/vault/1.6.2/vault_1.6.2_linux_arm.zip

# Install unzip & Unzip
unzip vault_1.6.2_linux_arm.zip

# Move vault to /usr/bin
sudo mv vault /usr/bin

# Confirm all good
vault -v

Now that the binary is installed, the next stage is to check if vault is actually running. This is done slightly differently to normal as it is a local_action. This means that it runs on the controller, rather than the controlled device.

    - name: Check vault is running
      local_action:
        module: ansible.builtin.uri
        url: "{{ vault_addr }}"        # Check Vault URL
        return_content: yes                                     # Return Http Response content
        validate_certs: no                                      # Not recommended, but a simple hack to avoid SSL checks
        status_code:                                            # List of HTTP status code considered to be as a successful request
          - 200

Once we know vault is actually running, we need to check if it is actually sealed. This is done by running vault status, again on the controller. This is done in a slightly different way, since I was trying out the ways this could work. This also marks the task as changed when the return code (rc) is set to 2 (as per the docs for the vault binary).

    - name: check if vault is sealed
      shell: |
          vault status
      register: vault_status
      environment:
          VAULT_ADDR: "{{ vault_addr }}"
      ignore_errors: True
      changed_when:
      - vault_status.rc == 2
      delegate_to: 127.0.0.1

Lastly, we unseal the vault, if it is actually sealed.

    - name: Unseal vault with unseal keys
      shell: |
          vault operator unseal {{ unseal_key }}
      environment:
          VAULT_ADDR: "{{ vault_addr }}"
      when: vault_status.rc == 2
      delegate_to: 127.0.0.1

For completeness, the full code is here:

---
  - name: "Vault on Docker Playbook"
    hosts: docker.host
    become: yes
    become_method: sudo

    vars:
    - container: vault
    - domain: 'docker.host'
    - ports: "8200"
    - unseal_key: UNSEALKEY
    - root_token: ROOTTOKEN
    - vault_token: ROOTTOKEN
    - vault_addr: "http://{{ container }}.{{ domain }}:{{ ports }}"
    - openldap_url: "freeipa.host:389"

    tasks:

    - name: Create the homepage configuration volume
      docker_volume:
        name: "{{ container }}-{{ item }}"
      with_items:
      - config
      - logs
      - file
      tags: volumecreate

    - name: Create Traefik labels's dictionary
      set_fact:
        my_labels: "{{ my_labels | default({}) | combine ({ item.key : item.value }) }}"
      with_items:
      - { 'key': 'traefik.enable' , 'value': 'true'}
      - { 'key': 'traefik.docker.network', 'value': "traefik-public"}
      - { 'key': "traefik.http.middlewares.{{ container }}-https-redirect.redirectscheme.scheme", 'value': "https"}
      - { 'key': "traefik.http.routers.{{ container }}-secure.entrypoints",'value': "https"}
      - { 'key': "traefik.http.routers.{{ container }}-secure.rule",'value': "Host(`{{ container }}.{{ domain }}`)"}
      - { 'key': "traefik.http.routers.{{ container }}-secure.service",'value': "{{ container }}"}
      - { 'key': "traefik.http.routers.{{ container }}-secure.tls",'value': "true"}
      - { 'key': "traefik.http.routers.{{ container }}.entrypoints",'value': "http"}
      - { 'key': "traefik.http.routers.{{ container }}.middlewares",'value': "{{ container }}-https-redirect"}
      - { 'key': "traefik.http.routers.{{ container }}.rule",'value': "Host(`{{ container }}.{{ domain }}`)"}
#      - { 'key': "traefik.http.services.{{ container }}.loadbalancer.server.port", 'value': "8200"}
      - { 'key': "traefik.http.services.{{ container }}.loadbalancer.server.port", 'value': "{{ ports }}"}

    - name: Start Vault and apply labels
      docker_container:
        name: "{{ container }}"
        state: started
#        networks:
#        - name: traefik-public
        image: hashicorp/vault:latest
        env:
          PUID: "1000"
          PGID: "1000"
          TZ: "Etc/UTC"
          VERSION: "docker"
        ports:
        - "{{ ports }}:{{ ports }}"
        volumes:
        - "{{ container }}-config:/vault/config.d"
        - "{{ container }}-file:/vault/file"
        - "{{ container }}-logs:/vault/logs"
        labels: "{{ my_labels }}"
      tags: deploycontainer

    - name: Check vault is running
      local_action:
        module: ansible.builtin.uri
        url: "{{ vault_addr }}"        # Check Vault URL
        return_content: yes                                     # Return Http Response content
        validate_certs: no                                      # Not recommended, but a simple hack to avoid SSL checks
        status_code:                                            # List of HTTP status code considered to be as a successful request
          - 200

    - name: check if vault is sealed
      shell: |
          vault status
      register: vault_status
      environment:
          VAULT_ADDR: "{{ vault_addr }}"
      ignore_errors: True
      changed_when:
      - vault_status.rc == 2
      delegate_to: 127.0.0.1

    - name: Unseal vault with unseal keys
      shell: |
          vault operator unseal {{ unseal_key }}
      environment:
          VAULT_ADDR: "{{ vault_addr }}"
      when: vault_status.rc == 2
      delegate_to: 127.0.0.1

Now you should have a playbook that you can run if you have to restart your vault instance, as well as unseal it remotely.

As an Amazon Associate I earn from qualifying purchases.

If you have found this post useful, please consider donating.