メインコンテンツへスキップ
  1. Posts/

NCP(Naver Cloud Platform)を使ったインフラ構成 第2編. JenkinsとSourceDeployを使ったCI/CDとゼロダウンタイムデプロイの構成

NineKoo9
著者
NineKoo9
目次

第1編に続いて第2編を書いていきます。

第1編はこちらのリンクをクリックしてください!

Jenkinsサーバーの作成
#

NCPのServerに入り、Jenkins用として使う新しいサーバーを作成します。

新しく作成したJenkinsサーバーではGitHubのwebhookリクエストを受け取ります。 これを許可するために、GitHub webhookのリクエストIPをACGに設定する必要があります。

https://api.github.com/meta

上のリンクにアクセスしてhooksを検索し、4つのIPv4アドレスをinboundルールに追加します。

そして作成したJenkins用サーバーに接続します。

https://www.jenkins.io/doc/book/installing/linux/#debianubuntu

上のリンクを参考にしてJenkinsをインストールします。

sudo systemctl status jenkins

サーバーのインストール後に上のコマンドを実行して、緑色のactive(running)が表示されればJenkinsのインストールは成功です。

後でJenkinsがプロジェクトをビルドするために、自分のプロジェクトで使っているJDKバージョンに合わせてJDKもインストールします。

私はJenkinsインストール時と同様にJDK17を使用しているので、別途インストールはしません。

Object Storageバケットの作成
#

https://console.ncloud.com/objectStorage/objectStorageList

上のリンクにアクセスしてバケットを作成します。

JenkinsサーバーへのAWS CLIインストール
#

NCPのObject StorageはAWS S3の上に作られたプラットフォームなので、AWS S3のCLIと互換性があります。

https://cli.ncloud-docs.com/docs/guide-objectstorage

上のリンクを参考にしてインストールだけ行います。認証も一緒に行ってもいいですが、後でSourceDeployを使う時にもNCP認証情報が必要なので、後回しにします。

参考として、Ubuntu22.04はデフォルトでインストールされているPythonが3.10バージョンのため、AWS CLIを使用するにはpython 3.8以下をインストールする必要があります。

SourceDeployの作成
#

SourceDeployを使用するには、SourceDeploy agentをデプロイ先のサーバーにインストールしておく必要があります。この部分は第1編で適切にインストールされているはずだと思います。

SourceDeployに入り、デプロイプロジェクト作成をクリックします。

好きな名前を指定して

デプロイ環境設定でデプロイターゲットをAuto Scalingに選択します。

そして第1編で作成したAuto Scaling Groupを選択します。 私の場合は以前に作っていたプロジェクトなので、Auto Scaling Group名が第1編とは異なる点をご了承ください:)

デプロイプロジェクトが作成されたらクリックしてデプロイシナリオを作成します。

ゼロダウンタイムデプロイ戦略を選択します。Auto Scalingに適用するゼロダウンタイムデプロイ戦略は、基本(Rolling)またはブルー/グリーンデプロイ戦略のみ可能です。カナリアデプロイ戦略はncloud kubernetesで可能だと思います。

詳細はこちらの公式ドキュメントリンクをクリックしてください!

そしてデプロイ後に既存のAuto Scaling Groupをどうするかを選択できます。便宜上、維持するようにします。

デプロイファイルを取得する場所を選択します。ここではJenkinsがビルド後にObject Storageにビルドされたファイルをアップロードするので、Object Storageを選択します。

そしてデプロイファイルを選択するのですが、

設定していると、デプロイファイルを事前にアップロードしておく必要があったため、zipに圧縮したjarファイルをあらかじめアップロードしておきました。

zip -r nutridiary.zip build/libs/nutridiary-0.0.1-SNAPSHOT.jar

圧縮コマンドは上のコマンドを参考にしてください!

各ステップごとのコマンドを説明すると、

「デプロイ前実行」はブルーグリーンデプロイ方式によって新しく作成されたAuto Scaling Groupのサーバーに適用されます。サーバーが新しく作成されたとき、以前作っておいたinit scriptが実行されて既存のデプロイ前のjarファイルが実行されているはずです。そのためこれを停止する必要があり、新しいjarファイルとの競合を防ぐために既存のjarファイルを削除します。

kill -15 $(pgrep -f nutridiary-0.0.1-SNAPSHOT.jar) && rm /root/deploy/nutridiary-0.0.1-SNAPSHOT.jar

2番目の「ファイルデプロイ」はObject Storageからサーバーへファイルを転送するステップです。Object StorageでzipF圧縮されたフォルダを基準に自分のjarファイルの位置を正確に記入し、サーバーに保存するjarファイルの位置を指定してください。

3番目の「デプロイ後実行」で新しいjarファイルをバックグラウンドで実行します。

nohup java -jar /root/deploy/nutridiary-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > /root/deploy/output.log 2>&1 &

上のコマンドを入力します。

最後にデプロイシナリオを作成します。

さて、Jenkinsに戻りましょう!

GitHub Access Tokenの発行
#

JenkinsサーバーからGitHubリポジトリにアクセスするにはaccess tokenを発行する必要があります。 またはJenkinsサーバーでSSHキーを生成して認証する方法もありますが、ここではaccess token形式で行います。

自分のGitHubアカウントのSettingsでDeveloper Settingsをクリックします。

右側のGenerate new tokenボタンをクリックします。

access token名を指定して、repoとadmin:repo_hookを選択します。

下部のGenerate tokenを選択すると、上のトークン値が表示されます。

トークン値は一度しか表示されないので安全な場所にコピーしておき、 JenkinsでGitHub Repositoryにアクセスできるようにcredentialを登録する必要があります。

JenkinsへのGitHubトークン登録
#

Webブラウザで「JenkinsサーバーのパブリックIP:8080」にアクセスします。

そして上の赤い部分に表示されたURLを参考にして、Jenkinsの初期パスワードをコピーして貼り付けます。

私は最初のボタンを選択し、必要なプラグインをすべてインストールします。

その後会員登録を進めて、

Jenkins画面にアクセスしたら「Jenkinsの管理」をクリックして「Credential」をクリックします。

赤いボックスのglobalをクリックします。

右上の「Add Credential」をクリックします。

kindはUsername with passwordを選択して、UsernameにGitHubのID、Passwordに発行したトークン値を貼り付けます。 ID値は任意に指定します。

追加でNCP情報もCredentialに事前登録します
#

リンクにアクセスしてAccess keyとSecret keyをコピーします。

そしてkindはSecret textを選択して、SecretにそれぞれコピーしたNCPの値を入力し、IDには任意に決めます。

後でJenkins pipelineスクリプトでそのIDで環境変数を設定することを事前に参考にしてください!

すべて登録すると上の画像のようになります。

実際にはこのようにNCPの情報をJenkinsサーバーに保存することは危険なことがあります。もしJenkinsサーバーがハッキングされたら、Access keyが全部漏洩してしまいます。

AWSの場合はAMIベースのRoleを持ってJenkinsサーバーからAWSサービスにアクセスすることを許可できます。しかしNCPはお問い合わせの結果、そのような機能はサポートされていないようですね…

GitHub webhookの設定
#

Jenkinsの管理に入り、Pluginsをクリックします。

GitHub integrationプラグインを検索して選択し、インストールします。

GitHubプロジェクトのリポジトリのSettingsに入ります。 そしてWebhooksをクリックしてAdd webhookをクリックします。

Payload URLに自分のJenkinsサーバーのアドレスと後ろに「/github-webhook/」を追加します。

これで自分のGitHubリポジトリにコミットが発生すると、Jenkinsサーバーにwebhookが送信されます。

Jenkinsパイプラインの構成
#

Jenkinsサーバーにアクセスして「新しいアイテム」をクリックします。

名前を指定してPipelineを選択します。

自分のGitHubリポジトリのアドレスを入力します。 入力しなくても構いませんが、入力しておくと、 右の画像のようにJenkinsプロジェクトのタブからプロジェクトのGitHubリポジトリに移動するボタンが生成されます。

各項目を上のように構成して、Credentialは先に登録したものを使用します。

ビルドするブランチはmainと記入します。

最後にScript PathはJenkinsfileと記入します。

Jenkinsスクリプトの作成
#

pipeline {
    agent any
    environment {
        API_ACCESS_KEY = credentials('ncp-api-access-key')  
        API_SECRET_KEY = credentials('ncp-api-secret-key')
    }
    stages {
        stage('Prepare') {
            steps {
                echo 'Preparing...'
                git branch: 'main', url: 'https://github.com/f-lab-edu/nutri-diary.git'
            }
            post {
                success {
                    echo 'Preparation completed successfully!'
                }
                failure {
                    echo 'Preparation failed!'
                }
            }
        }
        stage('Build') {
            steps {
                sh './gradlew clean build'
            }
            post {
                success {
                    echo 'Build completed successfully!'
                }
                failure {
                    echo 'Build failed!'
                }
            }
        }
        stage('Upload') {
            steps {
                sh 'chmod +x ./script/upload.sh'
                sh './script/upload.sh'
            }
            post {
                success {
                    echo 'Upload completed successfully!'
                }
                failure {
                    echo 'Upload failed!'
                }
            }
        }
        stage('Deploy') {
            steps {
                sh 'chmod +x ./script/deploy.sh'
                sh './script/deploy.sh'
            }
            post {
                success {
                    echo 'Deploy completed successfully!'
                }
                failure {
                    echo 'Deploy failed!'
                }
            }
        }
    }
    post {
        success {
            echo 'Build, Test, and Deploy completed successfully!'
        }
        failure {
            echo 'Build or Deploy failed!'
        }
    }
}

最初の行ではJenkinsに登録しておいたNCPのCredential情報を環境変数に設定します。

そして各ステップごとにビルド後にupload.sh、deploy.shを実行します。 (upload.sh、deploy.shスクリプトは以下にあります)

Jenkins pipelineスクリプトは上のように作成し、ファイル名は「Jenkinsfile」(拡張子なし)と指定します。 そしてJenkinsfileはプロジェクトの最上位に置き、upload.sh、deploy.shはscriptフォルダを作ってその中に入れます。

upload.sh
#

SourceDeployシナリオコマンド
#!/bin/bash
zip -r nutridiary.zip build/libs/nutridiary-0.0.1-SNAPSHOT.jar
aws configure set aws_access_key_id ${API_ACCESS_KEY}
aws configure set aws_secret_access_key ${API_SECRET_KEY}
aws --endpoint-url=https://kr.object.ncloudstorage.com s3 rm s3://${your_bucket_name}/nutridiary.zip
aws --endpoint-url=https://kr.object.ncloudstorage.com s3 cp nutridiary.zip s3://${your_bucket_name}/nutridiary.zip

まずビルドされたファイルを圧縮します。

このときファイルの圧縮パスとSourceDeployコマンドのパスに注意してください!

2〜3番のコマンドは環境変数に登録したaccesskeyとsecretkeyで認証を行います。

4〜5番のコマンドはそれぞれ既存のObject Storageにあるzipファイルを削除して、新しいzipファイルをアップロードします。

deploy.sh
#

#!/bin/bash
# Naver Cloud Platform API 基本設定
SOURCEDEPLOY_API_URL="https://vpcsourcedeploy.apigw.ntruss.com"
PROJECT_NAME="nutridiary"
# ヘッダー設定(API認証)
get_auth_headers() {
  local method="$1"
  local uri="$2"
  local api_timestamp=$(perl -MTime::HiRes -e 'printf("%d\n", Time::HiRes::time()*1000)')
  local signature=$(generate_signature "${method}" "${uri}" "${api_timestamp}")
  # ヘッダーを明示的に配列で管理してcurlに渡す
  headers=(
    -H "x-ncp-apigw-timestamp: ${api_timestamp}"
    -H "x-ncp-iam-access-key: ${API_ACCESS_KEY}"
    -H "x-ncp-apigw-signature-v2: ${signature}"
  )
}
# Signature生成関数(API呼び出しに必要)
generate_signature() {
  local method="$1"
  local uri="$2"
  local time_stamp="$3"
  local nl=$'\\n'
  SIG="${method}"' '"${uri}"${nl}
  SIG+="${time_stamp}"${nl}
  SIG+="${API_ACCESS_KEY}"
  SIGNATURE=$(echo -n -e "${SIG}"|iconv -t utf8 |openssl dgst -sha256 -hmac ${API_SECRET_KEY} -binary|openssl enc -base64)
  echo "${SIGNATURE}"
}
# 1. プロジェクトIDを取得
get_project_id() {
  local project_name="$1"
  local uri="/api/v1/project?projectName=${project_name}"
  # ヘッダー準備
  get_auth_headers "GET" "${uri}"
  # プロジェクト一覧を取得
  response=$(curl -s -X GET "${SOURCEDEPLOY_API_URL}${uri}" "${headers[@]}")
  # プロジェクトIDをパース
  project_id=$(echo "${response}" | jq -r '.result.projectList[0].id')
  # エラー処理: project_idがない場合はエラーメッセージを出力
  if [[ -z "${project_id}" || "${project_id}" == "null" ]]; then
    echo "Error: プロジェクトIDが見つかりません。"
    exit 1
  fi
  # project_idを返す
  echo "${project_id}"
}
# 2. ステージIDを取得(プロジェクトIDが必要)
get_stage_id() {
  local project_id="$1"
  local uri="/api/v1/project/${project_id}/stage"
  # ヘッダー準備
  get_auth_headers "GET" "${uri}"
  # ステージ一覧を取得
  response=$(curl -s -X GET "${SOURCEDEPLOY_API_URL}${uri}" "${headers[@]}")
  # ステージIDをパース
  stage_id=$(echo "${response}" | jq -r '.result.stageList[0].id')
  # エラー処理: stage_idがない場合はエラーメッセージを出力
  if [[ -z "${stage_id}" || "${stage_id}" == "null" ]]; then
    echo "Error: ステージIDが見つかりません。"
    exit 1
  fi
  # stage_idを返す
  echo "${stage_id}"
}
# 3. シナリオIDを取得
get_scenario_id() {
  local project_id="$1"
  local stage_id="$2"
  local uri="/api/v1/project/${project_id}/stage/${stage_id}/scenario"
  # ヘッダー準備
  get_auth_headers "GET" "${uri}"
  # シナリオ一覧を取得
  response=$(curl -s -X GET "${SOURCEDEPLOY_API_URL}${uri}" "${headers[@]}")
  # シナリオIDをパース
  scenario_id=$(echo "${response}" | jq -r '.result.scenarioList[0].id')
  # エラー処理: scenario_idがない場合はエラーメッセージを出力
  if [[ -z "${scenario_id}" || "${scenario_id}" == "null" ]]; then
    echo "Error: シナリオIDが見つかりません。"
    exit 1
  fi
  # scenario_idを返す
  echo "${scenario_id}"
}
# 4. デプロイ開始リクエスト
start_deploy() {
  local project_id="$1"
  local stage_id="$2"
  local scenario_id="$3"
  local uri="/api/v1/project/${project_id}/stage/${stage_id}/scenario/${scenario_id}/deploy"
  # ヘッダー準備
  get_auth_headers "POST" "${uri}"
  response=$(curl -s -X POST "${SOURCEDEPLOY_API_URL}${uri}" "${headers[@]}")
  # レスポンスからhistoryIdを抽出
  history_id=$(echo "${response}" | jq -r '.result.historyId')
  # エラー処理: history_idがない場合はエラーメッセージを出力
  if [[ -z "${history_id}" || "${history_id}" == "null" ]]; then
    echo "Error: デプロイリクエストに失敗しました。"
    exit 1
  fi
  # デプロイ成功
  echo "デプロイが開始されました。History ID: ${history_id}"
}
# 実行フロー
PROJECT_ID=$(get_project_id "${PROJECT_NAME}")
STAGE_ID=$(get_stage_id "${PROJECT_ID}")
SCENARIO_ID=$(get_scenario_id "${PROJECT_ID}" "${STAGE_ID}")
## デプロイ開始
start_deploy "${PROJECT_ID}" "${STAGE_ID}" "${SCENARIO_ID}"

https://api.ncloud-docs.com/docs/devtools-sourcedeploy

deploy.shで実行するSourceDeploy APIの詳細情報は上のリンクを参考にしてください!

追加で上のスクリプトではJSON値をパースするために「jq」ライブラリを使用しているので、Jenkinsサーバーに接続してapt-get install jqでインストールしてください!

まとめ
#

これですべて完了しました。テストするためにGitHubプロジェクトに任意のコミットを適用すると、NCPのSourceDeployを活用したゼロダウンタイムデプロイまで適用されるはずです。

もし不足している部分があればコメントで教えてください!