I maintain a Django application at the company at which I am employed. Initially, changes  weren’t very frequent, so logging into the host and running a few commands when the application needed to be updated wasn’t a big deal. However, feature creep has set in and changes to the code base are not  only more frequent, but are done by more people. Something had to be done.

Whatever solution I was going to implement had a few requirements:

  1. No secrets should be visible in source control.
  2. No secrets should be built into the Docker image (anyone who can inspect it can view said secrets).
  3. All code for the process should be stored in source control
  4. The deployment process should not be limited to being run from my laptop or the application server.

Numbers one and three are problematic together, as it’s very easy to  just leave a few files in your .gitignore that contain all of your  secrets, but that means that #4 is impossible then. Clearly, many people have done this before, right? I mean, isn’t this what CI/CD and DevOps are all about? So, I took to Google.

The third principal of the Twelve Factor App is that the “app stores config in environment variables.” I was already doing that: setting the environment variables for things like my database password and secret key in the supervisor config. A lot  of blog posts recommend setting those environment variables with the ENV command in the Dockerfile, but that violates requirement #2 and  possibly #1, depending on the method. So that was right out.

The docker run command supports passing environment variables to the  container with the -e flag. This seemed like the place to do it, but the question was how.

Another project I had worked on was doing a proof-of-concept of Ansible Tower. For a multitude of reasons (including very high licensing costs), we went with the open-source upstream, AWX, instead. As I’d been  implementing that, I’d become aware of Ansible Vault, the a feature of  Ansible that allows keeping sensitive data such as passwords or keys in  encrypted form instead of plaintext. As Ansible has modules for Docker, AWX was looking like a great tool to use for deployment.

I started writing a simple playbook:

---
- hosts: appservers
  become: yes
  become: appuser
  vars_files:
    - "vars/Production.yml"
  tasks:
    - name: Clone git repository
      git:
        repo: ssh://[email protected]/myproject.git
        dest: /opt/myproject/src
        accept_hostkey: yes
        version: master
        key_file: /opt/myproject/.ssh/id_rsa
        force: yes
      register: gitpull
    - name: Build Docker Image
      docker_image:
        name: myproject-prod
        path: /opt/myproject/src
        state: present
        force: yes
        buildargs:
          settingsfile: "{{ settings_file }}"
      when: gitpull.changed
      register: build
    - name: Run the container
      docker_container:
        name: myproject-prod
        image: myproject-prod:latest
        published_ports: '8000:8000'
        state: started
        restart_policy: always
        recreate: '{{ "yes" if (build.changed) else "no" }}'
        env:
            SECRET_KEY: "{{ secretkey }}"
            SQL_HOST: "{{ sqlhost }}"
            SQL_USERNAME: "{{ sqluser }}"
            SQL_PASSWORD: "{{ sqlpassword }}"
            BIND_PASSWORD: "{{ bindpassword }}"
            

I’m doing a number of things here. Most of them should be fairly simple,  but I’ll talk about a few of the more interesting things I did. First,  I’m only building the container when cloning the git repo reports a  change. There’s no need to rebuild the image if it’s going to build the  same thing. Second, I’m setting recreate to yes if I did rebuild the image, as I want to run the new container, not keep the old  one chugging along. I could have left it statically as yes and set the  task to run based on “when” conditions, but there are situations in which I may need to start the container via deployment (like if my host  goes down).

You’ll also note that I brought in a vars_file. This file contains  definitions of all of the variables you see in the “env” section of the  docker_container task and looks something like this:

secretkey: !vault |
    $ANSIBLE_VAULT;1.1;AES256
    39393766663761653337386436636466396531353261383237613531356531343930663133623839
    3436613834303264613038623432303837393261663233640a363633343337623065613166306363
    37336132363462386138343535346264333061656134636631326164643035313433393831616131
    3635613565373939310a316132313764356432333366396533663965333162336538663432323334
    33656365303733303664353961363563313236396262313739343461383036333561
sqlhost: prodsql.mycompany.com
sqluser: myproject_prod
sqlpassword: !vault |
    $ANSIBLE_VAULT;1.1;AES256
    34396232643133323034666335313939633865356534303064396238643939343337626330666164
    6231303061373666326264386538666564373762663332310a323938626239363763343638353264
    64646266663361386633386331656163353438623033626633366664303536396136353834336364
    6363303532303265640a396264616562663963653034376462613035383333373437653362616566
    3531

This was created using the ansible-vault command, like this:

ansible-vault encrypt_string 'super_secret_password' --name 'sqlpassword'

This tool then asks you for the vault password, then spits out the encrypted version of the string.

From there, we can run the playbook like this:

ansible-playbook --ask-vault-pass -K -i prod deploy.yml

After entering our sudo and vault passwords, the application is deployed.

Next time, I’ll talk about turning this into a one-click operation with AWX.