This is the third and last tutorial in our series about creating CICD pipelines with Docker containers.Part one focused on how to use Docker Hub to automatically build your application images, andpart two used the online Travis-ci platform to automate the process of running those units tests.
Now that our little flask application is tested by Travis , let's see how we can make sure a docker image is being built and pushed to the hub, instead of using the autobuild feature. Then, we will see how to deploy the latest build image automatically every time some code is added to the project.
I will assume in this tutorial that you already have a remote host set up and that you have access to it through ssh. To create the examples, I have been using a host running on Ubuntu, but it should be very easy to adapt the following to any popular linux distribution.
RequirementsFirst, you will need to have the travis command-line client installed on your workstation (make sure the latest version of ruby is installed and run):
sudo gem install travisLogin with your GitHub account by running:
travis login --orgOn your remote host, make sure that Docker engine and docker-compose are installed properly. Alternatively, you can also install compose locally; this will be useful if you later add services on which your applications rely (e.g., databases, reverse-proxies, load-balancer, etc.).
Also, make sure that the user you are logging in with is added to the docker group; this can be done on the remote host with:
sudo gpasswd -a ${USER} dockerThis requires a logout to be effective.
Building and pushing the image with TravisIn this step, you will be modifying your existing Travis workflow in order push to the image we've built and tested onto the hub. To do so, Travis will need to access the hub with your account. Let's add an encrypted version of your credentials to your .travis.yml file with:
travis encrypt DOCKER_HUB_EMAIL=<email> --add travis encrypt DOCKER_HUB_USERNAME=<username> --add travis encrypt DOCKER_HUB_PASSWORD=<password> --addWe can now leverage the tag and push features of the Docker engine by simply adding the following lines to the script part:
- docker tag flask-demo-app:latest $DOCKER_HUB_USERNAME/flask-demo-app:production - docker push $DOCKER_HUB_USERNAME/flask-demo-app:productionThis will create an image tagged "production" and ready to download from your Docker Hub account. Now, let's move on to the deployment part.
Automatic deployment with TravisWe will useDocker compose to specify how and which image should be deployed on your remote host. Create a docker-compose.yml file at the root of your project containing the following text:
version: '2' services: app: image: <your_docker_hub_id>/<your_project_name>:production ports: - 80:80Send this file to your production host with scp:
scp docker-compose.yml ubuntu@host:Now that you have set up docker-compose on your remote host, let's see how you can prepare Travis to do the same automatically each time your application is builded and tested successfully.
The general idea is that you will add some build commands in the Travis instructions that will connect to your remote host via ssh and run docker-compose to update your application to its latest available version.
For that purpose, you will create a special ssh key that will be used only by Travis. The user using this key will be allowed to run only one script named deploy.sh, which calls several docker-compose commands in a row.
Create a deploy.sh file with the following content:
docker-compose down docker-compose pull docker-compose up -dMake the file executable and send it to your host with:
chmod +x ./deploy.sh scp deploy.sh ubuntu@host:Create the deploy key in your repo code with:
ssh-keygen -f deploy_keyCopy the output of the following command in your clipboard:
echo "command=./deploy.sh",no-port-forwarding,no-agent-forwarding,no-pty $(cat ./deploy_key.pub)Connect to your host and paste this output to the .ssh/authorized_keys of your user. You should end up with a command similar to this one:
echo 'command="./deploy.sh",no-port-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/OAw[...]kQ728t3jxPPiFX' >> ~/.ssh/authorized_keysThis will make sure the only command allowed for the user connecting with the deploy key is our deployment script.
You can test that everything is in order by running once:
ssh -i deploy_key <your_user>@$<your_remote_host_ip> ./deploy.shNow that you have tested your deployment script, let's see how you can have Travis run it each time the tests are successful.
First, let's encrypt the deployment key (necessary because you DO NOT want any unencrypted private key in your repository) with:
travis encrypt-file ./deploy_key --addNote the use of the --add option that will help you by adding the decryption command in your travis file. Refer to the Travis documentation on encryption to learn more.
Add it to the project with:
git add deploy_key.enc git commit -m "Adding enrypted deploy key" deploy_key.enc git pushYou should now be able to see your encrypted deploy_key in your projects settings on Travis-ci:
encrypted_deploy_key.png
Used with permission
Finally, add the following section to your .travis.yml file, (take care of updating accordingly your remote host ip):
deploy: provider: script skip_cleanup: true script: chmod 600 deploy_key && ssh -o StrictHostKeyChecking=no -i deploy_key ubuntu@<your_remote_host_ip> ./deploy.sh on: branch: masterCommit and push your change:
git commit -m "Added deployment instructions" .travis.yml git pushHead to your Travic-ci dashboard to monitor your build, you should see a build output similar to this one:
deploy.png
Used with permission
Your build has been deployed to your remote host! You can also verify this by running a docker ps on your host and check for the STATUS column, which should give you the uptime of the app container:
ubuntu@demo-flask:~$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ae1797d92bf8 lalu/flask-demo:latest "/entrypoint.sh" 3 hours ago Up 10 minutes 0.0.0.0:80->80/tcp ubuntu_app_1To sum it up, here is what your final travis.yml file should look like:
sudo: required language: python services: - docker before_install: - docker login --email=$DOCKER_HUB_EMAIL --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD - openssl aes-256-cbc -K $encrypted_ced0c438de4d_key -iv $encrypted_ced0c438de4d_iv -in deploy_key.enc -out ./deploy_key -d - docker build -t flask-demo-app . - docker run -d --name app flask-demo-app - docker ps -a script: - docker exec app python -m unittest discover - docker tag flask-demo-app:latest $DOCKER_HUB_USERNAME/flask-demo-app:production - docker push $DOCKER_HUB_USERNAME/flask-demo-app:production after_script: - docker rm -f app deploy: provider: script skip_cleanup: true script: chmod 600 deploy_key && ssh -o StrictHostKeyChecking=no -i ./deploy_key ubuntu@demo-flask.buffenoir.tech './deploy.sh' on: branch: master env: global: - secure: DCNxizK[...]pygQ= - secure: cnpkOl9[...]dHKc= - secure: wy5+mu0[...]MqvQ=Et voilà! Each time you will be adding code to your repository that pass your set of tests, it will also be deployed to your production host.
ConclusionOf course, the example application showcased in this series is very minimalistic. But it should be easy to modify the compose file to add, for example, some databases and proxies. Also, the security could be greatly improved by using private installation of Travis. Last but not least, the workflow should be customized to support different branches and tags according to your environments (dev, staging, production, etc.).
Read previous articles:
Integrating Docker Hub In Your Application Build Process
How to Automate Web Application Testing With Docker and Travis