見出し画像

《第2回》CI/CDコードの作成・登録|GitLab-CI/CDでレガシー環境でのGitOps事始め

こんにちは。CAT推進グループ 小田柿 です。
だいぶ間が空いてしまいましたが、GitLabでレガシー環境でのGitOps事始め 第2回です。今回はSpring Bootデモアプリ用のCI/CDコードの作成・登録になります。

前回)《第1回》 GitLab CI/CD環境構築|GitLab-CI/CDでレガシー環境でのGitOps事始め


1. GitLabにおけるCI/CDコード

1.1. .gitlab-ci.yml

ソースコードを登録したリポジトリのルートディレクトリに.gitlab-ci.ymlを登録します。GitLabは自動的にこのファイルを読み込み、連携したGitLab-Runnerからのスクリプト実行が可能になります。

この.gitlab-ci.ymlをデモアプリリポジトリとデプロイスクリプトリポジトリの両方に登録し、それぞれにビルドジョブ群とデプロイジョブ群を記述することによってGitでバージョン管理されたInfrastructure as Code(=GitOps)が完成します。

1.2. .gitlab-ci.ymlの基本構造

基本的な.gitlab-ci.ymlの仕様は以下の公式ページを参照ください。この記事では.gitlab-ci.ymlの基本構造については各自この公式ページで押さえていただいたうえで、記載したサンプルコードを参照しながら使用した構文や設定だけを説明していきます。

GitLab CI/CD パイプライン設定リファレンス
https://gitlab-docs.creationline.com/ee/ci/yaml/


2. デモアプリ

デモアプリは特に凝ったものは必要ないので、Spring Initializer (https://start.spring.io/)でひな形を作り、ControllerとThymeleafのHTMLおよびJUnitテストコード(1テストのみ)を足しただけのものにしています。動作確認ができれば何でもよいです。

Spring Bootデモアプリ
.
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
   ├── main
   │   ├── kotlin
   │   │   └── com
   │   │       └── example
   │   │           └── demo
   │   │               ├── DemoApplication.kt
   │   │               ├── ServletInitializer.kt
   │   │               └── app
   │   │                   ├── TestController.kt    # コントローラー
   │   │                   └── TestValues.kt        # DTO
   │   └── resources
   │       ├── application.yml
   │       └── templates
   │           └── index.html                        # Thymeleafテンプレート
   └── test
       └── kotlin
           └── com
               └── example
                   └── demo
                       ├── DemoApplicationTests.kt
                       └── app
                           └── IntegrationTests.kt    # テストコード 


3. デモアプリ用CI/CDサンプルコード

3.1. サンプルCI/CDコードの構成

デモアプリおよびデプロイスクリプトの構成は以下のようになります。デモアプリの.gitlab-ci.ymlとdeployリポジトリ内の.gitlab-ci.yml、deploy-demo.shの3つがCI/CDコードとなります。

リポジトリ構成
.
├── demo
│   ├── .gitignore
│   ├── .gitlab-ci.yml         <= ビルドパイプライン&開発用デプロイパイプライン呼び出し
│   ├── build.gradle.kts
│   ├── gradle
│   ├── gradlew
│   ├── gradlew.bat
│   ├── settings.gradle.kts
│   └── src
└── deploy
    ├── .gitlab-ci.yml         <= デプロイパイプライン(開発/本番切替可)
    └── deploy-demo.sh         <= デプロイ用シェル(既存シェルを流用するイメージ感)

役割としては、デモアプリのリポジトリルートの.gitlab-ci.ymlにはビルドパイプライン(テストジョブ→ビルドジョブ→デプロイパイプライン呼び出しジョブ)を記述します。ビルドパイプラインはソースコードのpush/mergeをトリガーに実行されるほか、画面から直接実行することもできるようにします。

deployリポジトリ上の.gitlab-ci.ymlにはデプロイパイプラインを記述し、ビルドパイプラインからの呼び出しあるいは画面からの直接起動で実行できるようにします。デプロイパイプラインはデプロイ先環境の切替をきちんとできるようにし、ビルドパイプラインからの流れで実行されるときには本番環境へのデプロイが起動されないようにします。

そしてdeploy-demo.shは実際のデプロイ処理を記述したものになり、デプロイパイプライン(deploy > .gitlab-ci.yml)から呼び出される構造になります。この部分はAnsible等の構成管理ツールに置き換えればより高度にデプロイを制御することができるようになります。

次はコード上でどのように上記を実現するか見ていきます。


3.2. ビルドパイプライン用.gitlab-ci.yml

まずは、ソースコードのルートディレクトリに配置したビルドパイプライン用コードです。コード全体の構成とコードは以下のようになります。このコードで使用している特徴的な構文としては、別途定義したデプロイスクリプトのリポジトリ上のGitLab-CI/CDをAPI経由で呼び出すことで、ビルドパイプラインとデプロイパイプラインの分離を実現しています。その他の処理の詳細な意味はコード上にコメントとして記載してあるので本文での説明は割愛します。

<コード構成>

画像1

<コード>

demo > .gitlab-ci.yml
# 設定
stages:  # 処理のステージ構成
 # 複数のジョブに同じステージを指定すると、それらのジョブは並列実行される
 # 下記例では、Testジョブ群がまず並列実行され、全て成功するとBuildジョブ群に移り、…
 - Test
 - Build
 - Trigger
variables:  # 変数定義
 # ビルド成果物のアーカイブディレクトリ(ビルド用RunnerもGitLabサーバー上にいる前提)
 ARCHIVE_DIR: /mnt/archives
# 以降、ジョブを定義
# テストコード実行
Test:  # 予約語以外の最上位階層はジョブ名になる
 # ステージの指定(このジョブは"Test"ステージのジョブとして実行される)
 stage: Test
 # Runner指定(この例では"build"タグが付けられたRunnerだけが実行可能)
 tags: [build]
 # このジョブの実行条件(各条件(if)どうしはOR扱いになる)
 rules: 
   # デフォルトブランチにコミットがpushまたはmergeされた場合
   - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
   # プロテクトされたブランチ・タグのパイプラインを画面から直接起動した場合
   - if: $CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_REF_PROTECTED
 # テスト実行コマンド(before_script > script > after_scriptの順に実行される)
 # ※script内は配列でLinuxコマンドの1行が表現できる
 before_script:
   - chmod +x gradlew
 script:
   - ./gradlew clean test -PjunitReport=xml    # テスト実行
 # JUnit実行結果を画面からダウンロードできるようにする
 artifacts:
   reports:
     junit:
       - "./build/test-results/test/**/TEST-*.xml"
# ビルド
Build:
 stage: Build      # Testステージのジョブが全て完了してから呼び出される
 tags: [build]
 rules:
   - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
   - if: $CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_REF_PROTECTED
 # ビルドコマンド
 before_script:
   - chmod +x gradlew
 script:
   - ./gradlew clean build    # ビルド実行
 after_script:
   # 作成したwarのバージョンをブランチ・タグ名(バージョンを名前にする運用を想定)で置換
   - mv ./build/libs/demo-*.war ./build/libs/demo-$CI_COMMIT_REF_NAME.war
   # アーカイブディレクトリにコピー
   - cp ./build/libs/demo-$CI_COMMIT_REF_NAME.war $ARCHIVE_DIR
 # ビルド成果物を画面からダウンロードできるようにする
 artifacts:
   when: on_success                                 # ビルドジョブが成功した場合のみ
   name: demo-$CI_COMMIT_REF_NAME.war               # 後続の処理で使う場合はこの名前で指定
   paths:
     - ./build/libs/demo-$CI_COMMIT_REF_NAME.war    # 成果物の出力先パス
# 開発デプロイパイプライン呼び出し
Dev-Deploy-Trigger:
 stage: Trigger
 tags: [build]
 needs: [Build]
 rules:
   - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
   - if: $CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_REF_PROTECTED
 script:
   # GitLab APIを使って別リポジトリのCI/CDパイプラインを呼び出す。
   # 以下の例では、
   #   ・パイプライン:ProjectID=3のリポジトリのmasterブランチ
   #   ・引数:ENVIRONMENT="dev"、TARGET_APP=demo、VERSION=<パイプラインのブランチ・タグ名>
   # を呼び出している。
   # ⇒ 開発環境(dev)だけをCD自動化、本番環境(prod)は画面からの実行のみに制限
   - >-
     curl -s -X POST
     -F token=$CI_JOB_TOKEN
     -F ref=master
     -F variables[ENVIRONMENT]=dev
     -F variables[TARGET_APP]=demo
     -F variables[VERSION]=$CI_COMMIT_REF_NAME
     $CI_API_V4_URL/projects/3/trigger/pipeline


3.3. デプロイパイプライン用.gitlab-ci.yml

次にデプロイパイプラインを見ていきます。コード全体の構成とコードは以下のようになります。こちらについても処理の詳細についてはコード上のコメントを参照していたくとして、特徴的な構文としては次のようになっています。まず、ドット(.)始まりのジョブはRunnerの実行対象から外れるという仕様とextendsを使ってジョブの内容を継承・オーバーライドできるという仕様を利用して、開発デプロイと本番デプロイを別々のジョブに分離しています。さらにタグ(tags)機能を使ってそれぞれのジョブが実行可能なRunnerを開発用と本番用と切り分けることにより、開発・本番それぞれのデプロイパイプラインの分離を実現しています。

<コード構成>

画像2

<コード>

deploy > .gitlab-ci.yml
# 設定
stages:
 # デプロイ処理のみ
 - Deploy
variables:
 # ビルドアーカイブ情報
 ARCHIVE_HOST: gitlab
 ARCHIVE_DIR: /mnt/archives
 # デプロイ先情報
 DEPLOY_HOST_DEV: dev-server
 DEPLOY_HOST_PROD: prod-server
 DEPLOY_DIR: /opt/tomcat/webapps
# デプロイジョブ
.Deploy-Demo:  # "."始まりのジョブは実行時に無視される ⇒ 抽象クラスのように定義できる
 stage: Deploy
 script:
   # デプロイシェル呼び出し(リポジトリルート=カレントディレクトリ)
   # 引数:$1=アーカイブ先ホスト、$2=アーカイブ先Dir、$3=アーカイブ名、$4=デプロイ先ホスト
   - sh deploy-demo.sh $ARCHIVE_HOST $ARCHIVE_DIR demo-$VERSION.war $DEPLOY_HOST
Dev-Deploy-Demo:  # demoアプリ開発環境デプロイ
 extends: .Deploy-Demo    # デプロイ共通ジョブを継承 ⇒ 実行条件と変数を開発環境用に切り替え
 tags: [deploy, dev]      # ⇒ 開発デプロイ用Runner(GitLabサーバー上)が起動
 rules:
   # 実行条件で対象アプリ(demo)とデプロイ先環境(dev:開発)を指定
   - if: $TARGET_APP == "demo" && $VERSION != "" && $ENVIRONMENT == "dev"
 variables:
   DEPLOY_HOST: $DEPLOY_HOST_DEV    # デプロイ先ホストに開発サーバーを指定
Prod-Deploy-Demo:  # demoアプリ本番環境デプロイ
 extends: .Deploy-Demo    # デプロイ共通ジョブを継承 ⇒ 実行条件と変数を本番環境用に切り替え
 tags: [deploy, prod]     # ⇒ 本番デプロイ用Runner(本番踏み台サーバー上)が起動
 rules:
   # 実行条件で対象アプリ(demo)とデプロイ先環境(prod:本番)を指定、画面実行のみ許可
   - if: >-
       $TARGET_APP == "demo" && $VERSION != "" && $ENVIRONMENT == "prod" &&
       $CI_PIPELINE_SOURCE == "web"
 variables:
   DEPLOY_HOST: $DEPLOY_HOST_PROD   # デプロイ先ホストに本番サーバーを指定

3.4. デプロイ実行用シェル

最後にデプロイ実行用シェルです。こちらについては、ビルドしたSpringBootのデモアプリのWarファイルをGitLab-Runnerサーバー上にコピーし、さらにデプロイ先サーバーのTomcatの既存モジュールと置換してTomcatを再起動する、という流れのシェルになっています。ただし、このデプロイスクリプトを動かすにはGitLab-Runnerの実行環境上にSSH設定が必要で、その設定については次回説明します。また今回はサンプルコードのため単純なシェルにしましたが、Ansibleのような構成管理ツールを呼び出すようにすることでより高度なデプロイも可能になるので実運用では構成管理ツールと連携する形をお勧めします。

deploy > deploy-demo.sh
#!/bin/bash
ARCHIVE_HOST=$1
ARCHIVE_DIR=$2
ARCHIVE_NAME=$3
DEPLOY_HOST=$4
echo "ビルドアーカイブを取得"
scp ${ARCHIVE_HOST}:${ARCHIVE_DIR}/${ARCHIVE_NAME} /tmp/
if [ $? -ne 0 ]; then
 echo "エラー"
 exit 1
fi
echo "Tomcat停止・旧モジュール削除"
ssh ${DEPLOY_HOST} "sudo systemctl stop tomcat; sudo rm -rf /opt/tomcat/webapps/ROOT;"
if [ $? -ne 0 ]; then
 echo "エラー"
 exit 1
fi
echo "ビルドアーカイブをデプロイ"
scp /tmp/${ARCHIVE_NAME} ${DEPLOY_HOST}:/opt/tomcat/webapps/ROOT.war
if [ $? -ne 0 ]; then
 echo "エラー"
 exit 1
fi
echo "Tomcat再起動"
ssh ${DEPLOY_HOST} "sudo systemctl start tomcat;"
if [ $? -ne 0 ]; then
 echo "エラー"
 exit 1
fi
echo "後処理"
rm -f /tmp/${ARCHIVE_NAME}
exit 0


第2回 CI/CDコードの作成・登録は以上になります。
次回は踏み台サーバー(GitLab-Runnerサーバー)の環境設定をしたうえで、実際にCI/CDを実行してみようと思います。

__________________________________

執筆者プロフィール:小田柿 敦士
株式会社SHIFT で統合型ソフトウェアテスト管理ツール「CAT」の製品開発およびインフラ管理に携わっています。

《このライターの他の記事を読む》
《第1回》 GitLab CI/CD環境構築|GitLab-CI/CDでレガシー環境でのGitOps事始め

3分でわかるSHIFTについて
★SHIFTの導入事例はコチラ
★SHIFTの最新イベント情報はコチラ
★SHIFTの最新コラムはコチラ

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら   
https://recruit.shiftinc.jp/career/