Fig for Test Environment
Nov 29, 2014Fig is this amazing tool that’s based on Docker, what it does is it lets you provision Docker containers as environments at the same time respecting Docker’s way of doing things (ie. Dockerfile). Makes things easy like exposing ports, linking containers and environment variables by defining them declaratively using a YAML file. Think of it as an abstraction that focuses more managing your containers as services that can be linked together.
Due to the isolated nature of containers, it makes for a perfect place to run our tests which requires fresh setup. Another is speed, we could achieve the same thing using a VM but the setup and teardown is slow compared to Docker containers. You want your tests to fail fast so you can react and fix it.
To demonstrate we’re going to build a simple key-value storage, dictionary as a service if you will. To do this, we’re going to write the application layer in Python and for the storage we’re going to take advantage of Redis’ hash. So here we have two components, the application and the storage, we’re going to split them into their own container later and use Fig to link them and run our tests.
Lets go ahead and write our application:
# bucket.py
import redis
from flask import Flask, request
app = Flask(__name__)
r = redis.StrictRedis(host='localhost', port=6379, db=0)
@app.route('/set/', methods=['POST'])
def set(name):
r.set(name, request.form.get(name))
return 'OK'
@app.route('/get/', methods=['GET'])
def get(name):
return r.get(name)
if __name__ == "__main__":
app.run()
Our app is just a basic Flask application that exposes two endpoints, one for set and another to get whatever we set. Then we write test if this thing actually works:
# bucket_test.py
import bucket
import unittest
class BucketTestCase(unittest.TestCase):
def setUp(self):
self.app = bucket.app.test_client()
def test_set(self):
response = self.app.post(
'/set/foobar', data={'foobar': 'wangskata'})
assert response.status_code == 200
assert response.data == 'OK'
def test_get(self):
response = self.app.get('/get/foobar')
assert response.status_code == 200
assert response.data == 'wangskata'
if __name__ == '__main__':
unittest.main()
Lets make sure our tests actually passes:
$ python bucket_test.py
..
--------------------------------------------------------
Ran 2 tests in 0.020s
Alright, now time to containerize these things, here’s our fig.yml file:
# fig.yml
redis:
image: redis:2.8
test:
build: .
volumes:
- .:/var/www/bucket
links:
- redis
command: python bucket_test.py
There’s a couple of things going on in here, first we have two services defined: redis and test. The redis service uses the official redis image provided by Docker. Once we run fig up later, it’ll download that image so we can spawn container base-off of it.
On the test service we use build instead which creates our custom image for our app, the details about this image is handled by Dockerfile which is Docker’s official way for image recipe. We’ll define this later, next we mount the current directory inside the container to /var/www/bucket, this will be the location of our app. Then we link the redis service to test, when linking Fig will automatically create a bunch of environment variables that exposes a bunch of info about the linked service (redis). You can read more about how these environment variables are created here.
Time to define our Dockerfile:
FROM ubuntu:14.04
ADD . /var/www/bucket
WORKDIR /var/www/bucket
RUN apt-get update -y && apt-get install python-pip -y
RUN pip install -r requirements.txt
Pretty straighforward, we’re creating an image based-off of an ubuntu:14.04 image, adding the current directory to the container under /var/www/bucket and installing the requirements. We also need to update our bucket.py so we don’t hard-code redis’ host and port, instead we’ll fetch them from the environment variables that becomes available once the redis service is linked:
# bucket.py
import os
import redis
from flask import Flask, request
redis_host = os.environ.get('REDIS_1_PORT_6379_TCP_ADDR')
redis_port = os.environ.get('REDIS_1_PORT_6379_TCP_PORT')
app = Flask(__name__)
r = redis.StrictRedis(host=redis_host, port=redis_port, db=0)
...
Now lets run fig up which will create, start, link and attach to our services:
$ fig up
This will pull all the required images, redis:2.8 in this case, build our app’s image and them link them. Once the images are built, we can now send one-off commands to our app container. In particular, we’re interested in sending our command defined on fig.yml for our test service.
$ fig run --rm test
Starting bucket_redis_1...
..
--------------------------------------------------------
Ran 2 tests in 0.022s
Removing bucket_test_run_1...
The flag –rm here just means to remove the container once the tests stops running which is nice since we no longer need it. Also, notice how we only pass the service name, it’ll use the command from fig.yml, if you pass anything, it’ll override the one from fig.yml.
And there we have it, we can now run our tests in an isolated container. You can download all the source files here.