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:
- No secrets should be visible in source control.
- No secrets should be built into the Docker image (anyone who can inspect it can view said secrets).
- All code for the process should be stored in source control
- 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://git@git.mycompany.com/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.
Comments