GitLabによるCI実践入門――Kubernetesで利用するコンテナイメージをビルドする:Cloud Nativeチートシート(6)
Kubernetesやクラウドネイティブをより便利に利用する技術やツールの概要、使い方を凝縮して紹介する連載。今回は、GitLabによるCI(継続的インテグレーション)について解説します。
Kubernetesやクラウドネイティブをより便利に利用する技術やツールの概要、使い方を凝縮して紹介する本連載「Cloud Nativeチートシート」。前回は、Kubernetesの利用を前提とした「Kubernetes Native」なCI/CD(継続的インテグレーション/継続的デリバリー)について、歴史と背景を踏まえ、CI/CDに具備すべき機能がどういったものかをツールの比較と動向も交えて解説しました。
今回は、CIによるKubernetesにデプロイするコンテナイメージの作成方法を紹介します。CIを行う際にポイントとなるマージリクエストによるCIの実行、コンテナの脆弱(ぜいじゃく)性スキャン、テスト結果やカバレッジのCIの結果、開発者環境とCI環境でテスト結果を同じようするコツなどを併せて紹介します。Kubernetesを用いたCI/CDを、これから行うとしている方の参考になればと思います。
なおCDについては、次回記事で紹介します。
目次
KubernetesアプリケーションにおけるCIとは
前回の記事の再掲となりますが、KubernetesでCI/CDを行う場合、CIはアプリケーションをソースコードから最終的にコンテナイメージを作成し、コンテナレジストリに登録するところまでを担当します。つまり、コンテナレジストリにイメージを登録できればゴールです。
なお今回は、CIツールとして「GitLab」を、サンプルアプリケーションとして「Go言語」で作成したWebアプリケーションを、コンテナイメージの脆弱性スキャンツールとして「Trivy」を利用します。Trivyは、Kubernetesのセキュリティで定評のあるAqua Securityが開発しています。
以降、ソースコードからコンテナイメージをビルドし、イメージをレジストリに登録していく手順を紹介します。
手順1.ブランチ作成
下準備として、機能追加、バグフィクス用にGitブランチを作成しておきます。Gitのブランチに関する詳細は「いまさら聞けない、成功するブランチモデルとgit-flowの基礎知識」などの記事をご覧ください。このブランチ作成は、ソースコードの変更を承認してもらうための、下記手順3.マージリクエストの下準備です。
手順2.ソースコード作成・編集
ソースコードを作成、編集し、Gitリポジトリにコミット、プッシュします。
手順3. マージリクエスト(プルリクエスト)作成
手順1.で作成したブランチからマージリクエストを作成します。CIはマージリクエスト作成、更新時に実行します。マージリクエストの更新は、手順1.で作成したブランチに変更をプッシュすると自動的に反映されます。また、マージリクエストをmain(master)ブランチにマージした際にも、手順1.でブランチを作成した後の変更に影響がないかどうかを確認するためにCIを実行します。
コラム masterブランチ終了のお知らせ。mainブランチこんにちは
最近、Master(主人)やSlave(奴隷)といった、差別を想起させる用語を使わないようにしようという動きがIT業界にあります。
Gitのデフォルトブランチ名で利用される「master」についても同様で、廃止して「main」にしようという動きがあります。
GitLabにおいても、2021年5月22日にリリースされたGitLab 14でデフォルトブランチ名をmasterからmainに変更しました。GitHubでは、既に2020年10月からデフォルトブランチをmainブランチに変更しています(GitHubのアナウンス)。
デフォルトブランチは今後、できるだけ「main」を利用した方がよいでしょう。
手順4. コンテナイメージビルド
コンテナイメージをビルドします。ビルドは、マージ/プルリクエストの作成、修正や、mainブランチへのマージをトリガーとして行います。
手順5. 脆弱性スキャン
コンテナイメージに脆弱性がないかどうかをスキャンします。
手順6. コンテナレジストリへのプッシュ
コンテナレジストリにコンテナイメージを登録します。
コラム CIはマージリクエスト/プルリクエストで
最近は、ソースコードをGitのリポジトリで管理する現場が増えていると思いますが、CIはマージリクエスト(GitLab)、プルリクエスト(GitHub)上で行うと便利です。マージリクエスト上でCIを行うことにより、次のようなメリットがあります。
- 機能の実装やバグフィクスごとにCIを実行でき、他の作業の影響を避けることができる
- mainブランチへのマージ前にCIの結果を確認でき、確実にCIが成功してからmainブランチにコードをマージできる
- マージリクエスト上でCIの結果が確認でき、CIの失敗履歴や、失敗に対応したソースコードの修正履歴を1つのマージリクエストの画面で追えるようになる
- マージリクエストを作成しなければ、作業途中のコードをブランチにコミットしてもCIが実行されないので、無駄なCIによるマシンリソースの消費を防げる
GitLabによるCIの特徴
本稿で紹介するGitLabを利用したCIの特徴を簡単に紹介します。
特徴1.マージリクエスト上でCIを実行できる
GitLabは、Gitリポジトリの機能、Gitブランチのマージの承認を依頼するマージリクエスト(プルリクエスト)の機能を持っています。また、このマージリクエストが作成されたときとマージリクエストで指定されたブランチが更新されたときに、自動的にCIを実行する機能も持っています。
特徴2.コンテナイメージもGitLabで管理できる
GitLabには、コンテナイメージのレジストリ機能もあるので、ユーザーはビルドしたコンテナイメージをGitLabで管理できます。GitLabのレジストリにプッシュしたコンテナは、他のユーザーに公開することもできますし、プロジェクトや個人に閉じて非公開にすることもできます。
特徴3.広く使われている
広く使われているツールなので、CIを実行するスクリプトのサンプルが簡単に見つかります。
特徴4.インストール不要でコンテナイメージのビルドが可能
無料のSaaSバージョンを利用すれば、インストールせずにイメージのビルドからコンテナレジストリへのアップロードまで可能です。何もインストールしなくても、手軽に始めることができます。
GitLabでCIを実践してみよう
ここからは、「GitLabでコンテナイメージを作成するにはどうすればよいか」についてサンプルアプリケーションを用いて解説します。本稿では、サンプルアプリケーションとしてGo言語で記述された簡易なREST APIを利用します。
サンプルアプリケーションは「https://gitlab.com/cloudnativetips/ci-sample」にあります。本稿の手順に沿ってサンプルを試す場合、GitLabアカウントを作成し、上記リポジトリをフォークしてご利用ください。
この章で一通り動きを確認した後で、サンプルのコードとCIの実装を確認します。
CIの実行
GitLabでは、CIが実行されるタイミングを設定することができます。CIは設定されたタイミングで実行されることになります。基本的に、ソースコードの変更リクエストがマージリクエストとして投げられたときと、マージリクエストが修正されたときに、CIを実行します。マージリクエスト上でのCIによって、変更の承認者がソースコードの変更内容と、CIの実行結果(主にテスト結果)を確認して、変更を承認できるようになります。
また、マージリクエストを修正している間に、mainブランチに対する変更が現在のマージリクエストに影響を与える場合があるので、mainブランチにマージした後も念のためCIを実行します。
それでは、今回のサンプルを使って、CIを実行してみましょう。
※注
本記事では、GitLabの言語を日本語に設定した状態で解説しています。言語設定を日本語にする方法は、「GitLab.comのアカウントを作成し、安全に利用する方法」の「UIを日本語に変更する」をご覧ください。
1.リポジトリのフォーク
下準備として、GitLabのサンプルプロジェクトをクローンします。GitLabのSaaS版のアカウントをお持ちでなければ、GitLabのサインアップのページから作成してログインします。ログインしたら、サンプルのサイト「https://gitlab.com/cloudnativetips/ci-sample」に移動して、右上の「フォーク」ボタンを押してください。
フォークするネームスペース(ユーザー名もしくはグループ名)の「選択」を押してください。
2.ブランチの作成
次にソースコード変更用のブランチを作成します。Gitコマンドをあらかじめインストールしておいてください。先ほどフォークしたリポジトリのクローンと、変更するためのフィーチャーブランチ(ここではfeature-test)を作成します。
$ git clone https://gitlab.com/フォーク先の「アカウント名」もしくは「プロジェクト名」/ci-sample $ cd ci-sample $ git checkout -b feature-test
3.ソースコードの変更
ソースコードを変更します。今回はWebサーバの待受ポートを80から8080に変更してみます。「cmd/main.go」ファイルを下記のように変更します。
func main() { router := sample.NewRouter() router.Logger.Fatal(router.Start(":80")) // 修正箇所 }
func main() { router := sample.NewRouter() router.Logger.Fatal(router.Start(":8080")) // 修正箇所 }
変更したら、コミットし、プッシュします。
$ git add cmd/main.go $ git commit -m "ブランチの編集のテスト" $ git push --set-upstream origin feature-test (※初回のみ--set-upstream originが必要)
これで、GitLabのリポジトリ上にブランチが新たに作成されます。
お詫びと注意(2021年7月13日17時30分)
2021年6月22日の公開当初の「ci-sample」プロジェクトはfeature-testブランチが既に存在していたため、プッシュ時にエラーが発生していました。プッシュに失敗した場合は、一度GitLabにフォークしたプロジェクト(リポジトリ)を削除し、もう一度フォークし直してください。読者ならびに関係者の方々にお詫び申し上げます(編集部)。
4.マージリクエストの作成
作成したブランチからマージリクエストを作成します。マージリクエストとは、ブランチを別のブランチ(ここではmainブランチ)にマージするための要求です。
GitLab上でフォークした「ci-sample」プロジェクトの画面で「リポジトリ」→「ブランチ」から作成したブランチ(ここではfeature-test)の「マージリクエスト」ボタンを押してください。
「New merge request」画面が表示されるので、タイトルや内容を入力し、「Create マージリクエスト」ボタンを押してください。
5.CIの実行結果の確認
先ほど作成したマージリクエスト上に時間がたつと、マージリクエスト上にCIの実行結果が表示されます。成功すれば、緑のチェックマークが表示されます。
失敗すると、赤で×マークが表示されます。
マージリクエスト上のCIの実行結果にパイプラインIDが表示されています。
このパイプラインIDをクリックすると、パイプラインの実行状況の詳細が確認できます。
この実行結果では、buildとscanのジョブが成功したことを示しています。失敗した場合は、赤の×ボタンが表示されます。
また、各ジョブをクリックすると、ジョブの実行結果を確認できます。
CIが失敗した場合は、失敗したジョブの実行結果をこの画面で確認し、エラーの原因を探ります。
一度マージリクエストの作成が完了すると、次からは、マージリクエストに対応するブランチ(ここではfeature-testブランチ)を修正、コミット、プッシュするだけで、マージリクエストに自動的に新たな変更が通知され、CIがブランチの修正を都度実行します。
サンプルの概要
以下、今回利用したCIのサンプルについて解説します。
ファイルの配置
GitLabのビルド定義ファイルは、「.gitlab-ci.yml」という名前で、リポジトリのルートディレクトリに配置します。また、Dockerfileも今回は、ルートディレクトリに配置しました。Dockerfileは他の場所に配置することも可能です。
GitLabのビルド定義(.gitlab-ci.yml)
GitLabでは、CIおよびCDを行う一連のフローを「パイプライン」として定義します。パイプラインは幾つかのステージに分かれ、各ステージを順番に実行します。例えば、「ビルド」「テスト」「イメージスキャン」などのステージを用意し、これらのステージを順番に実行します。各ステージで実行する具体的な内容はジョブとして記述します。本稿のサンプルでは、1ステージ1ジョブの構成ですが、1ステージに複数のジョブを定義することもできます。その場合、ステージ内の複数のジョブを並列で実行します。順序性を持たせたいときは「ステージを分ける」と考えるとよいでしょう。今回作成したビルドファイルは、下記のステージとジョブがあります。
ステージ | ステージに含まれるジョブ | ジョブの説明 |
---|---|---|
stage-build | build | コンテナイメージ作成処理(アプリケーションのコードスキャン、ビルド、単体テスト、カバレッジ取得、中間イメージのプッシュ) |
stage-scan | scan | コンテナイメージのスキャン |
stage-update-latest-image | update-latest-image | コンテナイメージのlatestイメージをアップデート |
以下、サンプルファイルのビルド定義ファイルを見ていきます。
# 使用するイメージの指定 image: docker:stable # 使用するサービスの指定 services: - docker:dind # 環境変数 variables: CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH REPO_NAME: gitlab.com/cloudnativetips/ci-sample TRIVY_VERSION: 0.29.1 before_script: - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com # ステージ stages: - stage-build - stage-scan - stage-update-latest-image # tage-buildステージ build: # ステージ stage: stage-build # 処理内容 script: - docker build --target codescan -t $CONTAINER_IMAGE:work . - docker build --target unittest -t $CONTAINER_IMAGE:work . - docker run $CONTAINER_IMAGE:work cat ./report.xml > report.xml - docker run $CONTAINER_IMAGE:work cat ./cover.txt - docker build --target build_image -t $CONTAINER_IMAGE:$CI_COMMIT_SHA . - docker push $CONTAINER_IMAGE:$CI_COMMIT_SHA # カバレッジ coverage: '/coverage: \d+\.\d+% of statements/' # アーティファクト artifacts: reports: junit: report.xml # CI実行契機 only: - merge_requests - main # stage-scanステージ scan: stage: stage-scan script: - docker pull $CONTAINER_IMAGE:$CI_COMMIT_SHA - wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf - - ./trivy --cache-dir .trivycache/ image --exit-code 0 --no-progress --format template --template "@contrib/gitlab.tpl" -o gl-container-scanning-report.json $CONTAINER_IMAGE:$CI_COMMIT_SHA - ./trivy --cache-dir .trivycache/ image --exit-code 1 --severity CRITICAL --no-progress $CONTAINER_IMAGE:$CI_COMMIT_SHA artifacts: reports: container_scanning: gl-container-scanning-report.json only: - merge_requests - main # stage-update-latest-imageステージ update-latest-image: stage: stage-update-latest-image script: - docker pull $CONTAINER_IMAGE:$CI_COMMIT_SHA - docker tag $CONTAINER_IMAGE:$CI_COMMIT_SHA $CONTAINER_IMAGE:latest - docker push $CONTAINER_IMAGE:latest only: - main
それでは、設定パラメーターを見ていきましょう。
image(使用するイメージを指定)
image: docker:stable
使用するイメージを指定します。今回は、コンテナイメージを作成するので、「docker:stable」を指定します。このイメージから起動したコンテナ上で、CIのスクリプトを実行します。
services(使用するサービスのイメージを指定)
services: - docker:dind
使用するサービスのイメージを指定します。サービスでは、スクリプトを実行するコンテナ以外のコンテナを起動できます。
例えば、データベースなどのミドルウェアの試験などに利用します。ここでは、コンテナ上でさらにDockerを利用する「Docker IN Docker」を利用する「dind」コンテナを起動します。
dindコンテナをサービスとして起動することにより、パイプライン内のスクリプトでDockerコマンドを利用できます。
詳細は、「Use Docker to build Docker images」をご覧ください。
コラム dockerコンテナとdindコンテナの関係
上記で、imageとservicesでコンテナを起動していますが、この2つのコンテナの関係は、図12のようになります。
dockerコンテナ上でジョブのスクリプトを実行します。dockerコンテナ上で実行したdockerコマンドの処理は、dindコンテナの「dockerd」(コンテナ管理の常駐プロセス)に移譲され、dindコンテナ上でコンテナを操作します。
「docker cp」コマンドが「docekr run」コマンドのリダイレクトを利用して、dindで起動されたコンテナ内のファイルをコピーしたり(result.xml)、標準出力に出力したり(cover.txt)しています。
あまり意識しないかもしれませんが、トラブルシューティングなどでこの構成を覚えておくと役に立つでしょう。
variables(各ジョブで使用する共通変数を設定)
variables: CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH REPO_NAME: gitlab.com/cloudnativetips/ci-sample TRIVY_VERSION: 0.29.1
各ジョブで使用する共通変数を設定します。「CONTAINER_IMAGE」変数で使用している「$CI_PROJECT_PATH」変数は、GitLabのプロジェクトのパスを表すGitLabで定義済みの環境変数です。
その他のGitLabで定義済の環境変数については「Predefined environment variables reference | GitLab」をご参照ください。参考までに今回のサンプルでは、下記の定義済み環境変数を利用しています。
変数 | 説明 |
---|---|
CI_PROJECT_PATH | CIが実行されるGitLabのプロジェクトのパス |
CI_JOB_TOKEN | GitLabコンテナレジストリで利用する認証トークン |
CI_COMMIT_SHA | CIを実行する対象のGitリポジトリへのコミットのハッシュ値 |
キャプション |
before_script(各ジョブ実行前の処理を記述)
before_script: - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
before_scriptでは、各ジョブ実行前の処理を記述します。今回は事前にGitLabコンテナレジストリに「docker login」でログインし、コンテナレジストリにプッシュできるようにしています。
「-p $CI_JOB_TOKEN」オプションを利用すると、GitLabのコンテナレジストリにログインできます。
stages(パイプラインの各ステージを設定)
stages: - stage-build - stage-scan - stage-update-latest-image
パイプラインの各ステージを設定します。今回は下記3ステージの構成としました。
- stage-build:コンテナイメージ作成処理(アプリケーションのコードスキャン、ビルド、単体テスト、カバレッジ取得、中間イメージのプッシュ)
- stage-scan:コンテナイメージのスキャン
- stage-update-latest-image:latestタグイメージを更新
ステージを細かく分割すると各ステージの処理内容が分かりやすくなりますが、各ステージの処理時間やステージ間の待ち時間が長くなります。ステージの分割方針はプロジェクトに応じて変更してください。
各ステージには、パイプライン内の具体的な処理、つまりジョブを含めることができます。1つのステージに1つのジョブを記述したり、1つのステージに複数のジョブを記述したり、ジョブを並列実行させたりすることもできます。
コラム コンテナイメージのビルドの設計指針
Docker社の「CI/CD Best practices」には、下記のような記述があります。
- コンテナにおけるCI/CDでは「inner loop」(ローカルマシン上でのコード修正、ビルド、実行、テストを行うサイクル)と「outer loop」(CIツール上のコードプッシュ、ビルド、テスト、デプロイを行うサイクル)の2つのビルド〜テストサイクルが存在する
- CI上(outer loop)での実行結果を見ながらデバッグするのは実行結果の取得まで時間がかかるので、開発者は通常ローカルマシン上(inner loop)でテストやデバッグを実施する
- このとき、CI上でのテスト結果とローカルマシン上での実行結果が同じであることが望ましい
ここで、Dockerfileに単体テストのステージを設け、「docker build」コマンドで単体テストを実行することにより、ローカルマシンとCIで同じコンテナを利用した同じテストが行え、同じテスト結果を簡単に得ることができます。そこで、テストはDockerfileの中でテストステージを設けてテストすることが推奨されます。
この考え方に従った実践例として、Docker社の記事が紹介されています。本実践例では、単体テストだけではなく、ビルド(コンパイル)やコードの静的解析をDockerfileのステージ(GitLabのステージとは異なります)で設け、各ステージをdocker buildで呼び出す仕組みです。
ここまでの内容を踏まえ、今回の実践例においてもdocker buildでDockerfileの各ステージをコマンドで呼び出せるように構成しました。
次に、各ステージのジョブ定義を見ていきます。
stage-buildステージ
# stage-buildステージ build: # ステージ stage: stage-build # 処理内容 script: - docker build --target codescan -t $CONTAINER_IMAGE:work . - docker build --target unittest -t $CONTAINER_IMAGE:work . - docker run $CONTAINER_IMAGE:work cat ./report.xml > report.xml - docker run $CONTAINER_IMAGE:work cat ./cover.txt - docker build --target build_image -t $CONTAINER_IMAGE:$CI_COMMIT_SHA . - docker push $CONTAINER_IMAGE:$CI_COMMIT_SHA # カバレッジ coverage: '/coverage: \d+\.\d+% of statements/' # アーティファクト artifacts: reports: junit: report.xml # CI実行契機 only: - merge_requests - main
stage-buildステージでは、下記処理を実行します。なお、下記のDockerfileのステージはDockerfileのマルチステージ機能で利用するステージで、GitLabのパイプラインのステージではないので、注意してください。Dockerfileのステージについては、後ほどDockerfileの説明の箇所で詳説します。
- Dockerfileのcodescanステージのコンテナイメージをビルド
- Dockerfileのunittestステージのコンテナイメージをビルド
- Dockerfileのunittestステージで作成した「report.xml」を読み込み、リダイレクト(結果をアーティファクトとして保存するため)
- Dockerfileのunittestステージで作成した「cover.txt」を読み込む(正規表現でカバレッジを取得するため)
- Dockerfileのbuild_imageステージのコンテナイメージをビルド
- Dockerfileのbuild_imageステージで作成したコンテナイメージをレジストリにプッシュ
上記処理の6.でコンテナイメージをプッシュしているのは、stage-scan、stage-update-latest-imageステージでこのイメージを使用するからです。docker buildで作成したローカルのコンテナイメージを、GitLab CIのジョブ/ステージに跨がって利用することはできません。そのため、ここでは、GitLabのコンテナレジストリをキャッシュとして利用しています。
コンテナイメージのキャッシュについては「Use Docker to build Docker images」のマニュアルにも記載があるので、ご参照ください。
coverage(カバレッジの登録)
coverage: '/coverage: \d+\.\d+% of statements/'
coverageキーワードでジョブのコードカバレッジを設定します。カバレッジは正規表現で記載する必要あり、Goでは「go test -cover」コマンドを実行すると、下記のように出力されます。
ok golang-ci/pkg/sample 0.546s coverage: 100.0% of statements
出力結果のうち、カバレッジ値「100.0」を抽出するので、上記正規表現となります。他の言語を使用する場合は、使用する言語の表示形式に合わせて正規表現を修正する必要があります。
GitLab上には図13のように表示されます。
artifacts: reports: junit: report.xml
artifacts:reports:junitキーワードを設定することで、「JUnit」形式で出力された「report.xml」をartifactとして保存しています。これにより、ジョブの実行結果にテストレポートを表示できます。
GitLab上には図14のように表示されます。
# CI実行契機 only: - merge_requests - main
onlyキーワードを指定することで、ジョブの実行タイミングを、マージリクエスト時とmainブランチコミット時に指定します。
Dockerfileの解説
次に、今回利用したDockerfileの内容を見ていきます。Dockerfileはステージで分かれているので、ステージごとに処理内容を解説します。
FROM golang:latest as codescan WORKDIR /go/src/gitlab.com/cloudnativetips/ci-sample COPY . . RUN go vet $(go list ./... | grep -v /vendor/) FROM golang:latest as unittest WORKDIR /go/src/gitlab.com/cloudnativetips/ci-sample COPY . . RUN go install github.com/jstemmer/go-junit-report@v1.0.0 && \ go test -race -v $(go list ./... | grep -v /vendor/) 2>&1 | \ go-junit-report -set-exit-code > report.xml && \ go test -race $(go list ./... | grep -v /vendor/) -cover > cover.txt FROM golang:latest as build_app WORKDIR /go/src/gitlab.com/cloudnativetips/ci-sample COPY . . RUN go build -o app cmd/main.go FROM scratch as build_image COPY --from=build_app /go/src/gitlab.com/cloudnativetips/ci-sample/app /app ENTRYPOINT ["/app"]
・ソースコード静的解析
FROM golang:latest as codescan WORKDIR /go/src/gitlab.com/cloudnativetips/ci-sample COPY . . RUN go vet $(go list ./... | grep -v /vendor/)
codescanステージでは「golang:latest」を「Docker Hub」から取得し、「go vet」(ソースコードの静的解析)を行います。このステージのみ、イメージを外部から取得するので、処理時間が長くなります。
・単体テスト
FROM golang:latest as unittest WORKDIR /go/src/gitlab.com/cloudnativetips/ci-sample COPY . . RUN go install github.com/jstemmer/go-junit-report@v1.0.0 && \ go test -race -v $(go list ./... | grep -v /vendor/) 2>&1 | \ go-junit-report -set-exit-code > report.xml && \ go test -race $(go list ./... | grep -v /vendor/) -cover > cover.txt
unittestステージでは「go test」(単体テスト)を行い、テスト結果をJUnit形式に変換します。また、「go test -cover」によりカバレッジも取得しています。golang:latestイメージはcodescanステージで取得したイメージを使うので、処理時間は短くなります(以降のステージも同様)。
・アプリケーションコンパイル
FROM golang:latest as build_app WORKDIR /go/src/gitlab.com/cloudnativetips/ci-sample COPY . . RUN go build -o app cmd/main.go
build_appステージでは「go build」(コンパイル)を行います。
・コンテナイメージビルド
FROM scratch as build_image COPY --from=build_app /go/src/gitlab.com/cloudnativetips/ci-sample/golang-ci/app /app ENTRYPOINT ["/app"]
build_imageステージではbuild_appステージで生成したバイナリを配置します。このステージは作成されたコンテナイメージをできるだけ小さくするために、イメージが極力小さい「scratch」を使用します。
stage-scanステージ
# stage-scanステージ scan: stage: stage-scan script: - docker pull $CONTAINER_IMAGE:$CI_COMMIT_SHA - wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf - - ./trivy --cache-dir .trivycache/ image --exit-code 0 --no-progress --format template --template "@contrib/gitlab.tpl" -o gl-container-scanning-report.json $CONTAINER_IMAGE:$CI_COMMIT_SHA - ./trivy --cache-dir .trivycache/ image --exit-code 1 --severity CRITICAL --no-progress $CONTAINER_IMAGE:$CI_COMMIT_SHA artifacts: reports: container_scanning: gl-container-scanning-report.json only: - merge_requests - main
stage-scanステージでは、Trivyでコンテナの脆弱性をスキャンします。scriptセクションで下記処理を実行します。
- stage-buildステージでプッシュしたコンテナイメージ(本解説では「alpine」にサンプルアプリケーションがデプロイされたイメージ)を取得
- Trivyをインストール
- 「trivy」コマンドで重大度を指定せずにスキャンし、カスタマイズしたフォーマットに結果を出力する
- trivyコマンドで重大度を「CRITICAL」に限定してスキャンし、かつ「--exit-code 1」オプションを指定することで、脆弱性を検知した場合に処理を終了する
artifacts: reports: container_scanning: gl-container-scanning-report.json
artifacts:reports:container_scanningキーワードを指定し、スキャンした結果を保存しています。GitLab上には図15のように表示されます。
なお、Trivyによるスキャンで脆弱性が検出された場合、下記のような情報が出力されます。
LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE |
---|---|---|---|---|---|
apk-tools | CVE-2021-30139 | UNKNOWN | 2.10.4-r3 | 2.10.6-r0 | -->avd.aquasec.com/nvd/cve-2021-30139 |
busybox | CVE-2021-28831 | HIGH | 1.31.1-r9 | 1.31.1-r10 | busybox: invalid free or segmentation fault via malformed gzip data -->avd.aquasec.com/nvd/cve-2021-28831 |
ssl_client | CVE-2021-28831 | HIGH | 1.31.1-r9 | 1.31.1-r10 | busybox: invalid free or segmentation fault via malformed gzip data -->avd.aquasec.com/nvd/cve-2021-28831 |
脆弱性の詳細は、脆弱性データベースサイトの「CVE(Common Vulnerabilities and Exposures)」で確認できます。CVEのサイトを参考にしながら、脆弱性に対応するとよいでしょう。
stage-update-latest-imageステージ
# stage-update-latest-imageステージ update-latest-image: stage: stage-update-latest-image script: - docker pull $CONTAINER_IMAGE:$CI_COMMIT_SHA - docker tag $CONTAINER_IMAGE:$CI_COMMIT_SHA $CONTAINER_IMAGE:latest - docker push $CONTAINER_IMAGE:latest only: - main
stage-update-latest-imageステージでは、scriptセクションで下記処理を実行しています。
- stage-buildステージでプッシュしたコンテナイメージ(本稿ではalpineにサンプルアプリケーションがデプロイされたイメージ)を取得
- 取得したコンテナイメージに「latest」タグを付与
- latestタグが付与されたイメージをレジストリにプッシュ
stage-build、stage-scanステージと異なり、本ステージはmainブランチマージ時のみ動作するようにしています。これは、マージリクエストの承認前の途中のCIの状態では、最新のイメージとして登録せずに、マージリクエストが承認されて、mainブランチにマージされたときに最新イメージとしてlatestタグを付けて登録するためです。
次回は、「Argo CD」でKubernetesにデプロイするCD
今回は、アプリケーションのコードからコンテナイメージまでを作成するCIについて、GitLab CIを例に解説しました。CIには、「アプリケーションのビルドテストをどのように行うか」(ここでは、Dockerfileから呼び出させるステージ上で実行)、CIの実行をどこで行うか」(マージリクエストが操作されたタイミング)、「テスト結果をどう格納するか」などを記述するのがポイントです。
次回は、KubernetesにデプロイするCDについて「Argo CD」を例に紹介していきます。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- GitLabが日本市場に本格参入、「単一製品でDevOpsの全ライフサイクルをカバー」
DevOpsプラットフォーム製品を提供するGitLabは2020年4月28日、日本市場への本格参入を発表した。同社のCEOであるシッツェ・シブランディ氏は、国内メディア向けのオンライン説明会で、同社の戦略について語った。 - CI/CDは何がまずいのか、コード作成から本番デプロイまでの時間短縮に注力
オブザーバビリティツールを手掛けるhoneycomb.ioの共同創業者でCTOを務めるチャリティ・メージャーズ氏が、CI/CDの問題点を指摘した。CIにばかり注力せず、CDにも気を配るべきであり、特にコード作成から本番環境へのデプロイまでの「時間の短縮」にフォーカスすべきだという。 - CI/CDの世界を標準化? 新たに発足した組織「Continuous Delivery Foundation」とは
The Linux Foundationが2019年3月12日(米国時間)に発表した、CI/CD関連の新ファウンデーション、「Continuous Delivery Foundation」。具体的にはどのような活動をすることになるのか。このファウンデーションの設立に関わった、Jenkinsの生みの親でCloudBeesのCTOである川口耕介氏に聞いた。