So we are all building and packaging our stuff inside containers. That’s great! Containers lets us focus more on configuration and less on installation. We can scale stuff on Kubernetes. We can run stuff on everything from macOS to CentOS. In short, containers opens up a ton of opportunities for deployment and operations. Great.
If you are using containers you are probably also aware of continuous integration and how important it is to use automatic tests of all your new code. But do you test your containers, or just the code inside it?
At Basalt we don’t make applications. We make systems. We put together a bunch of third party applications and integrate them together. Most of it run inside containers. So for us it is important to test these containers, to make sure they behave correctly, before we push them to the registry.
Use encapsulation
So the approach that we chose was to encapsulate the container we wanted to test inside a second test container. This is easily done by using the FROM directive when building the test container.
The test container installs additional test tools and copies the test code into the container. We chose to use InSpec as the test framework for this, but any other framework, or just plain Bash, works just as well.
Testing Nginx with InSpec
So let’s make an example test of a container. In this example I will build a very simple web service using the Nginx container. Then I will use InSpec to verify that the container works properly.
Let’s start by creating all files and folders:
$ mkdir -p mycontainer mycontainer_test/specs $ touch mycontainer/Dockerfile \ mycontainer_test/{Dockerfile,specs/web.rb} $ tree . . ├── mycontainer │ └── Dockerfile ├── mycontainer_test │ ├── Dockerfile │ └── specs │ └── web.rb └── test_container 4 directories, 3 files
Then add the following content to mycontainer/Dockerfile
:
FROM nginx RUN echo "Hello, friend" > \ /usr/share/nginx/html/index.html
Now we can build the app container:
$ docker build -t mycontainer mycontainer/. Sending build context to Docker daemon 2.048kB Step 1/2 : FROM nginx ---> 2bcb04bdb83f Step 2/2 : RUN echo Hello > /usr/share/nginx/html/index.html ---> Running in 7af1cec318f9 Removing intermediate container 7af1cec318f9 ---> fe25cbbf80f9 Successfully built fe25cbbf80f9 Successfully tagged mycontainer:latest $ docker run -d --name hello -p 8080:80 mycontainer cfd3c3ea70c3512c5a7cc9ac2c9d74244aec4dd1a4bb68645e671cbe551af4ab $ curl localhost:8080 Hello, friend $ docker rm -f hello hello
Great! So the container build and manual testing shows that it works. Next step is to build the test container. Add the following content to mycontainer_test/Dockerfile
:
FROM mycontainer ARG INSPEC_VERSION=3.7.11 # install inspec RUN apt-get update \ && apt-get -y install curl procps \ && curl -o /tmp/inspec.deb \ https://packages.chef.io/files/stable/inspec/${INSPEC_VERSION}/ubuntu/18.04/inspec_${INSPEC_VERSION}-1_amd64.deb \ && dpkg -i /tmp/inspec.deb \ && rm /tmp/inspec.deb # copy specs COPY spec /spec
Next we write our tests. Add the following to mycontainer_test/specs/web.rb
:
name = 'nginx: master process nginx -g daemon off;' describe processes(name) do it { should exist } its('users') { should eq ['root'] } end describe processes('nginx: worker process') do it { should exist } its('users') { should eq ['nginx'] } end describe http('http://localhost') do its('status') { should eq 200 } its('body') { should eq "Hello, friend\n" } end
Now we can build and run our test container:
$ docker build -t mycontainer:test mycontainer_test/. Sending build context to Docker daemon 4.608kB ... snip ... Successfully built 6b270e36447a Successfully tagged mycontainer:test $ docker run -d --name hello_test mycontainer:test e3f0e4a06efa416167d5d30458785bf66975d7837d9fc2b04634bb8291bc5679 $ docker exec hello_test inspec exec /specs Profile: tests from /specs (tests from .specs) Version: (not specified) Target: local:// Processes nginx: master process nginx -g daemon off; ✔ should exist ✔ users should eq ["root"] Processes nginx: worker process ✔ should exist ✔ users should eq ["nginx"] HTTP GET on http://localhost ✔ status should eq 200 ✔ body should eq "Hello, friend\n" Test Summary: 6 successful, 0 failures, 0 skipped $ docker rm -f hello_test hello_test
And that’s it! We have successfully built an app container and tested it using InSpec. If you want to use another version of InSpec you can specify it as an argument when building the test container:
$ docker build -t mycontainer:test \ --build-arg INSPEC_VERSION=1.2.3 mycontainer_test/.
Happy testing!