見出し画像

GitLab CI/CD のための Lint & Diff 導入事例 ~デプロイの安全性向上~


はじめに

こんにちは。株式会社ラキールの LaKeel DX Platform Group SRE Team に所属する高橋です。
私たちのチームでは、LaKeel DX サービスのインフラ整備や、開発者・運用者のサポートを行っています。

私たちが実装している GitLab CI/CD の Lint & Diff について、背景から導入効果までご紹介いたします。


GitLab CI/CD とは

GitLab CI/CD

まずは、ラキールがソフトウェア開発で使用している GitLab CI/CD についてご説明します。

GitLab CI/CD を使用すると、開発者がコードの変更を GitLab リポジトリにコミットした際に、自動的にビルド・テスト・デプロイが行われます。この一連のパイプラインにより、ラキールはより迅速に、より効率的に、品質を維持しながらソフトウェアをリリースできています。

GitLab CI/CD の基本概念


.gitlab-ci.yml

GitLab CI/CD は .gitlab-ci.yml という YAML ファイルを使用して構成されます。この .gitlab-ci.yml 内にビルド、テスト、デプロイの各ステージで実行するタスク(ジョブ)を定義します。

.gitlab-ci.yml のサンプルコード

ラキールでは、GitLab のリポジトリのブランチごとにデプロイする環境を分けています。例えば、develop ブランチにコミットしたら開発環境にデプロイされ、master ブランチにコミットしたら本番環境にデプロイされます。


環境変数

上記のサンプルコードを見ていただくと、${CI_PROJECT_NAME} や ${DEVELOP_HOST} のように ${} で括られた箇所があります。これらが 環境変数 で、パイプラインで使用するデータや設定を管理できます。
環境依存な箇所は、環境変数によって管理しています。

環境変数を適切に設定することで、機密情報をソースコードから分離して保管したり、環境ごとに異なる設定をしたりと、パイプラインの設定や管理をより安全かつ効率的に行うことができます。

ラキールでは、環境依存の環境変数には、例えば ${MANAGER_DEVELOP_HOST} や ${MANAGER_PRODUCTION_EKS_CLUSTER_NAME} のように、それぞれの環境を示す識別子を含めています。これにより、開発・ステージング・本番など、異なる環境へのデプロイに対応できています。


以上が、GitLab CI/CD の簡単な説明となります。
それでは、Lint & Diff 導入事例のご紹介に移ります。


Lint & Diff の導入事例

背景

SRE Team では、LaKeel DX 製品リリースプロセスの際、 .gitlab-ci.yml の編集を手作業で行っています。
しかし、この手作業による編集プロセスにはヒューマンエラーが生じるリスクがあります。
例えば、以下のようなケースが起こりえます。

  • 本来必要なオプションを削除してしまった!

  • 本番環境のデプロイジョブに開発環境用のオプションをセットしてしまった!

  • 存在しない環境変数をセットしてしまった!

特に、本番環境にデプロイしてからエラーが発生してしまうと、大事故になります。

このようなリスクを軽減するため、CI/CD のプロセスに Lint & Diff の仕組みを導入しました。この仕組みは .gitlab-ci.yml に Lint & Diff ジョブを導入することで実現しています。

Lint & Diff ジョブの導入により、手作業によるエラーを可視化し、デプロイ前にミスに気づくことが期待できます。また、デプロイ前後の変更を確実に把握できるようになり、意図しない変更やエラーを発見しやすくなります。

Lint & Diff の仕組みをジョブとして導入・実装


Lint ジョブ: 環境変数チェック

Lint ジョブでは、.gitlab-ci.yml 内の環境変数を検証します。この Lint ジョブは、環境変数が正しく設定されているかどうかをチェックし、以下のようなミスを検出します。

  • 存在しない・空の環境変数をセットしていないか

  • 環境依存な環境変数を、違う環境のジョブにセットしていないか

Lint に違反するとジョブが失敗し、以下の通り後続のジョブを実行できなくなります。これにより、誤った設定のままデプロイされるのを未然に防ぐことができます。

パイプラインが失敗

Lint ジョブの実行結果を見ることで、どの設定にミスがあったか分かります。  例えば以下①では、DEVELOP_HOST という環境変数が空だったことが分かります。

Lint ジョブの実行結果①

また、以下②のように、本番環境にデプロイするジョブ(deploy-manager-production-eks)に開発環境用の環境変数(MANAGER_DEVELOP_HOST)をセットしたような場合も Lint エラーとなります。

Lint ジョブの実行結果②


このように、Lint ジョブの導入により、環境変数の設定ミスをデプロイ前に気づくことができるようになりました。


実装
Lint ジョブは以下の sh スクリプトで実装しています(実際のコードから一部抜粋・簡略化)。

job_names=$(yq e '. | keys | .[]' .gitlab-ci.yml | awk '/(^deploy-.*-)/')
for job_name in $job_names; do
    job_content=$(yq e ".${job_name}" .gitlab-ci.yml)
    vars=$(echo "$job_content" | grep -o '\${[^}]*}' | tr -d '{}' | tr -d '$')
    env_type=$(echo "$job_name" | awk -F- '{ print toupper($3) }')
    lint "$env_type" "$vars"
done

まず、yq という YAML ファイルを操作できるコマンドラインツールを使用して .gitlab-ci.yml の内容を読み込み、デプロイジョブの名前とそのジョブ内のすべての環境変数を抽出します。

デプロイジョブの名前は deploy-service-develop-eks や deploy-service-production-eks のように、環境を示す識別子を含む命名規則に従っています。このジョブ名から環境識別子を取得します。

これらの情報を基に、各デプロイジョブごとに lint() 関数を実行します。

lint() {
    env_type=$1
    vars=$2
    count=0
    for var in $vars; do
        env_name=$(echo "$var" | awk -F_ '{ print $2 }')
        if [[ -z "$(printenv "$var")" ]]; then
            echo -e "\e[1;31m[ERROR] $var is empty.\e[0m"
            count=$(expr $count + 1)
        fi
        if [[ "$env_name" != "$env_type" ]]; then
            echo -e "\e[1;31m[ERROR] $var is not expected.\e[0m"
            count=$(expr $count + 1)
        fi
    done
    if [ "$count" -eq 0 ]; then
        echo "All environment variables linted, 0 variables failed."
    else
        echo "All environment variables linted, $count variable(s) failed."
    fi
}

lint() 関数では、確認対象の環境識別子と環境変数のリストを用いて、各環境変数に対して以下の処理を行います。

  • 環境変数が設定されているか(空でないか)をチェック

  • 環境変数が期待される環境識別子と一致しているかをチェック

  • lint ルールに違反した場合、エラーメッセージを出力


Diff ジョブ: デプロイ前後の差分チェック

LaKeel DX サービスは、Helm Chart を使用して動的にマニフェストファイルを生成し、Kubernetes クラスターにデプロイされています。

Diff ジョブでは、デプロイ時に .gitlab-ci.yml における変更(Helm Chart のオーバーライド)がマニフェストファイルに正しく反映されたかどうか、デプロイ前後のリソース状態を比較して差分を出力します。

例えば、.gitlab-ci.yml のデプロイジョブを以下のように変更してみましょう。

+      --set env.ENV_HOSTNAME=${MANAGER_DEVELOP_HOST}

すると、Diff ジョブの実行結果は以下①のようになり、MANAGER_DEVELOP_HOST の値である www.example.com が正しく設定できたことがわかります。

Diff ジョブの実行結果①

また、Kubernetes リソース値(CPU/Memory など)の変更も差分として出力されます。.gitlab-ci.yml のデプロイジョブを以下のように変更します。

-      --set resources.requests.memory=300Mi
-      --set resources.limits.memory=300Mi
+      --set resources.requests.memory=400Mi
+      --set resources.limits.memory=400Mi

Diff ジョブの実行結果は以下②のように出力され、Kubernetes リソース値の変更差分が一目で確認できます。

Diff ジョブの実行結果②

実装
Diff ジョブでは Kubernetes のコマンドラインツールである kubectl を使用して、デプロイの前後でマニフェストファイルの状態を比較します。
Pre-Deploy ジョブと Post-Deploy ジョブをそれぞれデプロイジョブの前後に配置して、Post-Deploy ジョブでマニフェストファイルの差分を出力します。

Pre-Deploy ジョブ: デプロイジョブ前に実行

resources=("deployment" "statefulset" "cronjob" "job" "service" "ingress")
for resource in ${resources[@]}
do
    matched_resources=$(kubectl get $resource -n ${NAMESPACE} --no-headers 2>/dev/null | grep "${CI_PROJECT_NAME}" | awk '{print $1}' || true)
    if [[ -n $matched_resources ]]; then
        touch before_${resource}.yaml
        for matched_resource in $matched_resources
        do
            kubectl get $resource -n ${NAMESPACE} $matched_resource -o yaml 1> temp.yaml 2> /dev/null || true;
            {
                cat temp.yaml | yq eval '.spec' - || true;
                cat temp.yaml | yq eval '.metadata.annotations' - || true;
                cat temp.yaml | yq eval '.metadata.labels' - || true;
            } >> before_${resource}.yaml
        done
    fi
done

Pre-Deploy ジョブでは、Deployment や Service など特定の Kubernetes リソースを対象にして、デプロイの前にそれらのマニフェストファイルの現在の状態を取得し、before_${resource}.yaml ファイルに保存します。

Post-Deploy ジョブ: デプロイジョブ後に実行

resources=("deployment" "statefulset" "cronjob" "job" "service" "ingress")
for resource in ${resources[@]}
do
    if [ -e "before_${resource}.yaml" ] && [ -e "after_${resource}.yaml" ]; then
        echo -e "\e[1m$resource\e[0m"
        diff=$(diff -u before_${resource}.yaml after_${resource}.yaml || true)
        if [ -n "$diff" ]; then
            echo "$diff" | awk '
            /^-[^-]/ {print "\033[1;31m" $0 "\033[0m"; next}
            /^[+][^+]/ {print "\033[1;32m" $0 "\033[0m"; next}
            {print}'
        else
            echo "No difference."
        fi
    elif [ -e "after_${resource}.yaml" ]; then
        echo -e "\e[1m$resource\e[0m"
        while IFS= read -r line; do
            echo -e "\033[1;32m+${line}\033[0m"
        done < after_${resource}.yaml
    fi
done

Post-Deploy ジョブでは、Pre-Deploy ジョブ同様に、デプロイ後の情報を after_${resource}.yaml ファイルに保存します。
その後、diff コマンドを使用して before_${resource}.yaml と after_${resource}.yaml の差分を取得し、その結果を色分けして出力します。追加された行は緑色、削除された行は赤色で表示されるため、どの部分が変更されたのかを視覚的に理解しやすくなります。


このように、Lint & Diff ジョブを導入することで、.gitlab-ci.yml の変更による意図しない変更や不具合を防ぐことができます。これにより、LaKeel DX サービスを安全にデプロイできるようになりました。

パイプラインが成功

おわりに

Lint & Diff の仕組みを導入することで、.gitlab-ci.yml の編集プロセスにおけるヒューマンエラーのリスクを大幅に削減できました。
これにより、LaKeel DX サービスのデプロイプロセスはより安全かつ効率的になり、信頼性の高いプロダクトリリースが可能になりました。

今後も私たち SRE Team は、LaKeel DX のインフラの安全性と効率性の向上に努めてまいります。