マルチステージビルドを完全に理解して Cloud Run のセキュリティを向上する

## はじめに
こんにちは、Datadog Japan で Sales Engineer をしている [**AoTo**](https://qiita.com/AoTo0330) です。

この投稿は **[<font color="#6495ED">AoTo Advent Calendar 2024</font>](https://qiita.com/advent-calendar/2024/aoto) 18日目**の記事です。

Cloud Run では OS レベルのセキュリティ向上のため、Google が提供している**ベースイメージの自動更新**が可能です。

本機能によって、Cloud Run にデプロイしているコンテナに Google Cloud の提供するベースイメージを利用することで、ダウンタイム・リビルドなしで48時間以内に脆弱性へ対応するパッチを Google Cloud が適用してくれます。

:::note info
本機能は [Google Cloud Next ’24 で Private Preview](https://cloud.withgoogle.com/next?session=DEV205) として発表され、[2024年8月21日に Public Preview](https://cloud.google.com/run/docs/release-notes#August_21_2024) となりました。
:::

公式ガイドでは、以下の2つの方法が説明されています。
 - [アプリケーションソース](https://cloud.google.com/run/docs/deploying-source-code)からのデプロイ
 - Dockerfile で [`FROM scratch`](https://hub.docker.com/_/scratch) を利用して一から構築する

https://cloud.google.com/run/docs/configuring/services/automatic-base-image-updates?hl=ja

本記事では、これらの裏側の仕組みを考察しながら、ベースイメージの自動更新によってどのようなセキュリティが向上するのかを解説します。

:::note info
Google Cloud Next ’24 で発表された Cloud Run のアップデートは『[詳細解説 Cloud Run 最新アップデート](https://qiita.com/AoTo0330/items/3e47dfbbcfe8b38591e1)』で解説しています🐶
:::

https://qiita.com/AoTo0330/items/3e47dfbbcfe8b38591e1

## マルチステージビルドとは
まずは、ベースイメージの自動更新の仕組みを理解するためにも、軽量なコンテナイメージビルドの基本となるマルチステージビルド](https://docs.docker.com/build/building/multi-stage/)の仕組みを理解していきましょう。

マルチステージビルドとは、`Dockerfile` の可読性・保守性を向上して軽量なコンテナ実行イメージを作成する手法です。特に Go, Rust などのコンパイラ言語の場合、ビルド後のバイナリファイルのみを実行イメージに含めることで、軽量なコンテナイメージが作成・実行できます。

マルチステージビルドを利用しない `Dockerfile` とマルチステージビルドを利用した `Dockerfile` の差分は以下のようになります。[^1]
[^1]: Docker 公式ドキュメント『[Multi-stage builds](https://docs.docker.com/build/building/multi-stage/)』から引用。この例では Golang の hello world アプリケーションを main.go にそのまま記載しています。

```diff_docker: Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.23
WORKDIR /src
COPY <<EOF ./main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go

+ FROM scratch
+ COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]
```

少しわかりづらいので、平易な日本語に直してみましょう。

```diff_docker: Dockerfile
# おまじない
FROM ビルドするためのイメージ AS 名前
WORKDIR /アプリを置く場所
COPY コードを持ってくる
RUN コンパイルやビルドをする

+ FROM 実行するためのイメージ AS 名前
+ COPY --from=ステージ名 成果物を持ってくる
CMD ["エントリーポイント"]
```

追加された行は新たにベースイメージを指定し、ビルドされた `main.go` を成果物(artifact)としてコピーしています。この時、[`FROM secatch`](https://hub.docker.com/_/scratch) を指定すると、イメージに追加のレイヤーは作成されず、ベースイメージは空の状態になります。

![multistage_scratch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2363567/d5ef0eb4-50cd-4ceb-a3dc-2c4fea06a475.png)

こうした `FROM scratch` の記載は、例のように事項するバイナリにランタイム依存関係がない場合のみ正常に実行できます。つまり、通常にアプリケーションの場合は、アプリケーションの実行に必要な最小限のベースイメージを指定する必要があります。

つまり、**Cloud Run** においては [Google Cloud の Buildpack ベースイメージ](https://cloud.google.com/run/docs/configuring/services/automatic-base-image-updates?hl=ja)を実行イメージとして指定したくなると思います。ですが、[ベースイメージの自動更新](https://cloud.google.com/run/docs/configuring/services/automatic-base-image-updates?hl=ja)では、`FROM scratch` で作成された実行イメージを指定するだけで良いのです。

その理由を探るためにも、CloudNative Buildpacks(CNB) について知っておきましょう。

## CloudNative Buildpacks(CNB) とは

[CloudNative Buildpacks(CNB)](https://buildpacks.io/docs/) とは、ソース コードから安全で効率的な、本番環境に対応したコンテナイメージを作成できるツールです。

https://buildpacks.io/

コンテナイメージの作成は、`detect` と `build` フェーズを通して行われます。つまり **CNB** はマルチステージビルドを前提としたツールです。**CNB** は **Builder** と呼ばれる、**Stack**(マルチステージビルドのコンテナイメージ) と **Buildpack**(`Detect` フェーズ・`Build` フェーズからなるビルドプロセス)の集合によって定義されます。

そして、これらの Builder は Google Cloud でも `gcr.io/buildpacks/builder` が提供されており、Cloud Run, App Engine のソースコードデプロイ時に内部的に利用されています。

詳しくは昨年のブログ『[Buildpacks が Google Cloud で使える件](https://qiita.com/AoTo0330/items/be487ae1c497651e2f8e)』でも解説しています。

https://qiita.com/AoTo0330/items/be487ae1c497651e2f8e

つまり、**Cloud Run** では内部的に **Buildpasks** が利用されるため、**CNB** の恩恵を受けた**ソースコードデプロイ**と**ベースイメージの自動更新**が可能ということです。

では、**ベースイメージの自動更新**は **CNB** のどのような機能を利用しているか考察してみましょう。

## `pack rebase` とイメージ拡張(image extension)機能
**CNB** の機能をよく確認してみると、[`pack rebase`](https://buildpacks.io/docs/for-app-developers/concepts/rebase/) というベースイメージを最新のバージョンに更新できる機能を見つけられます。

この `pack rebase` は既存の実行イメージのベースイメージのバージョンを確認し、新しいバージョンが存在するかを判断して存在する場合は実行イメージのメタデータの更新をリビルドなしで実現します。

**Cloud Run のベースイメージの自動更新**ではダウンタイム・リビルドなしでベースイメージを更新できるため、本機能を利用していることが推測されます。

一方で `pack rebase` コマンドを内部的にそのまま実行している可能性は低く、`rebase` の機能は基本的にはビルドプロセスを定義する **Buildpack** に含められます。この **Buildpack** に追加できる任意の拡張機能が[イメージ拡張機能](https://buildpacks.io/docs/for-buildpack-authors/concepts/extension/)です。

そのため、[Google Cloud の Buildpack](https://cloud.google.com/docs/buildpacks/overview?hl=ja) でも、イメージ拡張機能を利用して[ランタイムのベースイメージの切り替え](https://buildpacks.io/docs/for-buildpack-authors/tutorials/basic-extension/05_run-dockerfile-switch/)を実現していることが推測されます。

![multistage_image_extension.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2363567/f3518511-4e91-f730-9dc8-f90628ec5370.png)

こうして、Google Cloud の Buildpack が内部的に存在することでベースイメージの自動更新が実現されます。図のように `FROM scratch` を利用したマルチステージビルドで作成されたコンテナ実行イメージは、指定したベースイメージにベースイメージの切り替え(rebase)が行われます。

公式ドキュメントでは `FROM scratch` つまり空のベースを実行イメージに指定するように指定しています。ですが、内部的には `rebase` が行われるため、[Google Cloud が提供するベースイメージ](https://cloud.google.com/run/docs/configuring/services/runtime-base-images)をそのまま指定しても問題なく動作します。[^2]
[^2]: 2024年12月時点。Go 1.22 ベースイメージ(`google-22/go122`)で検証。

## おわりに
Cloud Run のベースイメージの自動更新機能は、Google Cloud により管理されたベースイメージを使用して OS レベルの脆弱性に対するセキュリティの向上が行える機能です。開発者の責任範囲を Google Cloud に以上して、素早い脆弱性の対応を Google Cloud のセキュリティチームに任せることができるのが最大の利点です。

本投稿は、[Jagu'e'r クラウドネイティブ分科会](https://jaguer.jp/workinggroup/#cloudnative)で発表した内容を記事にしたものです。Google Cloud のクラウドネイティブ技術に関連する内容の濃いコミュニティにご興味があれば、是非 Jagu'e'r へご参加ください🐯

https://speakerdeck.com/aoto/dive-deep-in-cloud-run-automatic-base-image-update