株式会社ネットワールドのエンジニアがお届けする技術情報ブログです。
各製品のエキスパートたちが旬なトピックをご紹介します。

GitとCI/CDに関する知識ゼロのSEが、GitLabでコンテナイメージのスキャン結果によってイメージをプッシュする or しないパイプラインを作る

皆様こんにちは。SEの小池と申します。

前々回はコンテナレジストリ、前回はコンテナスキャンと、最近の本ブログはCI/CDパイプラインを使ったコンテナ関連処理祭りですが、今回はちょっとだけ応用編としてGitLabでコンテナスキャンを実装し、緊急性の高い脆弱性が無ければコンテナレジストリに登録させる処理を実装する話となります。

本記事の対象の方

  • GitLabのAuto DevOpsのコンテナスキャンの結果によって、コンテナレジストリへの登録可否の処理を分岐させたい方。
  • 前々回 及び 前回の本ブログをご覧になったうえで、ちょっと応用チックな処理を実装したい方。

今回のブログのゴール

このブログのゴールはこちらです。

今回のゴール
  • Auto DevOpsのコンテナスキャンの結果によって、コンテナレジストリへの登録可否を分岐させる。

このブログをお読みいただくにあたっての事前ご連絡事項

  • 本記事はオンプレミス版 (Self-Managed) のGitLab Enterprise Edition 14.10.4-ee (Ultimate) における仕様をベースに記載しております。それ以外のエディションやバージョンではこの記事に記載の通りではない可能性がございます。
  • 本記事は前回 (GitLab Auto DevOpsを使ったコンテナスキャンの実装) と 前々回 (GitLabのコンテナレジストリにイメージを登録する) を前提とした内容です。
  • 本記事ではGitLabのAuto DevOpsにおけるコンテナスキャンと、GitLabのコンテナレジストリがどういったものなのかの説明は記載しておりません。これについては恐れ入りますがGitLab Docs (こちら) をご参照ください。
  • 本記事のGitLabのGUIは日本語にローカライズした状態で掲載しております。それ以外の言語をご利用の方は適宜読み替えてください。

実装するナリオ

今回は以下のシナリオをGitLabで実現していこうと思います。

【実装するシナリオ】
  • CI/CDパイプラインを使って任意のイメージをビルドする。
  • ビルドしたイメージをAuto DevOpsを使ってスキャンする。
  • ビルドしたイメージに緊急度の高い脆弱性がない場合は、コンテナレジストリに登録する。
  • シナリオは3ステップで構成されます。
    まずGitLabのCI/CDパイプランを使って任意のコンテナイメージをビルドします。

    ビルドしたイメージを、Auto DevOpsのコンテナスキャンを使ってスキャンし、脆弱性があるかをチェックします。
    Auto DevOpsのコンテナスキャンは結果をjson形式のファイルに出力します。 (アーティファクト)

    スキャン結果が出力されたjson形式のファイル (アーティファクト) を参照し、脆弱性が何件あったかをチェックします。
    脆弱性が0件だった場合は、GitLabのコンテナレジストリにこのコンテナイメージを登録します。
    脆弱性が1件以上だった場合は、その旨のメッセージを出してCI/CDパイプラインのジョブを異常終了させます。
    アーティファクトが無いなど、上記以外のケースの場合もCI/CDパイプラインのジョブを異常終了させます。

    なお、今回の3つ目のシナリオの実装に際し、以下の.gitlab-ci.ymlファイルの内容を参考にさせていただきました。
    参考 : https://gitlab.cern.ch/gitlabci-examples/container_scanning/-/blob/master/.gitlab-ci.yml

    事前準備

    GitLabの Auto DevOps でコンテナスキャンを使うにあたっては以下いずれかのRunnnerが必要になります。

    • エクゼキューターが docker のRunner
    • エクゼキューターが kubernetes のRunner
    • エクゼキューターが shell 且つ Docker Engine がインストール済のRunner

    お手元のGitLab環境にこれを満たすRunnerがない場合は、GitLab Docsや弊社の過去のブログをご参照いただき、事前にご準備をお願い致します。
    Runnerのインストールに関するGitLab Docs : Install GitLab Runner | GitLab
    弊社の技術ブログ : GitとCI/CDに関する知識ゼロのSEが、GitLabでRunner (Docker) を登録するだけの話

    また、今回のシナリオではGitLabのコンテナレジストリを有効にする必要があります。
    オンプレミス版 (Self-Managed) の場合に限りデフォルトでは有効になっていない場合があります。
    その場合はGitLab Docs もしくは 弊社の技術ブログをご参照いただき、コンテナレジストリを有効にしてください。
    コンテナレジストリに関するGitLab Docs : GitLab Container Registry | GitLab
    弊社の技術ブログ : GitとCI/CDに関する知識ゼロのSEが、GitLabでCI/CDパイプラインを使ってコンテナレジストリにイメージを登録する話

    シナリオの実装

    シナリオ1 : CI/CDパイプラインを使って任意のイメージをビルドする

    まずはシナリオの1つ目、CI/CDパイプラインを使って任意のイメージをビルドします。

    【実装するシナリオ】
  • CI/CDパイプラインを使って任意のイメージをビルドする。
  • ビルドしたイメージをAuto DevOpsを使ってスキャンする。
  • ビルドしたイメージに緊急度の高い脆弱性がない場合は、コンテナレジストリに登録する。
  • このブログでは例として簡易なイメージをビルドしてみます。
    既にお手元のGitLabで、スキャンしたいイメージをCI/CDパイプラインでビルドしていらっしゃる場合は、この手順はスキップして2個目のシナリオの実装から実施してください。
    前回 及び 前々回 のブログと重複するため、不要な場合は読み飛ばしていただいて問題ありません。

    まず、今回の検証のために空のプロジェクト (Blank Project) を作成します。
    空のプロジェクトにDockerfile.gitlab-ci.ymlを追加します。
    Dockerfileの内容はご自由に設定なさってください。
    以下にサンプルを載せますので、こちらをコピーしていただいても大丈夫です。
    (コメントアウトしている2行目は重要度High以上の脆弱性が有るパターンを試すためのエントリです。)

    FROM hello-world
    #FROM centos/nginx-114-centos7:20201215-32cf52f
    

    次に、.gitlab-ci.ymlを作成し、先ほどのDockerfileを使ってイメージをビルドする処理を記載します。
    サンプルは以下の通りです。
    最終的にコンテナレジストリにイメージを登録することを念頭に、イメージのタグはGitLabのコンテナレジストリの命名規則に従っています。
    そのままコピーしていただいても、アレンジしていただいても、どちらでも大丈夫です。
    アレンジなさる場合はタグがGitLabのコンテナレジストリの命名規則を満たすように編集なさってください。
    参考 : GitLab Container Registry | GitLab

    stages:
      - build
    
    Build-SmapleJob:
      stage: build
      image: docker:20.10.17
      variables:
        # ビルドイメージの変数作成
        IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
      script:
        # ビルド
        - docker build --tag $IMAGE .
    

    変更をコミットし、mainブランチにDockerfile.gitlab-ci.ymlをマージしてください。
    なおこの際、マージリクエストの作成は任意です。

    この時点でパイプラインが成功しているかどうかを確認します。
    対象プロジェクトで [CI/CD] > [パイプライン] を開き、最新のパイプラインのステータスが 緑色で [成功] となっていることを確認します。

    以上が1つ目のシナリオの実装でした。
    なお、この時点ではイメージをビルドだけしてどこにも保存していませんが、最終的にコンテナレジストリに登録しますのでご安心ください。

    シナリオ2 : ビルドしたイメージをAuto DevOpsを使ってスキャンする

    前のシナリオでは、CI/CDパイプラインでイメージをビルドする処理を作成しました。
    次のシナリオでは、そのイメージをGitLabのAuto DevOpsを使ってコンテナスキャンを実行します。

    【実装するシナリオ】
  • CI/CDパイプラインを使って任意のイメージをビルドする。
  • ビルドしたイメージをAuto DevOpsを使ってスキャンする。
  • ビルドしたイメージに緊急度の高い脆弱性がない場合は、コンテナレジストリに登録する。
  • リポジトリに追加した.gitlab-ci.ymlファイルを使って、コンテナスキャンを有効にします。
    既存の.gitlab-ci.ymlにtestステージと、キーワードincludeを使ってジョブを追加します。
    (以下のサンプルの場合は3行目と、15~17行目を追加しています。)

    stages:
      - build
      - test
    
    Build-SmapleJob:
      stage: build
      image: docker:20.10.17
      variables:
        # ビルドイメージの変数作成
        IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
      script:
        # ビルド
        - docker build --tag $IMAGE .
    
    # イメージスキャン
    include:
       - template: Security/Container-Scanning.gitlab-ci.yml
    

    上記のymlファイルでコンテナスキャンは可能ですが、これだとすべての重要度の脆弱性を検知してしまいます。
    シナリオには緊急度の高い脆弱性という条件があるので、これを加えます。
    具体的にはAuto DevOpsのテンプレートで作成されるcontainer_scanningジョブに対してCS_SEVERITY_THRESHOLDという変数を使い、検知する脆弱性を限定します。
    今回のシナリオでは緊急度の高い脆弱性重要度 High 以上の脆弱性を指すものとし、.gitlab-ci.ymlファイルに重要度High以上の脆弱性のみを検知するように設定します。
    (以下のサンプルの場合は19~23行目を追加しています。)

    stages:
      - build
      - test
    
    Build-SmapleJob:
      stage: build
      image: docker:20.10.17
      variables:
        # ビルドイメージの変数作成
        IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
      script:
        # ビルド
        - docker build --tag $IMAGE .
    
    # イメージスキャン
    include:
       - template: Security/Container-Scanning.gitlab-ci.yml
    
    # イメージスキャンジョブのカスタム設定
    container_scanning:
      variables:
        # 指定した重要度以上の脆弱性を検知する Unknown/Low/Medium/High/Critical
        CS_SEVERITY_THRESHOLD: High
    

    参考 : Container Scanning | GitLab

    変更をコミットし、mainブランチに.gitlab-ci.ymlをマージしてください。
    この際、マージリクエストの作成は任意です。下図は差分を示した図です。

    この時点でパイプラインが成功しているかどうかを確認します。
    対象プロジェクトで [CI/CD] > [パイプライン] を開き、最新のパイプラインのステータスが 緑色で [成功] となっていることを確認します。

    以上が2つ目のシナリオの実装でした。

    シナリオ3 : ビルドしたイメージに緊急度の高い脆弱性がない場合は、コンテナレジストリに登録する

    前のシナリオでは、ビルドしたイメージをコンテナスキャンでスキャンする処理を実装しました。
    次のシナリオでは、コンテナスキャンの結果をチェックし脆弱性が0件だった場合はレジストリに登録する処理を実装します。

    【実装するシナリオ】
  • CI/CDパイプラインを使って任意のイメージをビルドする。
  • ビルドしたイメージをAuto DevOpsを使ってスキャンする。
  • ビルドしたイメージに緊急度の高い脆弱性がない場合は、コンテナレジストリに登録する。
  • まず、ステージを追加し、前の手順で作成したジョブのアーティファクトを参照する処理を作成します。
    (以下のサンプルの場合は4行目と、26行目以降を追加しています。)

    stages:
      - build
      - test
      - push_image
    
    Build-Job:
      stage: build
      image: docker:20.10.17
      variables:
        # ビルドイメージの変数作成
        IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
      script:
        # ビルド
        - docker build --tag $IMAGE .
    
    #イメージスキャン
    include:
      template: Container-Scanning.gitlab-ci.yml
    
    # イメージスキャンジョブのカスタム設定
    container_scanning:
      variables:
        # 指定した重要度以上の脆弱性を検知する Unknown/Low/Medium/High/Critical
        CS_SEVERITY_THRESHOLD: High
    
    #イメージスキャンの結果に基づくレジストリへの登録
    PushImage-Job:
      stage: push_image
      image: docker:20.10.17
      script:
        - |
          apk add jq;
          export vuln_counts=$(jq -e "( .vulnerabilities | length )" ./gl-container-scanning-report.json);
          echo 脆弱性の数 : $vuln_counts;
      dependencies:
        - container_scanning
    

    上のyamlファイルのジョブPushImage-Jobscriptでは以下を実行しています。

    • 32行目:アーティファクトがjson形式なので、jqコマンドをインストールしています。
    • 33行目:アーティファクトの中のキーvulnerabilitiesに入っているオブジェクト数を取得し、それを変数vuln_countsに代入しています。
    • 34行目:人間による目視確認用の出力。コンテナスキャンで見つかった脆弱性の数。
    • 35, 36行目:アーティファクトをフェッチするジョブのリストの定義。

    次に、アーティファクトから取得した脆弱性の数によって、コンテナイメージをレジストリに登録するかしないかの分岐処理を作成します。
    今回のシナリオの場合、アーティファクトに出力されている脆弱性の数が0個であればレジストリに登録し、1個以上ある場合はレジストリには登録しません。
    (以下のサンプルの場合は35~45行目を追加しています。)

    stages:
      - build
      - test
      - push_image
    
    Build-Job:
      stage: build
      image: docker:20.10.17
      variables:
        # ビルドイメージの変数作成
        IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
      script:
        # ビルド
        - docker build --tag $IMAGE .
    
    #イメージスキャン
    include:
      template: Container-Scanning.gitlab-ci.yml
    
    # イメージスキャンジョブのカスタム設定
    container_scanning:
      variables:
        # 指定した重要度以上の脆弱性を検知する Unknown/Low/Medium/High/Critical
        CS_SEVERITY_THRESHOLD: High
    
    #イメージスキャンの結果に基づくレジストリへの登録
    PushImage-Job:
      stage: push_image
      image: docker:20.10.17
      script:
        - |
          apk add jq;
          export vuln_countss=$(jq -e "( .vulnerabilities | length )" ./gl-container-scanning-report.json);
          echo 脆弱性の数 : $vuln_counts;
          if [ "$vuln_counts" -gt "0" ]; then
            echo 脆弱性が1個以上あるため、コンテナレジストリに登録できません。\(exit code : 101\)
            exit 101
          elif [ "$vuln_counts" -eq "0" ]; then
            echo 脆弱性が0個なので、コンテナレジストリに登録します。
            docker login --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASSWORD $CI_REGISTRY
            docker push $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
          else
            echo コンテナスキャン結果が不明な値のため、コンテナレジストリに登録できません。\(exit code : 102\)
            exit 102
          fi 
      dependencies:
        - container_scanning
    

    上のyamlファイルのジョブPushImage-Jobscriptでは以下を実行しています。

    • 35~37行目:脆弱性が1個以上あった場合の処理。コード101で異常終了させる。
    • 38~41行目:脆弱性が0個だった場合の処理。コンテナレジストリにログインし、イメージをプッシュする。
    • 42~45行目:上記のどちらにも当てはまらなかった場合の処理。コード102で異常終了させる。

    変更をコミットしてください。これにより、追加したステージpush_imageとジョブPushImage-Jobが実行されます。
    下図は差分を示した図です。(差分が多すぎて見切れていますが、実際は47行目まで更新しています。)

    この時点でパイプラインが成功しているかどうかを確認します。
    対象プロジェクトで [CI/CD] > [パイプライン] を開き、最新のパイプラインのステータスのボタンをクリックします。
    (パイプラインのステータスはスキャン対象のイメージにおける脆弱性の有無に依存するため、ここではあまり気になさらないでください。)

    ジョブPushImage-Jobのステータスを確認します。
    下図のように全てのジョブが成功している場合は、対象のコンテナイメージに重要度High以上の脆弱性が無く、且つ、コンテナレジストリに登録できているはずです。
    この場合、プロジェクトの [パッケージとレジストリ] > [コンテナレジストリ] に、対象のコンテナイメージが登録されていることが確認できます。

    ジョブPushImage-Jobが失敗している場合、[失敗したジョブ] タブからジョブの終了ステータスを簡易的に確認することができます。
    下図の場合、スキャン対象イメージに重要度High以上の脆弱性が27個あったため、登録しなかった旨が表示されています。

    下図の場合、コンテナスキャンジョブが失敗しているため、アーティファクトが作成されていません。
    そのため、ジョブPushImage-Jobは脆弱性の数とは異なる理由 (yamlファイルで設定した exit code : 102) で終了しています。

    以上が3つ目のシナリオ、緊急性の高い脆弱性が無ければコンテナレジストリに登録させる処理の実装でした。
    なお、もし、脆弱性のあるイメージをすぐに思いつかない・・・といった場合は、こちらのサンプルで提示したDockerfileにおいて、1行目 (Hello worldのエントリ) をコメントアウトし、2行目のコメントを外し、再度CI/CDパイプラインを実行してください。
    これでおそらく重要度High以上の脆弱性を1件以上検知するケースを試行することができます。

    【余談】筆者が検討したけど断念した実装方法

    今回のシナリオを実装するにあたり、筆者が「こんな方法で実装できないかな?」と思って検討したものの、色々な理由で実装を断念した方法をご紹介いたします。

    コンテナスキャンの結果を変数で取得したい

    これは筆者が

    検知した脆弱性の数を確認するのに毎回 jq コマンドをインストールするのはちょっと・・・。
    GitLabのことだから、コンテナスキャンの結果を変数で取得できるんじゃね?

    と思ったことが発端です。

    結論として、Auto DevOpsのコンテナスキャンの結果を取得できる変数は、2022年7月26日時点で存在しません。
    実はこの「コンテナスキャンで検知した脆弱性の数を格納する変数」は変数名CS_VULNERABILITY_THRESHOLDとして実装が検討されていたらしいのですが、実装の優先度が下がってしまった模様です。
    参考 : Add CS_VULNERABILITY_THRESHOLD var to Container Scanning (#213087) · Issues · GitLab.org / GitLab · GitLab

    上記のような便利な変数が無いとわかったため、おとなしくjqコマンドをインストールし、アーティファクトを参照して、脆弱性の有無を確認する方法を採用致しました。
    上記の参考URLのイシューのステータスはまだオープンなので、そのうち実装される・・・ことを願います。

    コンテナスキャンで脆弱性を検知したらジョブの終了ステータスを緑 (正常) 以外にしたい

    これは筆者が

    脆弱性が1件以上ある場合は container_scanning ジョブ自体を異常終了させたいな・・・。
    そうすれば後続のジョブでいちいちアーティファクトの中身を確認する必要なくね?

    と思ったことが発端です。

    結論として、Auto DevOpsのテンプレートをインポートして自動作成されたコンテナスキャンジョブcontainer_scanningの終了ステータスを、脆弱性の有無によって変更させることは難しいようです。
    コンテナスキャンのジョブcontainer_scanningにおいては、コンテナスキャンのスキャン自体が正常に終了しさえすれば、脆弱性の有無を問わず、ジョブは正常終了する仕様になっています。
    ただし、SAST、DAST 及び Fuzzingについてはスキャンジョブにallow_failureを設定することにより、脆弱性を検知した場合はそのスキャンジョブを正常終了以外のステータスで終了させることができる模様です。
    参考 : 🎨 Design: Update secure job status with corresponding exit code with correct icons (#300415) · Issues · GitLab.org / GitLab · GitLab, Security scanner integration | GitLab

    以上のことから、テンプレートによって自動作成されるコンテナスキャンジョブcontainer_scanningの終了ステータスを、脆弱性の有無によって変化させることは断念致しました。無念・・・。

    最後に

    この度はGitもCI/CDもよくわかっていないど素人SEによるGitLab検証ブログをお読みいただき、誠にありがとうございます。
    このブログの目標は以下のとおりでしたが、皆さまはいかがでしたでしょうか。


    今回のゴール
    • Auto DevOpsのコンテナスキャンの結果によって、コンテナレジストリへの登録可否を分岐させる。

    CI/CDパイプラインでビルドして、コンテナスキャンして、脆弱性が0件ならコンテナレジストリに登録して…と、簡単ではありながら、なかなかそれっぽい実装例となりました。
    この記事がGitLabを触り始めた方の一助となれば幸いにございます。


    GitLabに関するお問い合わせは、以下のフォームからお願い致します。
    GitLab製品 お問い合わせ

    今までのGitLabの検証ブログはこちらです。
    GitLab カテゴリーの記事一覧 - ネットワールド らぼ

    GitLab操作デモ動画 (基本編) を作ってみました。(音声の録音は自宅でiPhoneのボイスメモ使うという超低クオリティですが…。)
    つたない内容ではありますが、ご興味がおありでしたら是非ご視聴いただければと存じます。

    www.youtube.com