LoginSignup
6
4

【Docker】Dockerのbest practicesをGo言語で

Posted at

はじめに

これはDockerのbest practicesをGo言語のイメージを例にやってみた、という内容の記事です。

Dockerの使い方は覚えたけれど、どのようなDockerfileにすれば良いのかわからない。という方に1つの例を提供することを目的としています。

環境

  • Docker: Docker version 20.10.23, build v20.10.23

Dockerfileサンプル

Dockerのbest practicesを参考に作成したDockerfileがこちらになります。

Dockerfile
FROM golang:1.21.0-bullseye AS base
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod/ \
    go mod download

FROM base AS dev
RUN go install github.com/go-delve/delve/cmd/dlv@latest && \
    go install github.com/cosmtrek/air@latest
COPY . .
CMD ["air", "-c", ".air.toml"]

FROM base AS builder
COPY . .
RUN useradd -u 10001 scratch
RUN --mount=type=cache,target=/go/pkg/mod/ \
    go build \
    -ldflags="-s -w" \
    -o golang-app \
    -trimpath

FROM scratch AS runner
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /app/golang-app golang-app
USER scratch
CMD ["/golang-app"]

適切なベースイメージを選択する

Security best practicesには、以下のような記述があります。

The first step towards achieving a secure image is to choose the right base image. When choosing an image, ensure it’s built from a trusted source and keep it small.

今回のようにDockerfileから独自のイメージを構築する際には、要件にあった最小限のベースイメージを選択するべきとされています。

主なメリットはプル時間の短縮とセキュリティの向上です。イメージサイズが小さいということはそこに含まれているパッケージ数も少ないということであり、攻撃者が狙う脆弱性の数も少なくなります。

軽量なベースイメージを選択するという意味では、今回採用した1.21.0-bullseyeよりも軽量な1.21.0-alpine3.18があります。

※ alpineイメージはuseraddが上手くいかずに諦めました。後から知ったのですが、alpineの場合useraddではなくadduseraddgroupというコマンドを使うそうです。

レイヤーのキャッシュを活用する

Dockerのビルドキャッシュについて知ることで、ビルド時間を短縮できます。

Dockerfileの各命令はそれぞれが1つのレイヤーに対応しています。

レイヤーを構築する命令について

Best practices for writing Dockerfilesに以下のような記述があることから、厳密には1命令1レイヤーではないようです。

Only the instructions RUN, COPY, and ADD create layers. Other instructions create temporary intermediate images, and don’t increase the size of the build.

たとえばrunnerステージのイメージをdocker historyコマンドで確認してみると、4つのレイヤーがあることがわかります。

$ docker history --no-trunc golang:runner
IMAGE                                                                     CREATED          CREATED BY                                                                                                     SIZE      COMMENT
sha256:f5269c93a601261f48922e3bbe3ea20c9dd27fe8d908127b53afdb0950a81f36   44 seconds ago   /bin/sh -c #(nop)  CMD ["/golang-app"]                                                                         0B        
sha256:6de04a2d12be7dcf72177582715cfc2037faaf23ba2d8c061c0d0b4c7bfdb298   45 seconds ago   /bin/sh -c #(nop)  USER scratch                                                                                0B        
sha256:bfbf3472db0a1f864eb242aa4d261412617844d6b523388a2c1f431e72acd4de   45 seconds ago   /bin/sh -c #(nop) COPY file:ae42242b7bba1bd208972c75cbaab0a93f64777c4969c6985e9426583c62c525 in golang-app     1.33MB    
sha256:3e65b3f3dac37b22019fe4a98dc849e6b03caee141b95372d1f5ee854cb14551   46 seconds ago   /bin/sh -c #(nop) COPY file:b0213a4138a2af371e3368129198de30da15688fcbc16fb3becede1c4a2f7f93 in /etc/passwd    967B      

レイヤーは変更される度に再構築を行ないます。レイヤーが変更されると、その後ろにあるすべてのレイヤーも再構築されてしまい、ビルド時間が増加してしまいます。

依存関係のインストールなど、比較的に変更頻度の低い命令は上の方に。ソースファイルのCOPYなど変更頻度の高い命令は下に配置することが大切です。

また.dockerignoreファイルを活用して不要なファイルをビルドコンテキストから除外することでもキャッシュの最適化をはかることができます。

.dockerignore
node_modules

BuildKitを使う

BuildKitはより新しいビルダーです。BuildKitはバージョン23.0からDocker DesktopとDocker Engineのデフォルトビルダーになっています。

BuildKitを使用することで、不要なビルドステージのスキップ、ビルドステージ構築の並列化など、ビルドパフォーマンスが向上します。

またキャッシュマウントが使用できるようになり、パッケージをインストールする命令を高速化するのに役立ちます。キャッシュマウントでビルド中に使用するパッケージキャッシュを指定することで、レイヤーを再構築する場合でも、新しいパッケージや変更されたパッケージのみをダウンロードするようになります。

RUN --mount=type=cache,target=/go/pkg/mod/ \
    go mod download

--mount=type=cache,target=<path>という形式でキャッシュのディレクトリを指定します。

Go言語の場合は以下のコマンドでディレクトリの場所を調べることができます。

$ go env | grep CACHE

複数の命令を組み合わせる

複数の命令は組み合わせることでレイヤーの数を最小限に抑えることができます。

たとえば上述のDockerfileではRUN命令をアンパサンド2つ(&&)とバックスラッシュ(\)で1つにまとめています。

# RUN go install github.com/cosmtrek/air@latest
# RUN go install github.com/go-delve/delve/cmd/dlv@latest

RUN go install github.com/cosmtrek/air@latest && \
    go install github.com/go-delve/delve/cmd/dlv@latest

複数行にまたがる引数は英数順に並べ替えることでメンテナンス性が向上し、重複を防ぐことができます。

RUN --mount=type=cache,target=/go/pkg/mod/ \
    go build \
    -ldflags="-s -w" \
    -o golang-app \
    -trimpath

マルチステージビルドを使用する

マルチステージビルドを使うことで、本当に必要なものだけを最終イメージに含めることができます。

マルチステージビルドは複数のFROM命令を記述することで使用できます。上述のDockerfileの場合は、base, dev, builder, runnerの4ステージで構成されています。

base, dev, runnerステージのイメージサイズを比べてみたいと思います。

$ docker images     
REPOSITORY                   TAG               IMAGE ID       CREATED          SIZE
golang                       runner            522b09cddc5b   27 seconds ago   1.39MB
golang                       dev               4d8bd97eff7b   2 hours ago      1.1GB
golang                       base              186cddd98d38   2 hours ago      770MB

devステージのイメージサイズは、Go言語の依存関係のインストールに加え、開発用ツールのインストールなどを行なっているため1GBを超えています。

それに対してrunnerステージのイメージサイズは約1.4MBとかなり小さいことがわかります。

非ルートユーザーを使用する

コンテナが攻撃されたときのリスクを最小限に抑えるため、可能な限り非ルートユーザーでコンテナを実行するようにします。

USER scratch

scratchイメージをベースにしたコンテナを非ルートユーザーで実行する方法は、Non-privileged containers based on the scratch imageを参考にしました。

ホット リロード・デバッガーツールを活用する

Go言語にもnodemonのようにホット リロードを提供してくれるAirというツールがあります。devステージのCMD命令で実行しているのがAirです。.air.tomlファイルはサンプルをそのまま使用しました。

より詳細な情報はAirのGithubリポジトリを確認してください。

DelveはGo言語のデバッガーツールです。ステップ実行などさまざまなデバッグ機能を提供してくれます。

より詳細な情報はDelveのGithubリポジトリを確認してください。

イメージの脆弱性を確認する

脆弱性検出ツールを使うことでDockerイメージの脆弱性をスキャンできます。

脆弱性検出ツールの1つにGrypeがあります。今回はこのツールを使用してDockerイメージの脆弱性を検査してみたいと思います。

Tutorial: Get started with Gorsc.io/quoteパッケージを使用したコードを使います。

ビルドしたrunnerステージのイメージをGrypeで脆弱性スキャンしてみます。

$ grype golang:runner --scope all-layers
NAME               INSTALLED                           FIXED-IN  TYPE       VULNERABILITY        SEVERITY 
golang.org/x/text  v0.0.0-20170915032832-14c0d48ead0c  0.3.7     go-module  GHSA-ppp9-7jff-5vj2  High      
golang.org/x/text  v0.0.0-20170915032832-14c0d48ead0c  0.3.8     go-module  GHSA-69ch-w2m2-3vjp  High      

golang.org/x/textパッケージに脆弱性があることがわかりました。0.3.8以上で修正されたとのことなので、パッケージのアップデートを行ない、再度脆弱性をスキャンします。

$ grype golang:runner --scope all-layers
No vulnerabilities found

golang.org/x/textパッケージをアップデートすることで脆弱性は見つからなくなりました。

Grypeのより詳細な情報はGithubリポジトリを確認してください。

まとめ

今回の記事では、Dockerのbest practicesを参考にGo言語用のDockerfileを作成してみました。

これがGo言語用Dockerfileの1つの例になれば幸いです。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4