Amazon Web Services ブログ

Cloud Native Buildpacks による AWS CodeBuild と AWS CodePipeline を使ったコンテナイメージの作成

この記事は Creating container images with Cloud Native Buildpacks using AWS CodeBuild and AWS CodePipeline (記事公開日: 2021 年 10 月 6 日) を翻訳したものです。

Amazon Elastic Container Service (Amazon ECS)Amazon Elastic Kubernetes Service (Amazon EKS)、またはその他のコンテナオーケストレーターを使用している組織は、迅速に稼動させるためのよくある課題に直面しています。それは、アプリケーションのソースコードをコンテナイメージに迅速かつ効率的にパッケージ化するにはどうすればよいかということです。この “Source to Image” というという道のりは、組織がコンテナ技術を導入したばかりであるか、数百のワークロードにスケールアップしようとしているかに関わらず、さまざまな場面で課題となります。

今日使用されているコンテナオーケストレーションエンジンに関わらず、コンテナイメージを作成するための一般的なメカニズムは、引き続き Dockerfile です。Dockerfile は、ベースイメージとその上で実行する一連の命令を指定する単純なテキストファイルです。このフォーマットは非常に人気がありますが、その理由は、おそらく最初の学習コストとフォーマットの透明性のためです。

Open Container Initiative (OCI) のイメージ仕様によってコンテナイメージのフォーマットが標準化されたことで、Dockerfile なしでコンテナイメージをビルドする新しいメカニズムへの扉が開かれ始めています。コンテナオーケストレーターが標準化された OCI 仕様をサポートすることで、Dockerfile の代わりとなるツールは OCI 準拠のコンテナイメージを生成することができ、Docker、Kubernetes、Amazon ECS などで引き続き使用できます。

Dockerfile の代替ツールの 1 つは、Cloud Native Buildpacks (CNB) です。CNB は Cloud Native Computing Foundation (CNCF) の Incubating プロジェクトで、Dockerfile を使わずにアプリケーションのソースコードを OCI 準拠のコンテナイメージに変換する仕組みを提供しています。Buildpack のコンセプトは、歴史的には、Heroku や Cloud Foundry などの PaaS (Platform as a Service) オファリングに関連付けられてきました。最新の Buildpack は、これらのプラットフォームのビルドメカニズムを切り離して、より広範なコンテナコミュニティで利用できるようにしたものと考えることができます。このプロジェクトが成熟するにつれ、多くのベンダーやツールに採用されるようになりました。

CNB プロジェクトの主な利点は、コンテナイメージをビルドするために Dockerfile を作成する必要がなくなることです。アプリケーション開発チームは、コンテナプラットフォームでコードをより迅速に実行できるようになります。これにより Heroku や Cloud Foundry のような PaaS オファリングの側面を提供しつつ、柔軟にコンテナベースのサービスにデプロイすることができます。この技術は言語に依存せず、Java、.NET Core、Ruby、Node.js、Go、Python など、さまざまなプラットフォームをサポートするオープンソースの Builder があります。

また、CNB プロジェクトを活用することで、組織はコンテナイメージをビルドするための標準的なプロセスを、迅速に導入することができます。これに代わる方法は、組織がイメージを作成するための独自のプロセスを決定することですが、より分散型の組織では、異なるチーム間でコンテナイメージ作成のプロセスが分断され、同様のソリューションを解決および維持するためのオーバーヘッドが増える可能性があります。

CNB プロジェクトは、次のような多くの利点を約束します。

  • モジュール化され拡張可能なパターンで複数の Buildpack を組み合わせることで、再利用が促進されます。
  • コンテナイメージのレイヤーを作成するための代替メカニズムにより、レイヤーの変更が少なく、帯域幅の使用効率が向上します。
  • リベースによって多数のコンテナイメージのベースイメージを大規模に更新できます。
  • イメージにインストールされるパッケージを最小限に抑えることができ、各イメージに含まれる部品表 (Bill of Materials, BOM) を作成することもできます。

この記事では、Dockerfile を使用せずにアプリケーションのソースコードから直接コンテナイメージをビルドする、AWS サービスを使用したリファレンスパイプラインを提供します。これにより、Cloud Native Buildpacks プロジェクトを AWS CodePipeline および AWS CodeBuild と統合して、コンテナイメージをパッケージ化して Amazon EKS や Amazon ECS などのコンテナベースのサービスにデプロイするデリバリーパイプラインを作成する方法を検討できます。

Cloud Native Buildpacks のコンセプト

Cloud Native Buildpacks を使ってコンテナイメージをビルドすることは、Dockerfile のような他のツールを使うこととは異なる体験です。ここでは、CNB のエコシステムと、基本的なコンセプトについて見てみましょう。

CNB プロジェクトによって導入された主要なコンセプトがあり、公式サイトで十分に文書化されています。今回のブログ記事では、特に注目すべき点をいくつか挙げてみます。

  • アプリケーションイメージ: 最終製品として生成され、Docker、Kubernetes、Amazon ECS などで実行可能な OCI 準拠のコンテナイメージ。
  • Buildpack: 最終的な実行イメージにアーティファクトや設定を提供することを目的として、アプリケーションのソースコードを操作する作業単位。
  • Builder: 1 つ以上の Buildpack、ビルドステージと生成される最終イメージの両方のベースイメージ、および CNB ツールで使用できるようにするための設定やその他のメタデータがパッケージされたイメージ。 Builder は、コミュニティが作成した Buildpack をパッケージ化して配布するために使用されます。さらに、組織が独自の Buildpack のセットを構築してパッケージ化する機能を提供するためにも使用されます。
  • ビルドイメージ: Buildpack が実行されるビルド環境を構築するために使用されるベースイメージ。
  • 実行イメージ: 最終的なアプリケーションイメージに使用される最小限のベースイメージ。
  • Pack CLI: アプリケーションのソースコードと Builder を使用して、最終的なアプリケーションイメージを生成するツール。

これらのコンセプトがどのように相互作用するかを以下に示します。

Builder 例のコンセプト

エコシステムは大きく 3 つのパートに分けることができます。

  • Cloud Native Buildpacks 仕様pack CLI、CNB 仕様の一部のリファレンス実装を含む、CNCF の Cloud Native Buildpacks プロジェクト。
  • オープンソースプロジェクトおよび Paketo や Heroku などの商用ベンダーによって、作成および配布される Builder と Buildpack。
  • CNB プロジェクトとインテグレーションし、Builder や Buildpack を使用して “Source to Container” 機能を提供する HerokuSpring BootGitLab Auto DevOpsHashiCorp Waypoint などのオープンソースプロジェクトやベンダー。

このブログ記事では、pack CLI を介して、CNB プロジェクト、および Paketo プロジェクトのオープンソースの Builder と Buildpack の実装を操作します。このブログ記事に関連する CloudFormation テンプレートは、GitHub にあります

ソリューションの概要

Buildpack ソリューションの概要

プロセスのステップは以下の通りです。

  1. Java ベースの Spring Boot アプリケーションのサンプルのソースコードが、AWS CodeCommit のリポジトリに配置されます。
  2. リポジトリへのコミットにより、CodePipeline によるビルドプロセスのオーケストレーションがトリガーされます。
  3. CodeBuild プロジェクトが pack CLI を介して Cloud Native Buildpacks を呼び出し、コンテナイメージを作成します。
  4. イメージは、Amazon Elastic Container Registry (Amazon ECR) のリポジトリにプッシュされます。
  5. 作成されたイメージを参照する、サンプルの後続の CodeBuild プロジェクトが提供されます。

前提条件

このソリューションの前提条件は以下の通りです。

パイプラインをデプロイする

パイプラインをデプロイするには、以下の手順を実行します。

  1. CloudFormation のテンプレートとパイプラインのコードを GitHub リポジトリからダウンロードします。
  2. まだログインしていない場合は、AWS アカウントにログインします。
  3. CloudFormation コンソールで、Create Stack を選択します。
  4. CloudFormation パイプラインテンプレートを選択します。
  5. Next を選択します。
  6. スタックの名前を入力します。
  7. デフォルトのスタックパラメータを使用することができますが、微調整も可能です。
    • Builder の下で、コンテナイメージの構築に使用する CNB Builder を指定します。デフォルトでは、Paketo Buildpacks Base Builder を使用します。

パイプラインを調べる

CloudFormation スタックのデプロイが完了する頃には、CodePipeline はすでに実行されているか、おそらく完了しているでしょう。デプロイされた CloudFormation テンプレートには、Spring Initializr を使用して新しい Java ベースの Spring Boot アプリケーションを初期化し、CodeCommit リポジトリにコミットするカスタムリソースが含まれています。これによって動作するアプリケーションがすぐに準備でき、テストすることができます。

このアプリケーションを CNB と互換性のあるものにするために大変なことは何もありません。ここでは Maven ベースの Java プロジェクトを使用していますが、代わりに npm ベースの Node.js アプリケーションや Go アプリケーション、あるいはその他のサポートされているプラットフォームを使用することもできます。

Maven ベースの Java プロジェクトのファイル群

ソースコードに Dockerfile がないことに注意してください。通常は Dockerfile をアプリケーションをコンテナ化するために使用します。代わりに、CNB が我々のソースコードから、適切なコンテナイメージを構築する責任を負います。

最初の実行が完了するのを待つ間に、CodePipeline の構造を調べましょう。

CodePipeline の構造

パイプラインが完了すると、AWS CloudFormation スタックによって作成された Amazon ECR リポジトリにイメージがプッシュされます。

Amazon ECR のリポジトリ

また、CNB は OCI に準拠したイメージを作成しているため、Amazon ECR が提供するコンテナイメージスキャン機能などを活用して、イメージの脆弱性を検出することができます。

Cloud Native Buildpacks の動作

Dockerfile を使用する場合とは異なり、CNB はほとんど指示することなく多くのことを処理してくれました。以下のような OCI 準拠のコンテナイメージが Amazon ECR にプッシュされ、デプロイ可能な状態です。

  • Java Runtime Environment (JRE) などの必要なミドルウェアが含まれています。
  • 我々のアプリケーションフレームワーク (Spring Boot) のための特定のカスタマイズがなされています。
  • (実行可能ファイルや JAR ファイルのような配布可能なパッケージではなく) アプリケーションのソースコードのみの提供によって、使い捨てのビルドコンテナで作成されました。
  • デフォルトでセキュアであり、非 root ユーザーで実行され、最小限のパッケージがインストールされています。

Buildpack がどのようにコンテナイメージを生成したのかをよく理解するために、出力ログの一部を分解して、何が起こったのかを見てみましょう。

===> DETECTING
7 of 18 buildpacks participating
paketo-buildpacks/ca-certificates   2.1.0
paketo-buildpacks/bellsoft-liberica 7.0.0
paketo-buildpacks/maven             4.0.0
paketo-buildpacks/executable-jar    4.0.0
paketo-buildpacks/apache-tomcat     4.3.0
paketo-buildpacks/dist-zip          3.0.0
paketo-buildpacks/spring-boot       4.0.0

最初に行われたのは検出フェーズで、アプリケーションをビルドするために Builder のどの Buildpack を実行すべきかが決定されました。アプリケーションは、Maven ベースの Java アプリケーションとして正しく認識されました。さらに詳細な分析を行い、アプリケーションの構築に Spring Boot フレームワークが使用されていることも認識されました。これにより、Buildpack は生成するイメージをさらにチューニングすることができます。

===> BUILDING
Paketo BellSoft Liberica Buildpack 7.0.0
  https://github.com/paketo-buildpacks/bellsoft-liberica
  Build Configuration:
    $BP_JVM_VERSION              11.*            the Java version
  Launch Configuration:
    $BPL_JVM_HEAD_ROOM           0               the headroom in memory calculation
    $BPL_JVM_LOADED_CLASS_COUNT  35% of classes  the number of loaded classes in memory calculation
    $BPL_JVM_THREAD_COUNT        250             the number of threads in memory calculation
    $JAVA_TOOL_OPTIONS                           the JVM launch flags
  BellSoft Liberica JDK 11.0.9: Reusing cached layer
  BellSoft Liberica JRE 11.0.9: Contributing to layer
    Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.9+11/bellsoft-jre11.0.9+11-linux-amd64.tar.gz

最初に実行された Buildpack は、アプリケーションのビルドと実行に必要な Java Development Kit (JDK) と Java Runtime Environment (JRE) をインストールしました。Java のベストプラクティスに従うため、Buildpack は JDK をアプリケーションがビルドされパッケージ化される間のみ利用されるビルドコンテナにインストールすることで、その後は破棄されるようにします。JRE は生成される最終イメージにインストールされ、実行時に Java コードの実行に使用されます。

このケースでは、Paketo Builder はデフォルトで Bellsoft Liberica ディストリビューションを使用していますが、AdoptOpenJDK のような別のディストリビューションに変更することもできます。

Paketo Maven Buildpack 4.0.0
  https://github.com/paketo-buildpacks/maven
  Build Configuration:
    $BP_MAVEN_BUILD_ARGUMENTS  -Dmaven.test.skip=true package  the arguments to pass to Maven
    $BP_MAVEN_BUILT_ARTIFACT   target/*.[jw]ar                 the built application artifact explicitly.  Supersedes $BP_MAVEN_BUILT_MODULE
    $BP_MAVEN_BUILT_MODULE                                     the module to find application artifact in
    Creating cache directory /home/cnb/.m2
  Compiled Application: Contributing to layer
    Executing mvnw -Dmaven.test.skip=true package
[INFO] Scanning for projects...
[...]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  19.546 s
[INFO] Finished at: 2020-10-30T17:32:45Z
[INFO] ------------------------------------------------------------------------
  Removing source code

JDK がインストールされたので、Maven Buildpack が実行され、ビルドコンテナ内で Java アプリケーションがパッケージ化されました。これによって、コンテナイメージを作成する前にアプリケーションをビルドするために必要なシステムの依存関係がほとんどなくなり、ローカルでも継続的インテグレーションツールでも、一貫してアプリケーションをビルドできるため便利です。同様のプロセスが Node.js の npm / yarn および Go Modules に対しても実行されます。

Paketo Executable JAR Buildpack 4.0.0
  https://github.com/paketo-buildpacks/executable-jar
    Writing env.launch/CLASSPATH.delim
    Writing env.launch/CLASSPATH.prepend
  Process types:
    executable-jar: java org.springframework.boot.loader.JarLauncher
    task:           java org.springframework.boot.loader.JarLauncher
    web:            java org.springframework.boot.loader.JarLauncher

Paketo Spring Boot Buildpack 4.0.0
  https://github.com/paketo-buildpacks/spring-boot
  Launch Helper: Contributing to layer
[...]

アプリケーションがビルドされた後に、他にもいくつかの Buildpack が実行され、さまざまなタスクを実行します。最も重要なのは、コンテナへの適切なエントリーポイントの設置です。これは、Spring Boot アプリケーションフレームワークを使用しているため、Executable JAR Buildpack によって自動的に行われます。

===> EXPORTING
Adding layer 'paketo-buildpacks/bellsoft-liberica:helper'
Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
[...]
Adding label 'org.springframework.boot.spring-configuration-metadata.json'
Adding label 'org.springframework.boot.version'
Setting default process type 'web'
*** Images (sha256:a60d8410d71851b99c3b66840a8d8876fe4dca5338e2da680aef8e62324025d2):
      785487814634.dkr.ecr.us-west-2.amazonaws.com/buildpacks-blog-imagerepository-urtzbez3ivqi:latest
      785487814634.dkr.ecr.us-west-2.amazonaws.com/buildpacks-blog-imagerepository-urtzbez3ivqi:ad96a48

すべての Buildpack が関連するレイヤーに貢献した後、pack CLI は最終的なイメージをアセンブルし、さまざまなメタデータをイメージラベルとして追加します。

Adding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'
Adding cache layer 'paketo-buildpacks/maven:application'
Adding cache layer 'paketo-buildpacks/maven:cache'
Successfully built image '785487814634.dkr.ecr.us-west-2.amazonaws.com/buildpacks-blog-imagerepository-urtzbez3ivqi:latest'

最後に、後続のビルドを高速化するためにキャッシュイメージが作成されます。このアプリケーションでは、Buildpack は JDK と Maven の依存関係をキャッシュしているため、変更が発生しない限り依存関係を再ダウンロードする必要はありません。

CodeBuild プロジェクト

少し時間を取って CodeBuild プロジェクトを調べ、何が行われているのかを理解しましょう。 以下は、CloudFormation で指定されている buildspec.yml です。

version: 0.2
env:
  variables:
    builder: "paketobuildpacks/builder:base"
    pack_version: "0.18.1"
    application_name: "default_application"
  exported-variables:
  # Exported the image tag to be used later in the CodePipeline
  - IMAGE_TAG

phases:
  install:
    commands:
    # Download the pack linux binary
    - wget -q https://github.com/buildpacks/pack/releases/download/v$pack_version/pack-v$pack_version-linux.tgz -O - | tar -xz
    - chmod +x ./pack
  pre_build:
    commands:
    # Log in to ECR
    - ECR_DOMAIN="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com"
    - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_DOMAIN
    # Set up some derived values for subsequent phases
    - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
    - ECR_REPOSITORY="$ECR_DOMAIN/$application_name"
    - IMAGE_TAG="$ECR_REPOSITORY:$COMMIT_HASH"
  build:
    commands:
    - |
      ./pack build --no-color --builder $builder \
      --tag $IMAGE_TAG $ECR_REPOSITORY:latest \
      --cache-image $ECR_REPOSITORY:cache \
      --publish

この CodeBuild プロジェクトは比較的シンプルで、いくつかのタスクに分割できます。

  1. pack CLI をダウンロードし、実行可能な状態にします。
  2. ローカルの Docker デーモンに Amazon ECR の認証情報を与え、CodeBuild がイメージをプルおよびプッシュできるようにします。
  3. ビルドのトリガーとなった Git コミットの最初の 7 文字を使って、コンテナイメージのタグを決定します。
  4. 適切な引数を指定して pack CLI を実行し、次のことを指示します。
    1. CloudFormation スタックのデプロイ時に指定された Builder を使用します。
    2. イメージに latest のタグと、(3) によって決定されたタグの両方を付与します。
    3. Maven の .m2 ディレクトリなど、アーティファクトをキャッシュするために使用できるキャッシュイメージを生成します。
    4. ビルドが完了すると、イメージを Amazon ECR に自動的にプッシュします。

また、プロジェクトは環境変数 IMAGE_TAG をエクスポートするように設定されています。これにより CodePipeline は環境変数を参照し、Amazon ECS や Amazon EKS へのデプロイメントをトリガーするなどのパイプラインの下流のアクションで利用することができます。

アプリケーションを変更する

アプリケーションのコードを変更したときに Buildpack のプロセスがどのように動作するかを調べるために、アプリケーションのソースコードを変更し、その変更を CodeCommit にプッシュして新しいビルドをトリガーしてみましょう。必要であれば、CodeCommit リポジトリをクローンして、ローカルマシンで変更を加えることができます。しかし、ここでは簡単にやるために、コンソールから直接変更を行います。

CloudFormation スタックによって作成された CodeCommit リポジトリに移動し、Add fileCreate file の順に選択します。

CodeCommit リポジトリのファイル追加オプション

新しいファイルを追加してリポジトリにコミットすることができるユーザーインターフェースが提供されます。ここでは、Spring Boot で配信できる index.html ページを追加しましょう。

リポジトリでのファイル作成

変更がコミットされると、CodePipeline は新しい実行を開始します。履歴から前の実行と比較してみましょう。

実行履歴の比較

アプリケーションのコードを変更しただけにもかかわらず、ビルドは約 50 パーセントも速くなりました。これは Buildpack の 2 つの側面によるものです。

  1. pack CLI は、できるだけ多くのレイヤーを再利用するために、新しいビルドを実行する前に、まず latest とタグ付けされた前回のイメージをプルします。これが、イメージに latest と Git コミットの両方をタグ付けすることが重要な理由です。
  2. buildspec でキャッシュイメージを設定したので、CodeBuild のような一時的な CI/CD システムであっても、ビルドをまたがって Maven パッケージなどの依存関係をキャッシュすることができます。

次に、異なるビルドから作成された 2 つのイメージのレイヤーを比較してみましょう。

2 つの異なるビルドのイメージレイヤーの比較

興味深いことに、後半の 2 つのイメージレイヤーが変更されていますが、後続のレイヤーの中には変更されていないものもあります。なぜこのようなことが起こるのでしょうか?Dockerfile では、レイヤーを変更すると、後続のすべてのレイヤーが再ビルドされ、プッシュされ、プルされなければなりません。このことは、CNB のもう 1 つの大きな利点を示しています。つまり、CNB のレイヤーの構築方法により、pack CLI はイメージの中間レイヤーを更新し、レイヤーの変更を連鎖的に行うのではなく、その変更だけをプッシュすることができるということです。これにより、ビルド時間や、Amazon ECR のようなコンテナリポジトリとの間でイメージをプッシュしたりプルしたりすることが、はるかに効率的になります。

クリーンアップ

今後の課金を避けるために、この記事の一部として作成されたリソースをクリーンアップしましょう。

  • CloudFormation スタックの出力タブで PipelineS3Bucket というキーの値を確認し、S3 コンソールでこの名前のバケットを開きます。
  • バケット内のすべてのオブジェクトを選択し、Delete を選択します。
  • プロンプトが表示されたら、permanent delete と入力し、Delete を選択します。
  • CloudFormation スタックの出力タブで EcrRepository というキーの値を確認し、Amazon ECR コンソールでこの名前のリポジトリを開きます。
  • リポジトリ内のすべてのタグを選択し、Delete を選択します。
  • プロンプトが表示されたら、delete と入力し、Delete を選択します。

完了したら、CloudFormation コンソールを開き、スタックを削除することで残りのリソースを削除します。

まとめ

この記事では、以下について紹介しました。

  • Cloud Native Buildpacks の基礎知識。
  • Cloud Native Buildpacks を AWS CodeBuild でコンテナイメージを作成するために使用する方法。
  • コンテナイメージをデプロイに使用するための、AWS CodePipeline によるプロセスのオーケストレーション。

このブログ記事は、Buildpack の利点とその仕組み、そしてプロジェクトを CodePipeline や CodeBuild などの他の AWS サービスと統合することについて、ほんの一部に触れたに過ぎません。

Buildpack の詳細については、buildpacks.io にアクセスし、最近開催されたバーチャル KubeCon イベントのこのセッションをご覧ください。

翻訳はプロフェッショナルサービスの杉田が担当しました。原文はこちらです。