検索
連載

GitLabによるCI実践入門――Kubernetesで利用するコンテナイメージをビルドするCloud Nativeチートシート(6)

Kubernetesやクラウドネイティブをより便利に利用する技術やツールの概要、使い方を凝縮して紹介する連載。今回は、GitLabによるCI(継続的インテグレーション)について解説します。

PC用表示 関連情報
Share
Tweet
LINE
Hatena

 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はアプリケーションをソースコードから最終的にコンテナイメージを作成し、コンテナレジストリに登録するところまでを担当します。つまり、コンテナレジストリにイメージを登録できればゴールです。


図1 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を実行します。


図2 マージリクエストと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を行うことにより、次のようなメリットがあります。

  1. 機能の実装やバグフィクスごとにCIを実行でき、他の作業の影響を避けることができる
  2. mainブランチへのマージ前にCIの結果を確認でき、確実にCIが成功してからmainブランチにコードをマージできる
  3. マージリクエスト上でCIの結果が確認でき、CIの失敗履歴や、失敗に対応したソースコードの修正履歴を1つのマージリクエストの画面で追えるようになる
  4. マージリクエストを作成しなければ、作業途中のコードをブランチにコミットしても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」に移動して、右上の「フォーク」ボタンを押してください。


図3 右上の「フォーク」ボタンを押す

 フォークするネームスペース(ユーザー名もしくはグループ名)の「選択」を押してください。


図4 「選択」ボタンを押す

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)の「マージリクエスト」ボタンを押してください。


図5 「マージリクエスト」ボタンを押す

 「New merge request」画面が表示されるので、タイトルや内容を入力し、「Create マージリクエスト」ボタンを押してください。


図6 「Create マージリクエスト」ボタンを押す

5.CIの実行結果の確認

 先ほど作成したマージリクエスト上に時間がたつと、マージリクエスト上にCIの実行結果が表示されます。成功すれば、緑のチェックマークが表示されます。


図7 CIが成功した場合

 失敗すると、赤で×マークが表示されます。


図8 CIが失敗した場合

 マージリクエスト上のCIの実行結果にパイプラインIDが表示されています。


図9 CIの実行結果にパイプラインIDが表示される

 このパイプラインIDをクリックすると、パイプラインの実行状況の詳細が確認できます。


図10 パイプラインの実行状況の詳細画面

 この実行結果では、buildとscanのジョブが成功したことを示しています。失敗した場合は、赤の×ボタンが表示されます。

 また、各ジョブをクリックすると、ジョブの実行結果を確認できます。


図11 ジョブの実行結果

 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
.gitlab-ci.yml

 それでは、設定パラメーターを見ていきましょう。

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のようになります。


図12 dockerコンテナとdindコンテナの関係

 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ステージの構成としました。

  1. stage-build:コンテナイメージ作成処理(アプリケーションのコードスキャン、ビルド、単体テスト、カバレッジ取得、中間イメージのプッシュ)
  2. stage-scan:コンテナイメージのスキャン
  3. 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ステージ

 stage-buildステージでは、下記処理を実行します。なお、下記のDockerfileのステージはDockerfileのマルチステージ機能で利用するステージで、GitLabのパイプラインのステージではないので、注意してください。Dockerfileのステージについては、後ほどDockerfileの説明の箇所で詳説します。

  1. Dockerfileのcodescanステージのコンテナイメージをビルド
  2. Dockerfileのunittestステージのコンテナイメージをビルド
  3. Dockerfileのunittestステージで作成した「report.xml」を読み込み、リダイレクト(結果をアーティファクトとして保存するため)
  4. Dockerfileのunittestステージで作成した「cover.txt」を読み込む(正規表現でカバレッジを取得するため)
  5. Dockerfileのbuild_imageステージのコンテナイメージをビルド
  6. 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のように表示されます。


図13 GitLab上でのコードカバレッジの表示結果
  artifacts:
    reports:
      junit: report.xml

 artifacts:reports:junitキーワードを設定することで、「JUnit」形式で出力された「report.xml」をartifactとして保存しています。これにより、ジョブの実行結果にテストレポートを表示できます。

 GitLab上には図14のように表示されます。


図14 GitLab上でのテストレポートの表示結果
   # 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"]
Dockerfile

・ソースコード静的解析

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ステージ

 stage-scanステージでは、Trivyでコンテナの脆弱性をスキャンします。scriptセクションで下記処理を実行します。

  1. stage-buildステージでプッシュしたコンテナイメージ(本解説では「alpine」にサンプルアプリケーションがデプロイされたイメージ)を取得
  2. Trivyをインストール
  3. 「trivy」コマンドで重大度を指定せずにスキャンし、カスタマイズしたフォーマットに結果を出力する
  4. trivyコマンドで重大度を「CRITICAL」に限定してスキャンし、かつ「--exit-code 1」オプションを指定することで、脆弱性を検知した場合に処理を終了する
  artifacts:
    reports:
      container_scanning: gl-container-scanning-report.json

 artifacts:reports:container_scanningキーワードを指定し、スキャンした結果を保存しています。GitLab上には図15のように表示されます。


図15 GitLab上での脆弱性スキャンの表示結果

 なお、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

 stage-update-latest-imageステージでは、scriptセクションで下記処理を実行しています。

  1. stage-buildステージでプッシュしたコンテナイメージ(本稿ではalpineにサンプルアプリケーションがデプロイされたイメージ)を取得
  2. 取得したコンテナイメージに「latest」タグを付与
  3. 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.

[an error occurred while processing this directive]
ページトップに戻る