- TestOps Strategy
- Posts
- Github To Dockerhub: A CI/CD Pipeline With Serverspec (Part 1)
Github To Dockerhub: A CI/CD Pipeline With Serverspec (Part 1)
Building a Docker CI Pipeline
This is the first of 3 posts in building a CI Pipeline that validates an image for Caddy and publishes the image to DockerHub. Before it can be publish the image, the pipeline should validate that the image builds a proper container. That’s where Serverspec comes in.
What is Serverspec?
Serverspec makes it easy to write infrastructure tests using RSpec. It will verify that servers or in this case, containers, are configured and behaving as expected.
What is Caddy?
Caddy is an extensible platform that serves web sites, apis and services.
Setting up Serverspec
After writing my Dockerfile, I installed the serverspec gem and ran the serverspec-init command. It will ask for the OS and how to connect to the infrastructure under test.
Select OS type:
1) UN*X
2) Windows
Select number: 1
Select a backend type:
1) SSH
2) Exec (local)
Select number: 2
+ spec/localhost/
+ spec/localhost/sample_spec.rb
After the set up is done, I opened the sample_spec.rb file and wrote the following test that builds container from the image provided and verify the version of caddy is installed.
require 'docker'
require 'serverspec'
require 'spec_helper'
describe "Caddy is installed" do
before(:all) do
image = Docker::Image.build_from_dir('.')
set :backend, :docker
set :docker_image, image.id
end
describe command('caddy --version') do
its(:stdout) { should match "v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=\n"}
end
endThis test does two important things:
Builds the Docker image locally
Asserts that the container contains the exact version of Caddy expected
If the upstream package repository introduces a newer version, this test fails by design.
Let’s, run the test locally:
caddyshack % bundle exec rspec Example output:
caddyshack % bundle exec rspec
Caddy is installed
Command "caddy --version"
stdout
is expected to match "v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=\n"
Finished in 2.71 seconds (files took 0.30817 seconds to load)
1 example, 0 failureThe ServerSpec test validates that the container contains the expected version of Caddy. However, verifying that Caddy is present does not guarantee that the image behaves as intended. Let’s update the spec to validate:
Caddyfile exists in /etc/conf/Caddyshack
Caddyfile is configured properly
Caddy is running based on the Caddyfile
First, let’s add a Caddyfile that will be added to the image in order validate that caddy is running and the tests can pass. For simplicity, when the container is running locally, http://localhost should return ok.
:80 {
respond "ok" 200
}Here’s the updated spec that covers the three additional items.
require 'docker'
require 'serverspec'
require 'spec_helper'
describe "Caddy is installed" do
before(:all) do
image = Docker::Image.build_from_dir('.')
set :backend, :docker
set :docker_image, image.id
end
describe command('caddy --version') do
its(:stdout) { should match "v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=\n"}
end
describe "Caddyfile is present" do
describe file('/etc/caddy/Caddyfile') do
it { should exist }
it { should be_file }
its(:content) { should match(/:80/) }
end
end
describe "Caddy config is valid" do
describe command('caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile') do
its(:exit_status) { should eq 0 }
its(:stdout) { should match(/valid configuration|success|OK/i) }
end
end
end
describe "Caddy image runtime behavior" do
before(:all) do
@host_port = 8080
image = Docker::Image.build_from_dir('.')
@container = Docker::Container.create(
'Image' => image.id,
'ExposedPorts' => { '80/tcp' => {} },
'HostConfig' => {
'PortBindings' => {
'80/tcp' => [{ 'HostPort' => @host_port.to_s }]
}
}
)
@container.start
# Wait until Caddy is ready (prevents flaky tests)
30.times do
break if system("curl -fsS http://localhost:#{@host_port} >/dev/null 2>&1")
sleep 0.2
end
end
after(:all) do
@container&.delete(force: true)
end
it "returns ok on HTTP request" do
set :backend, :exec
response = command("curl -fsS http://localhost:#{@host_port}").stdout
expect(response).to eq("ok")
end
endAfter running the specs:
Caddy is installed
Command "caddy --version"
stdout
is expected to match "v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=\n"
Caddyfile is present
File "/etc/caddy/Caddyfile"
is expected to exist
is expected to be file
content
is expected to match /:80/
Caddy config is valid
Command "caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile"
exit_status
is expected to eq 0
stdout
is expected to match /valid configuration|success|OK/i
Caddy image runtime behavior
returns ok on HTTP request
Finished in 4.59 seconds (files took 0.3076 seconds to load)
7 examples, 0 failuresThe image and container has been validated in less than 5 seconds. As the Docker image continues to be enhanced, this test suite provides a strong baseline that can grow alongside new features and capabilities
With these ServerSpec checks in place, the image can be validated and is ready to be published to DockerHub. In the next post, this workflow will run as a GitHub Actions so every change is automatically built, tested, and only then published to DockerHub.