DockerでGoアプリケーションをコンテナ化するためのベストプラクティス
Jekayin-Oluwa Olabemiwo
2022年3月23日
0 分で読めますGoアプリケーションとコンテナは相性抜群です。Goのアプリケーションバイナリはサイズが小さく、DockerやKubernetesが得意とするマイクロサービスのデプロイ戦略に最適です。ただし、この相乗効果にも課題はあります。コンテナイメージに簡単に入り込むセキュリティ上の落とし穴を避けるには、コンテナのベストプラクティスと重要な概念を理解することが大切です。
この記事では、サンプルのGoアプリケーションをコーディングし、安全にコンテナ化して実行する方法を学びます。
前提条件
このチュートリアルを進めるには、DockerとGoがマシンにインストールされており、両方の基本的な知識が必要です。Dockerの入門手順とダウンロードについては、Dockerドキュメントの初期設定とセットアップページをご覧ください。Goの優れたチュートリアルは公式のGoドキュメントで確認できます。または、Linux向けのGoのインストールページに進んでください。
サンプルのGoアプリケーションを作成する
まず、Go APIを作成しましょう。最初に、マシン上でGoアプリケーションを配置するディレクトリに移動します。次に、「godocker」というディレクトリを作成します。このgodocker/ディレクトリで、次のコマンドを実行してGoモジュールを定義します。
次に、「godocker」ディレクトリ内に「main.go」というファイルを作成します。このファイルにAPIのコードを記述します。現在時刻をAPI経由で提供し、処理するロジックを追加しましょう。
次のコードをgodocker/main.goに入力します。
このコードは、httpパッケージのTimeFormat変数を使って、API経由で現在時刻を提供します。次に、NewServeMuxライブラリをmuxとして定義し、HTTPサーバーを設定します。最後に、/timeエンドポイントを登録してAPIで現在時刻を返し、/timeエンドポイントのレスポンスをJSONとしてエンコードします。
簡単にテストしてみましょう。シェルターミナルで次のコマンドを実行してアプリケーションを起動します。
次のような出力が表示されます。
次に、別のターミナルウィンドウでcURLを使ってAPIをテストします。ターミナルで次のコマンドを入力してください。
次のような出力が表示されます。
Dockerfileを準備する
Dockerfileには、アプリケーションをコンテナとしてパッケージ化してデプロイするための一連の指示が記述されています。このセクションではDockerfileを作成し、サンプルアプリケーションをコンテナとしてパッケージ化するために使える指示をいくつか確認します。
Dockerの構文バージョンを指定する
まず、構文ディレクティブを追加します。godocker/ディレクトリに「Dockerfile」という新しいファイルを作成し、最初の行に次のコードを入力します。
構文ディレクティブでは、Dockerfileのビルドに使用する構文の場所を指定します。このコード行では、Dockerfileの構文の場所をdocker/dockerfile:1として定義しています。これは最新リリースの構文バージョンです。Dockerは、Buildkitバックエンドを使ってDockerfileをビルドする前に、構文バージョンを確認します。
この行がファイルの先頭でコメントとして記述されていることを確認してください。。構文ディレクティブの後には、空行を入れるのが慣例です。
メモリ使用量の少ないベースイメージを継承する
次に、FROM命令で継承するDockerベースイメージを指定します。
次のコードをgodocker/Dockerfil`に追加します。
この命令を使えば、独自のDockerベースイメージをビルドする必要はありません。代わりに、Alpine Linux版のGoアプリケーション向け公式Dockerイメージを継承します。ベースイメージのGoバージョンは1.17です。alpineイメージは、ubuntuイメージのようなバリアントと比べて非常に小さいサイズです。
次に、各行の目的を簡単に説明するコメントをコードに含めながら、Dockerfileの残りを記述します。
godocker/Dockerfileを更新し、次のコードを記述します。
イメージをビルドする
Dockerfileが完成したので、docker buildコマンドを使ってDockerfileからDockerイメージをビルドしましょう。DockerはDockerデーモンを使ってイメージをビルドします。--tagオプション(-tと短縮可能)をdocker buildコマンドとともに使い、Dockerイメージに任意の名前を設定します。
ターミナルで次のコマンドを入力してください。
docker buildコマンドに.を付けると、ビルドコンテキストとして使われる現在のディレクトリでイメージがビルドされます。ビルドコンテキストに/パスを使用するのは避けてください。ソースコード全体がDockerデーモンに転送される可能性があります。
ビルド出力にFINISHEDという行が含まれ、次のように表示されます。
この出力は、godockerタグ付きのDockerイメージが完全にビルドされたことを示しています。
ローカルイメージの一覧を表示するには、ターミナルで次のコマンドを入力します。
次のような出力が表示されます。
出力にある、新しくビルドしたgodockerイメージのサイズは319MBです。シンプルなAPIアプリケーションとしては大きすぎるため、より軽量なイメージを作成できるようビルドを最適化する必要があります。次のセクションでは、マルチステージビルドの概念を実装して、軽量なビルドを実現します。
マルチステージビルドを使う
マルチステージビルドを使うと、前のセクションで紹介した単一ステージの方法と比べて、はるかに小さなイメージを作成できます。マルチステージビルドでは、イメージを使って各フラグメントをビルドし、それらを実行するために必要な要素だけで構成される小さなイメージにパッケージ化します。アプリケーションの実行に必要な最低限の要素までイメージを削減すれば、セキュリティの脆弱性が生じる可能性を抑えられます。これを実現するには、Dockerfileで複数のFROM命令を使います。
公式のscratchイメージを使う
scratchの公式Dockerイメージを継承すれば、空のイメージからビルドを開始できます。このセクションでは、マルチステージビルドでscratchイメージを使う方法を紹介します。
まず、アプリケーションのルートディレクトリに移動します。次に、「Dockerfile.multistage」というファイルを作成し、以下のコードを入力します。
このコードでは、公式のgolang:1.17-alpineイメージをベースイメージとして継承し、ステージ名をbuildに指定しています。次に、別のFROM命令を使ってマルチステージの仕組みを実装し、最初のステージでビルドしたバイナリを、2番目のステージの空のイメージにコピーします。
次に、新しいDockerfile.multistageファイルを使って、新しいイメージをビルドします。また、新しいイメージに「multistage」というタグを付けます。これにより、先ほどビルドしたイメージと区別できます。
ターミナルで次のコマンドを入力してください。
ビルドが正常に完了したら、ターミナルで次のコマンドを入力してイメージの一覧を確認します。
次のような出力が表示されます。
この出力から、godocker:multistageイメージとgodocker:latestイメージのサイズに大きな差があることがわかります。単一ステージのイメージは319 MBですが、マルチステージのイメージは6.1 MBで、明らかに改善されています。コンテナはすばやく起動する必要があるため、このような最適化はGoアプリケーションのコンテナ化に不可欠です。
コンテナをデプロイする
パフォーマンスと効率の最適化に加えて、コンテナを安全に実行できるよう、最適なデプロイ方法についても考える必要があります。このチュートリアルでは、この段階でいくつかのベストプラクティスを実装します。
root以外のユーザーとして実行する
最小権限の原則に基づき、システムリソースへのアクセスを制限する必要があります。GoのDockerコンテナはアプリケーションコンテナであり、root権限で実行する必要はありません。そのため、セキュリティを強化するため、Dockerfileでアクセス権を制限した新しいユーザーとグループを作成しましょう。
root以外のユーザーを作成するには、godocker/Dockerfile.multistageの最初のFROM命令の直後に、次の行を追加します。
この命令では、ARGキーワードでUSERNAMEとPASSWORDの引数を設定し、続いてRUN adduser命令でユーザーを作成します。
次に、最初のステージからユーザーの詳細をコピーし、2番目のステージに適用する命令を実装します。
次の例で太字になっているコードをgodocker/Dockerfile.multistageの2番目のステージに追加します。ファイルの末尾は次のようになります。
Kubernetesでは、securityContextフィールドでrunAsuser: UIDを指定できます。Podのセキュリティコンテキストを設定する方法については、Kubernetesのドキュメントをご覧ください。
読み取り専用のルートファイルシステムで実行する
アプリケーションのセキュリティを高めるもう1つの方法は、読み取り専用のファイルシステムでコンテナを実行することです。コンテナ内のファイルシステムを読み取り専用にするには、docker runコマンドに読み取り専用フラグを指定します。
ターミナルで次のコマンドを入力してください。
Linuxのケイパビリティを削除または拒否する
Linuxのケイパビリティとは、Linuxの使用時に有効化または無効化できる権限の集合です。コンテナはLinuxのバリアントをベースとしているため、これらのケイパビリティを使ってアプリケーションのセキュリティを高めることもできます。一部のケイパビリティを削除すると、コンテナのリスクを軽減できます。
このチュートリアルでは、setuid以外のすべてのケイパビリティを削除しましょう。
ターミナルで次のコマンドを入力してください。
CPUとメモリの使用量を制限する
docker runコマンドを使うと、Dockerコンテナがホストマシンのリソースを使用する量を制限できます。Dockerでは--cpusフラグを使います。
たとえば、コンテナが単一CPUの50%を超えて使用しないようにするには、Docker CLIで次のコマンドを入力します。
2つのCPUを使う必要がある場合は、Docker CLIで次のコマンドを使って使用量を制限できます。
Dockerコンテナのメモリ使用量を1024MBに制限するには、次のようにdocker runコマンドを使います。
このコマンドでは256MBのメモリ領域の上限も設定されます。Dockerがホストのメモリ不足を検出すると、この上限が適用されます。
まとめ
この記事では、Goアプリケーションをセットアップし、Dockerでコンテナ化する手順を紹介しました。また、パフォーマンスを最適化するマルチステージビルドを実装し、空の公式Dockerイメージからビルドする方法や、コンテナのビルドとデプロイに役立つベストプラクティスについても説明しました。これらのガイドラインは、DockerでGoのWebアプリケーションをコンテナ化する際に、効率性、セキュリティ、メモリ管理を強化するための出発点となります。
セキュリティのベストプラクティスについて詳しく知りたい方は、リソースセンターのSnyk Learnをご覧ください。
