Open42

Cloud Build ちゃんと理解したい

nbstshnbstsh

Cloud Build を利用してはいるが、やりたいことを実現するためのその場しのぎの知識しかないので、docs を読み進めて理解を深めていく。

nbstshnbstsh

Build and push a Docker image with Cloud Build

とりあえず quickstart をやる。
まずは、Build の quickstart。

https://cloud.google.com/build/docs/build-push-docker-image

nbstshnbstsh

Prepare source files to build

echo するだけのshell と、その shell を実行するだけの Dockerfile を作る。

quickstart.sh
echo "Hello, world! The time is $(date)."
Dockerfile
FROM alpine
COPY quickstart.sh /
CMD ["/quickstart.sh"]
nbstshnbstsh

Create a Docker repository in Artifact Registry

Artifact Registry に repository を作る。

gcloud cli で作成

gcloud artifacts repositories create quickstart-docker-repo --repository-format=docker --location=asia-northeast1 --descri
ption="Docker repository"

gcloud cli で repository の一覧表示

gcloud artifacts repositories list

nbstshnbstsh

Build an image using Dockerfile

gcloud builds submit --tag asia-northeast1-docker.pkg.dev/{PROJECT_ID}/quickstart-docker-repo/quickstart-image:tag1

{PROJECT_ID} は自身の project の projectId に置き換える。

nbstshnbstsh

Build an image using a build config file

同様の Docker image の build を、build config file (cloudbuild.yaml) を使って行う。

cloudbuild.yaml を作成

steps:
  - name: 'gcr.io/cloud-builders/docker'
    args:
      [
        'build',
        '-t',
        'asia-northeast1-docker.pkg.dev/$PROJECT_ID/quickstart-docker-repo/quickstart-image:tag1',
        '.',
      ]
images:
  - 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/quickstart-docker-repo/quickstart-image:tag1'

At build time, Cloud Build automatically replaces $PROJECT_ID with your project ID.

$PROJECT_ID はそのままでOK。build 時に自動的にその環境の projectId が利用される。

cloudbuild.yaml を使って gcloud builds submit する

gcloud builds submit --config cloudbuild.yaml
nbstshnbstsh

まとめ

  • gcloud builds submit は、Dockerfile のみでも Docker image を build できる。
  • gcloud builds submit は、 build の成果物を Artifact Registory の repository にアップロードする。
  • tag の規約が大事。
    • ${region}-docker.pkg.dev/${projectId}/${artifactRegistoryRepositoryName}/${imageName}:${tag}
  • config file (cloudbuild.yaml) を使ってbuild の詳細設定を管理できる。
nbstshnbstsh

image の push について

quickstart の例では images filed を指定することによって、build された image を自動で Artiafct Registry に push している。

https://cloud.google.com/build/docs/build-config-file-schema#images

以下の例のように、明示的に docker push で Artifact Registry に push する step を追加することで同様のことが実現できる↓

steps:
  # Docker Build
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 
           'us-central1-docker.pkg.dev/${PROJECT_ID}/my-docker-repo/myimage', 
           '.']

  # Docker Push
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 
           'us-central1-docker.pkg.dev/${PROJECT_ID}/my-docker-repo/myimage']
nbstshnbstsh

Deploy a containerized application to Cloud Run using Cloud Build

次は、Deploy の quickstart やる。

https://cloud.google.com/build/docs/deploy-containerized-application-cloud-run

以下の API を有効化

nbstshnbstsh

Grant permissions

下準備として権限周りを整える。

  1. projectId, projectNumber を環境変数にひかえる
PROJECT_ID=$(gcloud config list --format='value(core.project)')
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
  1. Cloud Build service account に Cloud Run Admin role を追加
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
    --role=roles/run.admin
  1. Cloud Build service account に Cloud Run runtime service account の IAM Service Account User role を追加
gcloud iam service-accounts add-iam-policy-binding \
    $PROJECT_NUMBER-compute@developer.gserviceaccount.com \
    --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
    --role=roles/iam.serviceAccountUser
nbstshnbstsh

Deploy a prebuilt image

Artifact Registry に存在する Docker image を使って、Cloud Run へ deploy する。

cloudbuild.yaml 作成

gcr.io/cloud-builders/gcloud Cloud Builder を使うと、gcloud cli を実行できる。

gcloud cli を使って、Cloud Run へ deploy するコマンドを記載。

cloudbuild.yaml
steps:
  - name: 'gcr.io/cloud-builders/gcloud'
    script: |
      gcloud run deploy cloudbuild-hello-example --image us-docker.pkg.dev/cloudrun/container/hello --region asia-northeast1 --platform managed --allow-unauthenticated

gcloud build submit で実行

cloudbuild.yaml に記載した deploy のフローを Cloud Build に submit して実行する。

gcloud builds submit --config cloudbuild.yaml

動作確認

GCP console > Cloud Run から確認。ちゃんと deploy できている。

nbstshnbstsh

まとめ

  • Cloud Build は build だけでなく、 deploy 等のいろんな step を構築できる。
  • gcloud cli でできることは、Cloud Build でもできる。
nbstshnbstsh

ここまでの流れを整理しとく

大雑把な deploy フロー

GCP に Docker を利用したアプリを deploy するおおまかなフローはこんな感じになる↓

  1. Docker Image を build する
  2. 1 で用意した Image をどっかに置いとく
  3. 2 の image を指定して deploy する

Cloud Build でできること

Cloud Build を使うと、この GCP への deploy までのフローを簡単に実現できる。

Cloud Run への deploy のケースを例にとると、

  1. Docker Image を build して tag 付け
  2. Artifact Registry の repository (= Docker Image の置き場所) に生成した image を push
  3. image をもとに Cloud Run へ deploy

この一連の処理を cloudbuild.yaml に記載しておけば、コマンド一発で deploy まで実現できる。

gcloud builds submit --config cloudbuild.yaml

Build & Deploy and more....

Cloud Build では、Cloud Builder という諸々のツールやプログラムの実行環境を土台に、いろいろな処理の "step" を積み重ねていくことができる。

ここまでの quickstart では、Build の step, Deploy の step のみを試したが、ここに test の step や通知の step を追加していけば包括的な CI/CD パイプラインを構築できそう。

nbstshnbstsh

gcloud builds submit は何をしている...?

ここまでの quickstart で gcloud builds submit で Dockerfile を build したり、Cloud Run へ deploy したりしてきたが、そもそも Cloud Build はなんで local の source code を build できるんだ...? source code をどっかにアップロードしてるのか...?

と、疑問が湧いてきたので調べる。

nbstshnbstsh

gcloud builds submit がやっていること

gcloud builds submit --region=us-west2 --tag gcr.io/PROJECT_ID/IMAGE_NAME .

The gcloud builds submit command:

  • compresses your application code, Dockerfile, and any other assets in the current directory as indicated by .;
  • uploads the files to a Cloud Storage bucket;
  • initiates a build in the location us-west2 using the uploaded files as input;
  • tags the image using the provided name
  • pushes the built image to Container Registry.

local の source code を compress して Cloud Storaeg bucket にアップロードしてるみたい。
gcloud builds submit SOURCE の形で、対象の source code の path を指定できる。何も指定しないとデフォルトで currrent directory が利用される。

その後、アップロードされた file をもとに build 実行 => tag 付け => image を registry に push の流れ。

nbstshnbstsh

Cloud Build が利用する Cloud Storage bucket

When you run gcloud builds submit for the first time in a Google Cloud project, Cloud Build creates a Cloud Storage bucket named [YOUR_PROJECT_NAME]_cloudbuild in that project. Cloud Build uses this bucket to store any source code that you might use for your builds. Cloud Build does not automatically delete contents in this bucket. To delete objects you're no longer using for builds, you can either set up lifecycle configuration on the bucket or manually delete the objects.

gcloud builds submit の初回実行時に、[YOUR_PROJECT_NAME]_cloudbuild という bucket が作成され、Cloud Build が利用する source code がここにアップロードされる。

Cloud Storage 確認したら確かに作成されていた↓

nbstshnbstsh

local の source code のアップロードをスキップ

Cloud Build が参照する source code が、すでに Cloud Storage に存在する場合は、以下のように 直接 Cloud Storage の file を指定することで local の source code をアップロードするフローをスキップできる。直接 Cloud Storage 内の指定した file に圧縮された source code が利用される。

gcloud builds submit \
--config cloudbuild.yaml \
gs://BUCKET/SOURCE.tar.gz
nbstshnbstsh

Substitution

Cloud Build config 内で参照できる変数。

e.g.) $PROJECT_ID: ID of your Cloud project

https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values

nbstshnbstsh

デフォで利用できる変数

$PROJECT_ID: ID of your Cloud project
$BUILD_ID: ID of your build
$PROJECT_NUMBER: your project number
$LOCATION: the region associated with your build

nbstshnbstsh

trigger による build で利用できる変数

$TRIGGER_NAME: the name associated with your trigger
$COMMIT_SHA: the commit ID associated with your build
$REVISION_ID: the commit ID associated with your build
$SHORT_SHA : the first seven characters of COMMIT_SHA
$REPO_NAME: the name of your repository
$REPO_FULL_NAME: the full name of your repository, including either the user or organization
$BRANCH_NAME: the name of your branch
$TAG_NAME: the name of your tag
$REF_NAME: the name of your branch or tag
$TRIGGER_BUILD_CONFIG_PATH: the path to your build configuration file used during your build execution; otherwise, an empty string if your build is configured inline on the trigger or uses a Dockerfile or Buildpack.
$SERVICE_ACCOUNT_EMAIL: email of the service account you are using for the build. This is either a default service account or a user-specified service account.
$SERVICE_ACCOUNT: the resource name of the service account, in the format projects/PROJECT_ID/serviceAccounts/SERVICE_ACCOUNT_EMAIL

nbstshnbstsh

github 特有の変数

$_HEAD_BRANCH : head branch of the pull request
$_BASE_BRANCH : base branch of the pull request
$_HEAD_REPO_URL : url of the head repo of the pull request
$_PR_NUMBER : number of the pull request

nbstshnbstsh

独自の変数

独自の変数を定義することも可能。

https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values

お決まり事

Substitutions must begin with an underscore (_) and use only uppercase-letters and numbers (respecting the regular expression [A-Z0-9]+). This prevents conflicts with built-in substitutions. To use an expression starting with $ you must use $$. Thus:

  • 変数名は _ で始めること
  • 利用可能な値は、uppercase-letters と数値のみ _[A-Z0-9_]+

利用方法

You can specify variables in one of two ways: $_FOO or ${_FOO}:

$_FOO or ${_FOO}

変数に値を指定する。

gcloud builld submit 時に、--substitutions で指定してあげればOK。

gcloud builds submit --config=cloudbuild.yaml \
  --substitutions=_NODE_VERSION_1="v6.9.4",_NODE_VERSION_2="v6.9.5" .

デフォルト値の指定

Cloud build config file の substitutions でデフォルト値を指定可能。

cloudbuild.yaml
# ....省略...
substitutions:
    _NODE_VERSION_1: v6.9.1 # default value
    _NODE_VERSION_2: v6.9.2 # default value
images: [
    'gcr.io/$PROJECT_ID/build-substitutions-nodejs-${_NODE_VERSION_1}',
    'gcr.io/$PROJECT_ID/build-substitutions-nodejs-${_NODE_VERSION_2}'
]
nbstshnbstsh

環境変数に map

substitutions で扱う変数は、あくまで Cloud Build Config 内で参照する値。
Build step 内で利用したい場合は、それぞれの step の環境変数に map してあげる必要がある。

e.g.) 以下の "$_USER" は bash script 実行時の環境変数。substitutions の $_USER を直接参照しているわけではない。

cloudbuild.yaml
steps:
- name: 'ubuntu'
  script: |
    #!/usr/bin/env bash
    echo "Hello $_USER"

一括で全ての変数を環境変数に map

Cloud build config で options.automapSubstitutions を true にセットすればOK。

cloudbuild.yaml
options:
  automapSubstitutions: true
substitutions:
  _USER: "Google Cloud"

step 内で全ての変数を環境変数に map

top level の options.automapSubstitutions を true にすると、全ての step が対象になるが、step level で automapSubstitutions を true にすると、その step 内のみ全ての変数を環境変数に map できる。

cloudbuild.yaml
steps:
- name: 'ubuntu'
  script: |
    #!/usr/bin/env bash
    echo "Your project ID is $PROJECT_ID"
  automapSubstitutions: true
substitutions:
  _USER: "Google Cloud"

手動で設定

各 step の env でその step の環境変数を定義できる。ここで substitution 変数を step の環境変数に map してあげることも可能。

cloudbuild.yaml
steps:
- name: 'ubuntu'
  env:
  - 'BAR=$PROJECT_ID'
  script: 'echo $BAR'
nbstshnbstsh

Github と接続

Cloud Build は "Github の変更を起点として実行する" みたいなことが可能。
この "何かのイベントを起点に Build を実行するやつ” を Cloud Build trigger と言う。

Github の変更を起点とする Cloud Build trigger を作りたいのだが、まず下準備として、Cloud Build を Github と接続させる必要があるとのことなので進めていく。

https://cloud.google.com/build/docs/automating-builds/github/connect-repo-github?generation=2nd-gen

nbstshnbstsh

Required IAM permissions

cloud build service agent (service-${PROJECT_NUMBER}@gcp-sa-cloudbuild.iam.gserviceaccount.com) に "roles/secretmanager.admin" を付与する必要あり。

PN=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
CLOUD_BUILD_SERVICE_AGENT="service-${PN}@gcp-sa-cloudbuild.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member="serviceAccount:${CLOUD_BUILD_SERVICE_AGENT}" \
  --role="roles/secretmanager.admin"
nbstshnbstsh

Connecting a GitHub host

connection 作成

 gcloud builds connections create github $CONNECTION_NAME --region=$REGION

ULR が表示されるので、github に login した状態で URL を開き、Cloud Build GitHub App の認証を完了させる。

Please log in to https://github.com using a robot account and then follow this link to authorize Cloud Build to access that account. After authorization, your GitHub authorization token will be stored in Cloud Secret Manager.
https://accounts.google.com/AccountChooser?continue=https%3A%2F%2Fconsole.cloud.google.com....

完了すると、github oauth token が secret manager に保存される。

確認

 gcloud builds connections describe $CONNECTION_NAME --region=$REGION

githubConfig に接続した github の情報が入っているか確認↓

githubConfig:
  appInstallationId: 'xxxxxxxxx'
  authorizerCredential:
    oauthTokenSecretVersion: projects/my-project-id/secrets/my-org-github-github-oauthtoken-xxxxx/versions/latest
    username: my-robot-account

ここまでで Github host との "connection" が作成された状態で、特定の repository とは接続はされているわけではない。

nbstshnbstsh

Connecting a GitHub repository

先ほど作成した "connection" を利用し、repository と接続する。

   gcloud builds repositories create $REPO_NAME \
     --remote-uri=$REPO_URI \
     --connection=$CONNECTION_NAME --region=$REGION

これで、接続した repository が Cloud Build trigger の repository 先として利用できる。

nbstshnbstsh

Cloud Build Trigger (Github)

gcloud cli で作成

gcloud builds triggers create github を利用すると、Github と連携した Cloud Build trigger 作成可能。

https://cloud.google.com/sdk/gcloud/reference/beta/builds/triggers/create/github

e.g.) master branch への pull request を契機に Build を実行する trigger

gcloud builds triggers create github \
  --name="my-trigger" \
  --repository=projects/my-project/locations/asia-northeast1/connections/my-connection/repositories/my-repo \
  --pull-request-pattern="^master$" \
  --build-config="cloudbuild.yaml" \
  --region=asia-northeast1
nbstshnbstsh

yaml から import

あらかじめ、設定値を記載した yaml file を用意しておき、その file から import する形で Cloud Build trigger を作成することも可能。

https://cloud.google.com/sdk/gcloud/reference/beta/builds/triggers/import

e.g.) master branch への pull request を契機に Build を実行する trigger

trigger.yaml を作成し、

trigger.yaml
filename: cloudbuild.yaml
name: my-trigger
repositoryEventConfig:
  pullRequest:
    branch: ^master$
    commentControl: COMMENTS_ENABLED
  repository: projects/my-project/locations/asia-northeast1/connections/my-connection/repositories/my-repo
  repositoryType: GITHUB

gcloud beta builds triggers import を実行。

gcloud beta builds triggers import --source=./trigger.yaml --region asia-northeast1
nbstshnbstsh

approval を有効化

trigger.yaml に approvalConfig.approvalRequired true を追加すればOK

trigger.yaml
#...
+approvalConfig:
+  approvalRequired: true
nbstshnbstsh

custom service account を利用

cloud build config yaml の serviceAccount を指定すればOK

cloudbuild.yaml
serviceAccount: 'projects/$PROJECT_ID/serviceAccounts/my-cloud-build-service-account@$PROJECT_ID.iam.gserviceaccount.com'
nbstshnbstsh

必要な role

roles/cloudbuild.builds.builder を利用すれば基本的な権限はカバーできる。

The email for the Cloud Build service account is [PROJECT_NUMBER]@cloudbuild.gserviceaccount.com. This service account may have permissions that are unnecessarily broad for your use case. You can improve the security posture by following the principle of least privilege. As part of this principle, we recommend creating your own service account to execute builds on your behalf, this can reduce the potential impact of misconfigurations or malicious users.

roles/cloudbuild.builds.builder は Cloud Build の default service account が利用する role。場合によっては、必要以上の権限になっている場合もあるのでその場合は自前の custom role で対応した方が良さげ。

https://cloud.google.com/build/docs/cloud-build-service-account

roles/cloudbuild.builds.builder permissions
description: Can perform builds
etag: AA==
includedPermissions:
- artifactregistry.aptartifacts.create
- artifactregistry.dockerimages.get
- artifactregistry.dockerimages.list
- artifactregistry.files.download
- artifactregistry.files.get
- artifactregistry.files.list
- artifactregistry.kfpartifacts.create
- artifactregistry.locations.get
- artifactregistry.locations.list
- artifactregistry.mavenartifacts.get
- artifactregistry.mavenartifacts.list
- artifactregistry.npmpackages.get
- artifactregistry.npmpackages.list
- artifactregistry.packages.get
- artifactregistry.packages.list
- artifactregistry.projectsettings.get
- artifactregistry.pythonpackages.get
- artifactregistry.pythonpackages.list
- artifactregistry.repositories.createOnPush
- artifactregistry.repositories.deleteArtifacts
- artifactregistry.repositories.downloadArtifacts
- artifactregistry.repositories.get
- artifactregistry.repositories.list
- artifactregistry.repositories.listEffectiveTags
- artifactregistry.repositories.listTagBindings
- artifactregistry.repositories.readViaVirtualRepository
- artifactregistry.repositories.uploadArtifacts
- artifactregistry.tags.create
- artifactregistry.tags.get
- artifactregistry.tags.list
- artifactregistry.tags.update
- artifactregistry.versions.get
- artifactregistry.versions.list
- artifactregistry.yumartifacts.create
- cloudbuild.builds.create
- cloudbuild.builds.get
- cloudbuild.builds.list
- cloudbuild.builds.update
- cloudbuild.operations.get
- cloudbuild.operations.list
- cloudbuild.workerpools.use
- containeranalysis.occurrences.create
- containeranalysis.occurrences.delete
- containeranalysis.occurrences.get
- containeranalysis.occurrences.list
- containeranalysis.occurrences.update
- logging.logEntries.create
- logging.logEntries.list
- logging.views.access
- pubsub.topics.create
- pubsub.topics.publish
- remotebuildexecution.blobs.get
- resourcemanager.projects.get
- resourcemanager.projects.list
- source.repos.get
- source.repos.list
- storage.buckets.create
- storage.buckets.get
- storage.buckets.list
- storage.objects.create
- storage.objects.delete
- storage.objects.get
- storage.objects.list
- storage.objects.update
name: roles/cloudbuild.builds.builder
stage: GA
title: Cloud Build Service Account
nbstshnbstsh

エラーでた

gcloud builds submit したところ以下のエラーに遭遇

ERROR: (gcloud.builds.submit) INVALID_ARGUMENT: generic::invalid_argument: if 'build.service_account' is specified, the build must either (a) specify 'build.logs_bucket', (b) use the REGIONAL_USER_OWNED_BUCKET build.options.default_logs_bucket_behavior option, or (c) use either CLOUD_LOGGING_ONLY / NONE logging options

custom service accoun を利用する場合は、log に関する設定が必要とのこと

nbstshnbstsh

log に関する設定

When you specify your own service account for builds, you must store your build logs either in Cloud Logging or in a user-created Cloud Storage bucket. You can not store your logs in the default logs bucket.

service account を指定した場合、デフォの log bucket は利用できないので Cloud Build の log の保存先を設定する必要がある。選択肢は以下の二つ。

  1. Cloud Logging を利用
  2. 自前の Cloud Storage bucket を利用

https://cloud.google.com/build/docs/securing-builds/configure-user-specified-service-accounts#set_up_build_logs

詳細は、Sore and manage build logs を参照

Cloud Logging を利用する場合

cloudbuild.yaml
#...
options:
  logging: CLOUD_LOGGING_ONLY

Cloud Sorage buket を利用する場合

cloudbuild.yaml
#...
logsBucket: 'gs://mylogsbucket'
options:
  logging: GCS_ONLY
nbstshnbstsh

step 間でデータの引き継ぎ

Cloud Build runs your tasks as a series of build steps, which execute in isolated and containerized environments. After each step, the container is discarded. This allows you to have totally different tools and environments for each step, and by default, any data created in one step can't contaminate the next step. But sometimes you may need to persist state from one step of a build to use in subsequent steps.

Cloud Build の各 step は独立した container 環境なので基本的にデータは分離されてる。
が、これだと前の step の成果物に依存する step を作りたい場合に困る。

e.g.) dependencies を install する step => test を実行する step

For such cases, Cloud Build provides volumes, which are read-write file paths that you can attach to any build step. Volumes retain their contents throughout the duration of the build. You can define your own volume or use /workspace, which is the default volume that Cloud Build provides for you. Before executing a build, Cloud Build extracts the source code to /workspace. Anything written to user-defined volumes and /workspace by any step will be available to subsequent steps.

このような場合に対応するために、各 step を跨いで尊属し、共有される volume が存在する。デフォルトで /workspace がこの volume として扱われる。

ちなみに、Cloud Build は user の source code をこの /workspace 下に置くので source code 内での変更は保持されるはず。なので install で生成される node_modules とか、build で生成される dist とかは次の step でも残っているはず。

https://cloud.google.com/build/docs/configuring-builds/pass-data-between-steps

nbstshnbstsh

node の例

特に明記されていないが、test や build を実行するためには install された node_modules が残っていないとおかしいので、install step で /workspace volume 内の source code に node_modules が生成されて、その上で test step, build step が実行されているはず。

cloudbuild.yaml
 steps:
 - name: 'node'
    entrypoint: 'npm'
    args: ['install']
 - name: 'node'
    entrypoint: 'npm'
    args: ['test']
 - name: 'node'
    entrypoint: 'npm'
    args: ['run', 'build']

https://cloud.google.com/build/docs/building/build-nodejs

nbstshnbstsh

Cloud Build とは何か

時間経ってだいぶ理解が深まってきたので今の理解をメモ。

Cloud Build は何かというと、

"containerize された実行環境に、指定した source code を取り込んで、何かしらの処理を実行できるやつ"

で、この時の実行の単位が step で、どの実行環境で実行するかを step.name で指定している。この指定対象の"実行環境"が Cloud Builder と呼ばれているわけだが、要は docker image なので dockerhub の image を指定することもできる。

gcloud build submit で source を指定するわけだけど、これが "実行環境"に取り込みたい source code" の指定になる。デフォで container 内の /workspace に配置される。

あとは、"source code を取り込んだ実行環境内のどこで何を実行したいか" 等の実行に関する詳細を各 step の option で定義していく流れ。

Cloud Build はこの step を積み重ねて一つのワークフローを定義できる。この時、各 step の実行環境は分離されているけど、取り込んだ source code 内の変更は引き継がれる(/workspace volume)ので、step を積み重ねて install/test/build みたいなフローが作成できる。