Koでコンテナイメージを簡単に作成
2022年10月10日
0 分で読めます以前の記事では、Google Open SourceグループのJibツールを使ってJavaアプリケーションのコンテナイメージを構築する方法、そしてその理由について紹介しました。Jibは、Dockerのようなコンテナランタイムを必要とせず、ベストプラクティスに準拠した、スリムでJVMベースのOCI準拠イメージを構築します。また、Dockerfileの作成と管理も不要になります。では、Goアプリケーションを構築する場合はどうでしょうか。Goにも同様に使える、Koというオープンソースツールがあります。
注:この記事の執筆中に、KoプロジェクトはGoogleが所有するGitHubリポジトリから、独立したトップレベル組織「ko-build」へ移行しました。この変更を反映するため、記事内の表記を更新しましたが、オンラインやモジュール名には、しばらく「Google Ko」という表記が残っている場合があります。いずれも同じプロジェクトですのでご安心ください。この移行に至るほど成功を収めたKoプロジェクトに、お祝いを申し上げます。
この記事では、Koを使ってDockerfileなしでコンテナイメージを構築する方法、SBOM、Kubernetesとの統合について解説します。
Koとは?
Koは、開発プロセスで現在goコンパイラを実行している箇所に代わって使うことを想定した、単一バイナリのコマンドラインツールです。アプリケーションをコンパイルするだけでなく、アプリケーションをインストールした非常に軽量なコンテナイメージも生成します。Jibと同様に、設定や実行方法に応じて、イメージをレジストリにプッシュするか、ローカルのDockerイメージキャッシュに格納します。
Koにはさらに、ソフトウェア部品表(SBOM)の作成やKubernetesとの統合に関する機能もあり、反復的な開発とデプロイのプロセスをとてもシンプルにできます。
Koが解決しようとしている課題
使用する言語を問わず、多くのプログラマーはコンテナの構築に慣れておらず、イメージのビルドについて次のような疑問を抱きがちです。
どのベースイメージを使えばよいのか。組織のポリシーに準拠しているのか。
レイヤーの肥大化を抑えるには、コマンドをどう組み合わせればよいのか。
アプリケーションを実行するために、アプリのどの部分をイメージにコピーすればよいのか。
イメージのビルドにはどのツールを習得する必要があるのか。(Docker?Buildah?BuildKit?)
組織の要件に応じて、含めるべき標準アノテーションはあるのか。
アプリケーションアーキテクトやリーダーは、ガバナンスや標準の実装をチームが容易に遵守できるようにしたいと考えています。しかし、各チームが独自に作り上げてきたDockerfileのパターンを使っていると、一貫した運用を維持するのは困難です。
セキュリティチームは、イメージに何が含まれるか、また構築にどのツールが使われるかにも大きな関心を寄せています。たとえば、CIビルドノードにDockerエンジン、つまりホストマシンのdocker.sockを公開すると、ビルド環境にそのノード上で高い権限を与えることになりかねません。
「[Docker]デーモン…には、ビルドやレジストリとのやり取り以外にも多くの機能があります。追加のセキュリティツールがなければ、このマシンでdocker buildを実行できるユーザーは、docker runを使って好きなコマンドを実行することもできます…。好きなコマンドを実行できるだけでなく、この権限を悪意ある行為に利用された場合、誰が実行したのかを特定するのは困難です。」
— Liz Rice著『Container Security』第6章
一方で、新しいビルドツールの導入は複雑になり、ビルドシステムの担当者にとって学習の負担が増す可能性があります。Koは、開発者に優れた体験を提供しながら、これらの課題それぞれに対処することを目指しています。
Koを使ったイメージのビルドと、使わない場合の比較
Koが上記の課題にどう対処するのかを説明するため、まず既存のGo + Dockerによるビルド手順と比較してみましょう。
Koを使わずにイメージをビルドする
この例では、Goの定番チュートリアルアプリケーションである「Hello World」のWebアプリをビルドします。
1.空のディレクトリに、次の内容をhello.goとして作成します。
2.よく使われるパターンなので、Dockerfile内でバイナリをビルドします。
これはマルチステージビルドです。最初のステージはbuildという名前で、公式golangイメージをベースにしています。このステージでは、アプリの内容(現時点ではhello.goファイルのみ)をイメージ内の/go/srcにコピーし、静的バイナリを生成するために必要なパラメーターを指定してgo buildツールを実行します。
2つ目のステージでは、Google Distrolessプロジェクトのstatic:nonrootベースイメージを使い、デフォルトユーザーをnonrootに設定します。buildステージからhelloバイナリをルートディレクトリにコピーし、公開するポートのメタデータを設定して、コンテナ起動時にデフォルトで./helloが実行されるようにします。
なぜ2つのステージを使うのでしょうか?
Dockerfile内でアプリケーションをビルドするのは一般的な方法です。これにより、開発者のワークステーションでも自動ビルドでも、適切なバージョンのコンパイラが使われることが保証されます。しかし、ビルドに使用したイメージをそのままデプロイするのはよくある間違いです。コンパイラなどの不要なツールだけでなく、アプリケーションのソースコードまでデプロイ先のイメージに含まれてしまうため、セキュリティリスクとなります。攻撃者が脆弱性を悪用したり、イメージを入手したりした場合、攻撃を拡大するために利用できる情報やツールが数多く手に入ります。distroless/static:nonrootベースイメージを使うことで、デフォルトユーザーがroot以外に設定された、極めて最小限のファイルシステムを実現できます。
3.Dockerfileの準備ができたら、docker buildコマンドを使ってイメージをローカルキャッシュに作成し、タグを付けます。
注:実行しているDockerのバージョンやBuildKitの拡張機能が有効かどうかによって、出力は多少異なる場合があります。
4.他の人がこのイメージを使えるようにするには、レジストリリポジトリにプッシュする必要があります。この例では、DockerHubのregistry:2イメージを使って、ワークステーションのポート5000でローカルリポジトリを実行しています。(そのため、イメージにlocalhost:5000/のプレフィックスを付けています。)
Dockerを使ったレジストリへのプッシュは、docker pushコマンドで簡単に行えます。
注:マネージドレジストリの場合は、先にdocker loginで認証する必要があります。
5.最後に、イメージを実行するにはdocker runか、適切なkubectlデプロイを使います。ここでは、よりシンプルな前者を使います。
次に、シンプルなcurlコマンドでアプリをテストします。
ここまでの手順を図にすると、次のようになります。

それでは最初に戻り、Koを使う場合の流れを見てみましょう。
Koを使ってイメージをビルドする
1.空のディレクトリに、同じhello.goファイルを用意します。
2.次に、イメージレジストリを環境変数に設定し、ko buildを実行します。
このコマンドは、次の処理を行います。
アプリケーションを静的バイナリにコンパイル
適切な形式のイメージに格納
レジストリにプッシュ
これらの作業にDockerfileもコンテナランタイムエンジンも必要ありません。さらに、出力にはSBOMが作成されて公開されたことも示されています。これについては後ほど説明します。
注:ここでのイメージタグにはmd5ハッシュが含まれています。デフォルトでは、Goアプリケーションのインポートパスがハッシュになります。この例には完全なGoモジュールがないため、ハッシュはhello.goから取得されます。タグの詳細はKoのドキュメントをご覧ください。
3.次に、docker runでイメージを実行します。
…そしてcurlでテストします。
Koのパイプラインを図にすると、次のようになります。

イメージの作成、構築、プッシュという負荷の高い作業をKoに任せることで、必要な手順を減らせました。また、ビルド時のコンテナランタイムエンジンへの依存や、Dockerfileに必要だった専門知識とメンテナンスの負担も軽減されます。
賢いデフォルト設定と、決定論的な特化設定
Koのユーザーは、オープンソースコミュニティが蓄積してきたベストプラクティスを活用できます。しかし、組織独自の標準がそれらに適合しない場合はどうでしょうか。そんなときに便利なのが、Koのコマンドラインオプションやko.yaml設定ファイルです。
カスタムベースイメージ
たとえば、会社でGoアプリケーションのベースとして使用する特定のイメージが決められているとします。トップレベルディレクトリにあるko.yamlファイルに、そのイメージを値としてdefaultBaseImage:の行を追加するだけです。
Goコンパイラのフラグ
元の例のようにGoコンパイラの-ldflagsを明示的に指定するには、同じko.yamlファイルにbuilds:エントリを追加し、Koのドキュメントに記載されている適切な設定を指定します。
Dockerラベル
Dockerラベル(OCIアノテーションとも呼ばれます)は、イメージにメタデータを付与する方法です。イメージのソースリポジトリや作成したCIビルドなどの情報や、チームが埋め込みたいその他のデータを記録できます。イメージラベルの詳細と利用すべき場面については、Dockerラベルの使い方と活用するタイミングに関する私のブログをご覧ください。
Koでは、コマンドラインの--image-labelフラグを使ってラベルを追加できます。
注:この記事の公開時点では、.ko.yaml設定ファイルからイメージラベルを指定する方法はありません。ただし、この機能の追加を求めるissueが公開されています。
Koのその他の便利な機能
イメージのSBOM
前述のとおり、Koは新しいコンテナイメージのSBOMを自動的に生成してプッシュします。デフォルトの形式はSPDXですが、コマンドラインで--sbom=cyclonedxフラグを渡せばCycloneDXも利用できます。--sbom=noneを指定して、この機能を無効にすることもできます。これらの設定などについてはKoのドキュメントをご覧ください。
SBOMとセキュアなサプライチェーンの構築における役割について詳しく説明することは、この記事の範囲を超えます。詳細を知りたい方は、オープンソースのサプライチェーンセキュリティに向けたSBOMの作成をご覧ください。
Kubernetesとの統合
Kubernetesクラスター上でイメージをテストする必要がある場合、次のような手順を何度も繰り返す手間に悩まされたことがあるでしょう。
イメージをビルドする
レジストリにデプロイする(または、別の方法でKubernetesクラスターに読み込む)
新しいイメージタグでデプロイメントYAMLを更新する
kubectlで再デプロイする
Koを使えば、これらすべての手順をko applyという1つのコマンドにまとめて自動化できます。デプロイメントYAMLのイメージタグを、専用形式のko://に置き換えるだけで、ko applyが上記の処理をすべて自動で実行します。デプロイ先は、kubectlの設定コンテキストで現在選択されているクラスターです。
たとえば、サンドボックスまたはローカルでKubernetesクラスターを実行し、開発を進めながら繰り返しデプロイしてテストする場合を考えてみましょう。「hello」アプリケーションをデプロイするためのマニフェストは次のとおりです。
image:の行にはko://が指定され、その後にGitHub上のアプリケーションのGoモジュールへのパスが続いていることがわかります。
あとはko applyを実行し、kubectlと同じように-fフラグでYAMLファイルを指定するだけです。
これでアプリケーションをテストし、変更を加え、必要なだけko apply -f myfile.yamlを再実行できます。面倒な作業はすべてKoが処理してくれます。
もちろん、次のような他のKubernetesコマンドも必要になるでしょう。
デプロイメントを削除する
deletekubectlやその他のツールに渡せるYAMLを生成するresolve
詳しいドキュメントについては、https://github.com/ko-build/ko#kubernetes-integrationをご覧ください。
Koを使用する際の課題
Koをワークフローに取り入れるメリットについてはすでにご紹介しました。導入を始める前に、Koの使用時に直面する可能性のある課題について説明します。
クロスプラットフォームに関する懸念事項
Linux以外のマシンで開発している場合、コンテナランタイムがないことで、作業が少し難しくなることがあります。コンテナベースのビルドイメージを使えば、ビルドツールだけでなくOSやそのライブラリも共通のベースビルドイメージにまとめられるため、開発用マシンのプラットフォームが本番環境と異なることを実質的に無視できます。
たとえば、上記のGoとDockerを使った例は、ほぼすべてのプラットフォームで動作します。どこでビルドするかにかかわらず、golangベースイメージには、Goビルダーが静的バイナリを作成するために必要なDebianベースのglibcライブラリが含まれているからです。一方、たとえばMacOSマシンで同じldflagsオプションを指定してKoの例を実行しようとすると、静的リンクに必要なライブラリが見つからないというエラーが発生します。
これは、MacOS/DarwinにはLinux向けの静的バイナリをクロスコンパイルするために必要なライブラリが含まれていないためです(少なくとも、この記事の執筆時点では)。Appleだけの問題ではなく、LinuxやWindows WLSのワークステーションでCPUアーキテクチャが異なる場合にも、同様の問題が発生することがあります。
スキルの衰退
適切に設計されたコンテナイメージの作成・保守に必要なスキルや知識は、貴重な資産です。それらを抽象化したり、不要にしたりするツールを使うと、問題発生時に対処する能力が低下する可能性があります。実際、チーム内でコンテナ技術を理解している人が少ないほど、実行時のトラブルシューティングや保守では、必要な経験を持つ人に頼る度合いが高まります。深刻な場合、燃え尽きや離職、サービス停止につながることもあります。
セキュリティへの油断
コンテナイメージの構築プロセスを完全に自動化し、開発者の手から切り離すと、イメージの脆弱性スキャンなどのプロセスが適切に実施されなくなるおそれがあります。更新されていないイメージやパッケージ、ライブラリなどに脆弱性が入り込み、アプリケーションが攻撃にさらされる可能性があります。そのため、次のような別の自動化手段を使って、スキャンを継続してください。
ビルドスクリプトやMakefile
Gitフック
CIのビルドステップ
イメージスキャンをまだ実施していない場合は、Snykの無料コンテナイメージスキャンをご利用ください。イメージ内の脆弱性を検出し、簡単に実行できる具体的な修正方法を提示します。
コンテナイメージをシンプルに
Koをはじめとする代替イメージ構築ツールを使うと、Goアプリケーションのコンテナイメージ作成を大幅に簡素化し、チームでのイメージ構築を効率化・標準化できます。特に大きなメリットが得られるのはCIツールです。ビルド環境にコンテナランタイムを公開する必要がなくなり、SDLCにおける特権アクセスを狙った攻撃経路を大幅に減らせます。Koのようなビルドツールを使うかどうかにかかわらず、チームがコンテナ技術と、イメージ構築に使うプロセスやツールに伴うセキュリティ上の影響を十分に理解していることを確認してください。
CTFを始めよう
オンデマンドのバーチャル入門ワークショップで、CTFチャレンジの解き方を学びましょう。



