Chapter 12

ベストプラクティス

ここだけは理解しよう

  • Image を小さく保とう
  • データの永続化戦略をしっかり考えよう
  • CI/CD
  • .dockerignore を利用しよう

参考

Docker development best practices
Dockerfileのベストプラクティス

Imageを小さく保つ

下記の観点でお話しします。

  • Image を小さくするメリット
  • イメージサイズを小さく保つため方法

■Image を小さくするメリット

イメージを小さくするとネットワーク上での引き込みが速くなり、
コンテナやサービスの起動時にメモリへのロードが速くなります。

■イメージサイズを小さく保つため方法

  • 適切なベースイメージを使いましょう

例えば、JDK が必要な場合は Ubuntu イメージに openjdk をインストールするのではなく、
公式の openjdk イメージをベースにすることを検討してください。

  • Multi-Stage Buildを使う

例えば、Maven イメージを使って Java アプリケーションをビルドし、
jar ファイルを軽量の alpine Linuxalpine で利用できます。

最終的なイメージには、ビルドだけに必要なライブラリと依存関係のすべてが含まれず、
アプリケーションを実行するために必要な依存関係だけを含む最小構成を作成できます。
*つまり Image を小さくできます。

  • レイヤーを減らしましょう

マルチステージビルドを含まないバージョンの Docker を使用する必要がある場合、
Dockerfile 内の個別の RUN コマンドの数を最小限にしましょう。
イメージ内のレイヤー数を減らすように努めてください。

これは、複数のコマンドを 1 つの RUN 行にまとめ、
シェルのメカニズムを使ってそれらを結合することで行うことができます。
以下の 2 つの断片を考えてみましょう。

こちらだと update のレイヤーと install のレイヤーができる。

Dockerfile
RUN apt-get -y update
RUN apt-get install -y python

こちらの方が updateinstall の 1 つのレイヤーを作ることができる。

Dockerfile
RUN apt-get -y update && apt-get install -y python
  • 共通コンポーネントのベースイメージを利用しましょう

共通点の多い複数のイメージを持っている場合は、
共有されているコンポーネントを使って独自のベースイメージを作成し、
それをベースに独自のイメージを作成することを検討してください。
プログラムで言う 継承 ですね。

メリットとしては下記になります。

共通のレイヤーを一度だけロードするだけで、それらはキャッシュされます。
これは派生イメージが Docker ホスト上のメモリをより効率的に使用し、
より速くロードできることを意味します。

本番環境のイメージを無駄なく、かつデバッグを可能にするため、
本番環境のイメージをデバッグイメージのベースイメージとして使用することを検討しましょう。
本番環境イメージの上に追加のテストやデバッグツールを追加できます。

  • 自動的に作成される最新のタグに頼らない

イメージを構築しアプリケーションを異なる環境にデプロイする場合は、
有用な情報をコード化し有用なタグを常に付けてください。

データの永続化戦略

下記の観点でお話しします。

  • 理由
  • 対策

アプリケーションデータをコンテナの書き込み可能なレイヤーに格納することはやめましょう。

■理由

コンテナのサイズを大きくなります。
I/O の観点からボリュームやバインドマウントを使用するよりも効率的ではありません。

■対策

  • ボリュームを使用してデータを保存しましょう

  • バインドマウントについて(適切なケース)

    • 開発中にソースディレクトリやコンテナにビルドしたばかりのバイナリをマウントしたい場合があります
    • 本番環境では、開発時にバインドマウントをマウントしたのと同じ場所にボリュームをマウントします

本番環境では、下記を行いましょう。

  • 機密性が高い:secret を使用しましょう
  • 機密性が低い:configs を使用しましょう

スタンドアローンコンテナを使用している場合、
これらのサービスのみの機能を利用できるように、
シングルレプリカサービスを使用するように検討してください。

CI/CD

ソースの変更を確認したりプルリクエストを作成したりする際、
Docker Hub や CI/CD パイプラインを使用して、
自動的にテストや Docker イメージのビルドやタグ付けを行いましょう。

イメージが本番環境にデプロイされる前、
開発チームや品質チームやセキュリティチームがプルリクのタイミングで、
テスト確認とイメージ作成できていることを確認した上でマージできます。

.dockerignoreの利用

Docker のビルド時に無視するファイル/ディレクトリを指定できます。
.dockerignoreは基本的に .gitignore と同じ書き方が可能です。

.dokcerignoreを利用する理由としては、下記になります。

  • .gitignore のようなコンテナ内に不要な情報を含まないようにする
  • node_modules のような上書きされると困るものを記述しないようにする

開発環境と本番環境

Development

  • バインドマウントを使用して、コンテナからソースコードにアクセスする
  • Docker Desktop for Mac または Docker Desktop for Windows を使用します
  • タイムドリフトを気にしないでください

Prodcution

  • コンテナデータを格納するためにボリュームを使用します

  • Docker エンジンを使用し可能であればユーザーズマッピングを使用して、
    Docker プロセスをホストプロセスからより分離します

  • Docker ホストと各コンテナプロセス内では常に NTP クライアントを実行し、
    すべてを同じ NTP サーバーに同期させます

  • swarm を使用する場合は、
    各 Docker ノードがコンテナと同じ時間ソースに時計を同期するようにしてください

Dockerfile

Imageを小さく保つ

Docker Image はレイヤーが少なくサイズも軽いものが良いものだとされています。
理由は下記となります。

  • レイヤー:増やすことはオーバーヘッドにつながります
  • サイズ:大きくすることで Image の pull の速度が遅くなります

最小構成

Docker は1 コンテナ 1 プロセスが原則です。

そのため、CentOS のベースイメージで、
Go の実行環境を作成して、MySQL を設定することは非推奨です。

復数のプロセスを使用したい場合はそれぞれコンテナに分け、
オーケストレーションツールを使用してコンテナを協調させて動かしましょう。
(引用:入門Docker)

上記のような Docker 利用をしていきましょう!
k8s ですね。

軽量なベースイメージ利用

コンテナを作ることが目的ではないので、
なるべく OS のイメージは小さいのを利用しましょう。
おすすめは下記になります。

  • Alpline
  • buster
  • stretch
  • nanoserver

ここら辺は知っていると便利です。
また busybox も知っていると非常に便利です。

build

下記の観点でお話しします。

  • キャッシュ戦略
  • Multi-Stage Build

キャッシュ戦略

■中間レイヤー
Docker Image は各コマンド毎に作成するキャッシュ。
ビルド後にコマンドの変更・ファイルの追加/更新など、
なにか変化を起こすと変化が起こったレイヤーの直前のキャッシュからビルドを実行します。

例えば
単純な依存ライブラリのインストールだけ行えば問題ないNode.jsのアプリケーションがあったとします。
開発中は頻繁にコードの変更を行うはずです。
コードの変更を行えばせっかく作成したキャッシュが効かなくなってしまい、ビルドのし直しになってしまいます。
単純な npm install が必要なアプリケーションであれば package.json と package-lock.json だけをコンテナ上へコピーして、
その後スクリプトのコピーを行うと高速なビルドを実現できるでしょう。
(引用:入門Docker)

キャッシュを無視した Dockerfile

FROM node:slim

WORKDIR /scripts

COPY . .

RUN npm install

CMD ["npm", "start"]

キャッシュを意識した Dockerfile

FROM node:slim

WORKDIR /scripts

COPY ./package.json ./package-lock.json /scripts/

RUN npm install

COPY . .

CMD ["npm", "start"]

Multi-Stage Build

■Multi-Stage Build

復数の Docker Image を作り、
最終的に任意のファイルだけを抽出して 1 つの Docker Image にします。

■例

Golang のようなビルドを行い成果物をバイナリとして出力する言語の場合、
最小限の OS と成果物のバイナリの 2 つだけで動作します。
この 2 つだけの最低限の環境を用意するために活躍するのが Multi-Stage Build です。

個人的には Docker の最小構成を作成するためにも Multi-Stage Build は必須だと思っています。

Dockerfile
# Build Layer
# buildという名前でgolangのビルドを行うDockerfileを記載します。
FROM golang:1.12-alpine as build

WORKDIR /go/app

COPY . .

RUN apk add --no-cache git \
  && go build -o app

# ----------------------------------------------
# Run Layer
# buildで作成したimageを実行するためのDockerfileを記載します。
FROM alpine

WORKDIR /app

# buildで記載したgolangのバイナリを取得します。
COPY --from=build /go/app/app .

RUN addgroup go \
  && adduser -D -G go go \
  && chown -R go:go /app/app

CMD ["./app"]