
ごきげんよう、じんぐうじです。
Dockerは非常に便利で、もはや無いと生きていけない身体にされてしまっています。
普通にDockerfileを書いてビルドすれば普通に動くとは思いますが、
将来的に変更を加えることを前提に考えれば、健全なコンテナ生活を送るためにlint、testが必要です。
ということで、そのために必要な道具をいくつか見繕いました。
※イメージに余分なものを入れないとかレイヤーを薄くする話は今回はしません
Dockerfileのlintをする
Dockerfileのlinterでぱっと見つかるのはこのあたりでした。
ということで雑に書いたDockerfileに対して実行してみます。
両方ともdocker imageで提供されてるので楽できますね。
-
Dockerfile
FROM golang AS build MAINTAINER jinguji WORKDIR /opt/ COPY ./message.go /opt/ RUN go build message.go FROM debian WORKDIR /opt/ COPY --from=build /opt/message /opt/ COPY message.txt /opt/ ENTRYPOINT ["./message"]
-
projectatomic/dockerfile-lint実行
$ docker run -it --rm --privileged -v $PWD:/root/ \ projectatomic/dockerfile-lint \ dockerfile_lint -f Dockerfile # Analyzing Dockerfile --------ERRORS--------- Line 1: -> FROM golang AS build ERROR: Invalid parameters for command.. Reference -> https://docs.docker.com/engine/reference/builder/ ERROR: Required LABEL name/key 'Name' is not defined. Reference -> http://docs.projectatomic.io/container-best-practices/#_recommended_labels_for_your_project ERROR: Required LABEL name/key 'Version' is not defined. Reference -> http://docs.projectatomic.io/container-best-practices/#_recommended_labels_for_your_project --------INFO--------- Line 2: -> MAINTAINER jinguji INFO: the MAINTAINER command is deprecated. MAINTAINER is deprecated in favor of using LABEL since Docker v1.13.0. Reference -> https://github.com/docker/cli/blob/master/docs/deprecated.md#maintainer-in-dockerfile INFO: There is no 'EXPOSE' instruction. Without exposed ports how will the service of the container be accessed?. Reference -> https://docs.docker.com/engine/reference/builder/#expose INFO: There is no 'CMD' instruction. None. Reference -> https://docs.docker.com/engine/reference/builder/#cmd
-
lukasmartinelli/hadolint実行
$ docker run --rm -i lukasmartinelli/hadolint < Dockerfile /dev/stdin DL4000 MAINTAINER is deprecated /dev/stdin:1 DL3006 Always tag the version of an image explicitly. /dev/stdin:7 DL3006 Always tag the version of an image explicitly.
lintのルールが異なるのは標準が無ければまあそうだよね、なのでお好みに合わせてご利用ください、
なのですが、dockerfile-lintの方は参照先リンクが表示されるのでなぜ引っかかったのかわかって助かりますね…!
FROM ... AS ...
がエラー扱いになる点がモヤっとしますが…
イメージのテスト
Dockerfileのlintをしたら(lintの結果を修正したり無視したりはケースバイケースですが)buildしてイメージを作るのですが、
最初に書いたように作ったイメージはテストしましょうね、ということで有名どころをいくつか試してみます。
Container Structure Tests
名前の通りコンテナイメージの構造のテストを行うためのツール、です。
できることとしては
- コマンドが正しく実行できるか
- ファイルが存在するか、しないか、パーミッションが正しいか
- ファイルの内容が正しいか
- Metadata(Env,Entrypoint,Cmd,Exposed Ports,Volumes,Workdir)が正しいか
- ライセンス
となっており、後述するServerspec/dgossと被る部分もあるが単体では不足する、といった具合です。
イメージのtarballに対しても実行できますが、その場合はコマンドテストは実施できません。
ということで例です。Dockerfileはさきほどのものを使います。
-
config.yaml
--- schemaVersion: '2.0.0' fileExistenceTests: - name: 'message' path: '/opt/message' shouldExist: true permissions: '-rwxr-xr-x' commandTests: - name: 'message' command: '/opt/message' args: [] expectedOutput: ['Hello World!!'] exitCode: 0 fileContentTests: - name: 'message.txt' path: '/opt/message.txt' expectedContents: ['Hello World!!'] metadataTest: entrypoint: ['./message'] cmd: [] workdir: ['/opt/'] licenseTests: - debian: true files:
-
container-structure-test実行
$ ./container-structure-test -test.v -image example-hello config.yaml Using driver docker === RUN TestAll 2018/03/14 13:02:02 Running tests for file config.yaml === RUN TestAll/Command_Test:_message === RUN TestAll/File_Existence_Test:_message === RUN TestAll/File_Content_Test:_message.txt === RUN TestAll/Metadata_Test === RUN TestAll/License_Test_#0 --- PASS: TestAll (90.92s) --- PASS: TestAll/Command_Test:_message (1.09s) docker_driver.go:85: stdout: Hello World!! --- PASS: TestAll/File_Existence_Test:_message (0.52s) --- PASS: TestAll/File_Content_Test:_message.txt (0.53s) --- PASS: TestAll/Metadata_Test (0.00s) --- PASS: TestAll/License_Test_#0 (88.77s) licenses.go:70: adduser licenses.go:70: apt licenses.go:70: base-files : (中略) : licenses.go:70: tzdata licenses.go:70: util-linux licenses.go:70: zlib1g structure_test.go:49: Total tests run: 5 PASS
設定ファイルをYAMLで書けるのは見通しが良くて便利感ありますね!
(ちなみに拡張子がymlだとちゃんと認識してくれません)
あくまで構造のテストとしてとらえるとtarballへ対するテストのみ実施して、コマンドテストはServerspec/dgoss
に任せた方がすっきりするかもしれません。
Serverspec/goss(dgoss)
Serverspecとgossはコンテナイメージがどうこうというものではなく、サーバに対してテストを実行するための道具となります。
dgossはgossのバックエンドとしてDockerを使用するためのラッパーとなります。
できることは多々あるのですが
- リスンポート
- サービス
- パッケージ
- ユーザ、グループ
- プロセス
- ファイル
- コマンド
等のテストが可能であり、Container Structure Testsと比較してOS寄りのテストが可能となります。
ということで下記の雑な例をテストしてみましょう。
-
Dockerfile
FROM debian:9 RUN apt-get update \ && apt-get install -y --no-install-recommends \ nginx \ net-tools \ procps \ && apt-get clean COPY ./index.html /var/www/html/ CMD ["/usr/sbin/nginx", "-g", "daemon off;"]
-
index.html
<html> <head></head> <body> Hello World!! </body>
-
goss.yaml
package: nginx: installed: true versions: - 1.10.3-1+deb9u1 process: nginx: running: true port: tcp:80: listening: true file: /usr/share/nginx/html/index.html: exists: true filetype: file contains: - "Hello World!!"
-
spec/example-nginx/example-nginx_spec.rb
require 'spec_helper' set :docker_image, 'example-nginx' describe package('nginx') do it { should be_installed } end describe command('nginx -v') do its(:stderr) { should match /1.10.3/ } end describe process('nginx') do it { should be_enabled } it { should be_running } end describe port('80') do it { should be_listening } end describe file('/usr/share/nginx/html/index.html') do it { should exist } its(:content) { should match /Hello World!!/ } end
-
dgoss実行
$ GOSS_PATH=./goss-linux-amd64 ./dgoss run --rm example-nginx INFO: Starting docker container INFO: Container ID: 6aaceea1 INFO: Sleeping for 0.2 INFO: Running Tests Process: nginx: running: matches expectation: [true] Port: tcp:80: listening: matches expectation: [true] File: /usr/share/nginx/html/index.html: exists: matches expectation: [true] File: /usr/share/nginx/html/index.html: filetype: matches expectation: ["file"] File: /usr/share/nginx/html/index.html: contains: matches expectation: [Hello World!!] Package: nginx: installed: matches expectation: [true] Package: nginx: version: matches expectation: [["1.10.3-1+deb9u1"]] Total Duration: 0.006s Count: 7, Failed: 0, Skipped: 0 INFO: Deleting container
-
Serverspec実行
$ bundle exec rspec Package "nginx" should be installed Command "nginx -v" stderr should match /1.10.3/ Process "nginx" should be enabled should be running Port "80" should be listening File "/usr/share/nginx/html/index.html" should exist content should match /Hello World!!/ Finished in 1.09 seconds (files took 0.36657 seconds to load) 6 examples, 0 failures
ということでnginxが80/tcpで動いていてindex.htmlが「Hello World!!」を含むことがテストできました。dgossの方はnginxのバージョンまで
dgossはやはりyamlで書けてすっきりしているのですが、複数パッケージのインストール状況をテストするようなケース等では
Serverspecであればrubyなのでループで記述できたりするなど一長一短ですね…
(ちなみにgdossも拡張子がymlだとちゃんと認識してくれません)
また、Serverspecではprocessのためにpsコマンド、portのためにnetstatが必要となり、
dgossのテストケースに似せるためにあえて今回はprocps、net-toolsを導入しています。本質的に必要ないものを導入する必要があるという点は抵抗感がありますね。
infrataster
infratasterはサーバの振る舞いをテストするもので、Serverspec/gossがホワイトボックステストであることに対してブラックボックステストを行います。
例えば今回の例であれば前述と同じコンテナイメージに対して、
- httpレスポンスが200で返ってくること
- レスポンスヘッダのcontent-typeがtext/htmlであること
- レスポンスボディに「Hello World!!」が含まれること
を等テストし、何のミドルウェアが稼働しているか、ファイルが存在するか、等は考慮しないテストとなります。
今回の例では使っていませんがCapybaraも使えます。便利ですね。
-
spec/infra_spec.rb
require 'spec_helper.rb' require 'infrataster/rspec' require 'docker' # Dockerfileをビルドしてコンテナを作成する image = Docker::Image.build_from_dir('.') container = Docker::Container.create('Image' => image.id) container.start # テストスイートの最後にコンテナを削除する RSpec.configure do |config| config.after(:suite) do container.delete(:force => true) end end # コンテナが準備できるまで待つ begin sleep 1 end while container.json['State']['Running'] == false # テスト対象のサーバ(コンテナ)の定義 Infrataster::Server.define(:example_nginx) do |server| server.address = container.json['NetworkSettings']['IPAddress'] end describe server(:example_nginx) do describe http('http://' + server(:example_nginx).server.address) do it 'responds as 200' do expect(response.status).to be 200 end it 'responds as "text/html"' do expect(response.headers['content-type']).to eq('text/html') end it 'responds as "Hello World!!"' do expect(response.body).to include('Hello World!!') end end end
-
Infrataster実行
$ bundle exec rspec server 'example_nginx' http 'http://172.17.0.4' with {:params=>{}, :method=>:get, :headers=>{}} responds as 200 responds as "text/html;" responds as "Hello World!!" Finished in 0.7387 seconds (files took 3.07 seconds to load) 3 examples, 0 failures
無事、コンテナの外側から見てhttpでindex.htmlがサーブされていることが確認できましたね。
この例では実行環境がDockerホストを想定して自前でコンテナの作成を行っていますが、実際の環境とは異なる(FWやProxyを経由していないなど)場合はテストのフェーズや目的によってコンテナの起動方法、テスト対象のURLや経路は考慮が必要となります。
まとめ
何をテストするためにどのツールを使うかは、作るものがどういうものであるかによって変わってきますが、
今回登場したものを組み合わせれば概ねコンテナイメージに関するlint/testはカバーできると思います。
これらをCIでまわせば健全なコンテナ生活が送れますね!!
ちなみにzuazo/dockerspecなるものがあり、これはServerspec/Infratasterをまるっとまとめてスッキリ書けるようです。READMEを見る感じDockerfileのメタデータのテストもできそうで便利感が滲み出ています。
infratasterのことを書いている辺りでこれを見つけました。
切ないですね。
頑張ります。