CLOVER🍀

That was when it all began.

hadolintでDockerfileの静的解析を行う

これは、なにをしたくて書いたもの?

hadolintというDockerfileのlintがあるので、試してみることにしました。

hadolint

hadolintは、Dockerイメージをベストプラクティスに従って構築するためのDockerfileのlinterです。

Dockerfile Linter

GitHub - hadolint/hadolint: Dockerfile linter, validate inline bash, written in Haskell

ベストプラクティスというのは、こちらのことです。

Best practices for writing Dockerfiles | Docker Documentation

機能としては、以下のようですね。

  • DockerfileのASTを解析して、ルール実行
  • ShellcheckによるRUN命令内のBashコードチェック

hadolintというのは、Haskell Dockerfile Linterの略で良いのでしょうか?

現時点でのバージョンは2.12.0のようです。

https://github.com/hadolint/hadolint/tree/v2.12.0

使い方としては、オンラインで実行するか

Dockerfile Linter

ダウンロードやパッケージインストールして

Haskell Dockerfile Linter / Install

実行することになるようです。

Haskell Dockerfile Linter / How to use

Dockerイメージもあるようです。

hadolint/hadolint

設定ファイル。

Haskell Dockerfile Linter / Configure

定義されているルール。

Haskell Dockerfile Linter / Rules

インテグレーションについては、こちら。

Hadolint Integrations

コードレビューに組み込んだり、GitHub Actions、GitLab CIなどに組み込む方法が書かれています。

EmacsVisual Studio Codevimなどのエディター向けのエクステンションやプラグインもあるようです。

とりあえず、使っていってみましょう。

環境

今回の環境は、こちら。Ubuntu Linux 20.04 LTSです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
Codename:       focal


$ uname -srvmpoi
Linux 5.4.0-135-generic #152-Ubuntu SMP Wed Nov 23 20:19:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

使用するDocker。

$ docker version
Client: Docker Engine - Community
 Version:           20.10.22
 API version:       1.41
 Go version:        go1.18.9
 Git commit:        3a2c30b
 Built:             Thu Dec 15 22:28:08 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.22
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.18.9
  Git commit:       42c8b31
  Built:            Thu Dec 15 22:25:58 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.13
  GitCommit:        78f51771157abb6c9ed224c22013cdf09962315d
 runc:
  Version:          1.1.4
  GitCommit:        v1.1.4-0-g5fd4c4d
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

hadolintをインストールする

今回は、GitHubのReleasesからバイナリをダウンロードして使うことにします。

Releases · hadolint/hadolint · GitHub

ダウンロード。

$ curl -o hadolint -L https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64

実行権限を与えて

$ chmod a+x hadolint

バージョン確認。

$ ./hadolint --version
Haskell Dockerfile Linter 2.12.0

ヘルプ。

$ ./hadolint --help
hadolint - Dockerfile Linter written in Haskell

Usage: hadolint [-v|--version] [-c|--config FILENAME] [DOCKERFILE...]
                [--file-path-in-report FILEPATHINREPORT] [--no-fail]
                [--no-color] [-V|--verbose] [-f|--format ARG] [--error RULECODE]
                [--warning RULECODE] [--info RULECODE] [--style RULECODE]
                [--ignore RULECODE]
                [--trusted-registry REGISTRY (e.g. docker.io)]
                [--require-label LABELSCHEMA (e.g. maintainer:text)]
                [--strict-labels] [--disable-ignore-pragma]
                [-t|--failure-threshold THRESHOLD]

  Lint Dockerfile for errors and best practices

Available options:
  -h,--help                Show this help text
  -v,--version             Show version
  -c,--config FILENAME     Path to the configuration file
  --file-path-in-report FILEPATHINREPORT
                           The file path referenced in the generated report.
                           This only applies for the 'checkstyle' format and is
                           useful when running Hadolint with Docker to set the
                           correct file path.
  --no-fail                Don't exit with a failure status code when any rule
                           is violated
  --no-color               Don't colorize output
  -V,--verbose             Enables verbose logging of hadolint's output to
                           stderr
  -f,--format ARG          The output format for the results [tty | json |
                           checkstyle | codeclimate | gitlab_codeclimate | gnu |
                           codacy | sonarqube | sarif] (default: tty)
  --error RULECODE         Make the rule `RULECODE` have the level `error`
  --warning RULECODE       Make the rule `RULECODE` have the level `warning`
  --info RULECODE          Make the rule `RULECODE` have the level `info`
  --style RULECODE         Make the rule `RULECODE` have the level `style`
  --ignore RULECODE        A rule to ignore. If present, the ignore list in the
                           config file is ignored
  --trusted-registry REGISTRY (e.g. docker.io)
                           A docker registry to allow to appear in FROM
                           instructions
  --require-label LABELSCHEMA (e.g. maintainer:text)
                           The option --require-label=label:format makes
                           Hadolint check that the label `label` conforms to
                           format requirement `format`
  --strict-labels          Do not permit labels other than specified in
                           `label-schema`
  --disable-ignore-pragma  Disable inline ignore pragmas `# hadolint
                           ignore=DLxxxx`
  -t,--failure-threshold THRESHOLD
                           Exit with failure code only when rules with a
                           severity equal to or above THRESHOLD are violated.
                           Accepted values: [error | warning | info | style |
                           ignore | none] (default: info)

では、使っていってみましょう。

hadolintを使ってみる

まずはDockerfileが必要ですね。hadolintのサンプルのDockerfileをそのまま使ってもよいのですが、ここは簡単に作成してみましょう。

ローカルのNode.jsはこちら。

$ node --version
v18.12.1


$ npm --version
8.19.2

Expressを使った、簡単なWebアプリケーションを作成することにします。

$ npm init -y
$ npm i express

バージョン。

  "dependencies": {
    "express": "^4.18.2"
  }

ソースコード

app.js

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
})

const server = app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

process.on('SIGINT', () => server.close(() => {}));
process.on('SIGTERM', () => server.close(() => {}));

ここで、hadolintのサンプルをマネたDockerfileを作成してみます。

Dockerfile

FROM debian

RUN export node_version="12.*" \
    && export npm_version="7.*" \
    && apt-get update \
    && apt-get -y install nodejs="${node_ver}" npm="${npm_version}"

ADD package*.json /usr/src/app/
ADD app.js /usr/src/app

RUN cd /usr/src/app \
    && npm ci

WORKDIR /usr/src/app
EXPOSE 3000

CMD ["node", "app.js"]

これに対して、hadolintを実行してみます。

$ ./hadolint Dockerfile
Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly
Dockerfile:3 SC2154 warning: node_ver is referenced but not assigned.
Dockerfile:3 DL3009 info: Delete the apt-get lists after installing something
Dockerfile:3 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
Dockerfile:8 DL3020 error: Use COPY instead of ADD for files and folders
Dockerfile:9 DL3020 error: Use COPY instead of ADD for files and folders
Dockerfile:11 DL3003 warning: Use WORKDIR to switch to a directory

各種エラー、警告などが表示されました。

シェルスクリプトの未割り当ての変数についての警告も出ています。

Dockerfile:3 SC2154 warning: node_ver is referenced but not assigned.

出力された情報をDockerfileマッピングすると、こんな感じですね。

Dockerfile

# DL3006
FROM debian  

# SC2154, DL3009, DL3015
RUN export node_version="12.*" \
    && export npm_version="7.*" \
    && apt-get update \
    && apt-get -y install nodejs="${node_ver}" npm="${npm_version}"

# DL3020
ADD package*.json /usr/src/app/
# DL3020
ADD app.js /usr/src/app

# DL3003
RUN cd /usr/src/app \
    && npm ci

WORKDIR /usr/src/app
EXPOSE 3000

CMD ["node", "app.js"]

では、hadolintから出力された内容に沿って書き直してみます。

Dockerfile

FROM debian:bullseye-slim

RUN export node_version="12.*" \
    && export npm_version="7.*" \
    && apt-get update \
    && apt-get -y install --no-install-recommends nodejs="${node_version}" npm="${npm_version}" \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /usr/src/app

COPY package*.json /usr/src/app/
COPY app.js /usr/src/app/

RUN npm ci

EXPOSE 3000

CMD ["node", "app.js"]

今度は、hadolintを実行してもなにも言われなくなります。

$ ./hadolint Dockerfile

OKですね。

設定してみる

ここで元のDockerfileに戻してみます。

Dockerfile

FROM debian

RUN export node_version="12.*" \
    && export npm_version="7.*" \
    && apt-get update \
    && apt-get -y install nodejs="${node_ver}" npm="${npm_version}"

ADD package*.json /usr/src/app/
ADD app.js /usr/src/app

RUN cd /usr/src/app \
    && npm ci

WORKDIR /usr/src/app
EXPOSE 3000

CMD ["node", "app.js"]

この状態だと、こんな出力になるのでした。

$ ./hadolint Dockerfile
Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly
Dockerfile:3 SC2154 warning: node_ver is referenced but not assigned.
Dockerfile:3 DL3009 info: Delete the apt-get lists after installing something
Dockerfile:3 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
Dockerfile:8 DL3020 error: Use COPY instead of ADD for files and folders
Dockerfile:9 DL3020 error: Use COPY instead of ADD for files and folders
Dockerfile:11 DL3003 warning: Use WORKDIR to switch to a directory

ここでいくつか、エラーや警告を無視するようにしてみましょう。--ignoreオプションを使用します。

$ ./hadolint Dockerfile --ignore DL3006 --ignore DL3020
Dockerfile:3 DL3009 info: Delete the apt-get lists after installing something
Dockerfile:3 SC2154 warning: node_ver is referenced but not assigned.
Dockerfile:3 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
Dockerfile:11 DL3003 warning: Use WORKDIR to switch to a directory

こんな感じですね。複数指定する場合は、--ignoreオプションを繰り返します。

次は、設定ファイルを使ってみます。

Haskell Dockerfile Linter / Configure

設定ファイルは、以下の順番で探していくようです。上にある方が優先ですね。

  • $PWD/.hadolint.yaml
  • $XDG_CONFIG_HOME/hadolint.yaml
  • $HOME/.config/hadolint.yaml
  • $HOME/.hadolint/hadolint.yaml or $HOME/hadolint/config.yaml
  • $HOME/.hadolint.yaml

今回は、カレントディレクトリで.hadolint.yamlファイルを作成してみます。

.hadolint.yaml

ignored:
  - DL3006
  - DL3020

これで、先ほど--ignoreオプションを指定したのと同じ効果になりました。

$ ./hadolint Dockerfile
Dockerfile:3 DL3009 info: Delete the apt-get lists after installing something
Dockerfile:3 SC2154 warning: node_ver is referenced but not assigned.
Dockerfile:3 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
Dockerfile:11 DL3003 warning: Use WORKDIR to switch to a directory

無視の設定は、インラインにも書けるようです。

Emacsに組み込む

最後に、Emacsに組み込んでみましょう。

こちらを見ると、Flycheckというものを使うようです。

Hadolint Integrations

Flycheckは、Emacsのオンザフライ構文チェック機能です。

Flycheck — Syntax checking for GNU Emacs — Flycheck 33-cvs documentation

hadolintとの組み合わせもサポートしています。

Supported Languages / Dockerfile

こちらを使う場合、hadolintに対するパスを通しておきましょう。

あとはパッケージからflycheckをインストールして、

Installation — Flycheck 33-cvs documentation

Emacsの設定ファイルに以下を追加します。

(use-package flycheck
  :ensure t
  :init (global-flycheck-mode))

推奨されているエクステンションということで、flycheck-color-mode-lineflycheck-pos-tipも入れておきました。

Recommended extensions — Flycheck 33-cvs documentation

flycheckとdocker-modeが有効になっていると、以下のようにhadolintと組み合わせて動かすことができます。

まとめ

hadolintを試してみました。けっこう簡単にDockerfileの確認ができるので、導入しておくと良さそうですね。