You are currently viewing jenkinsを導入してみる

jenkinsを導入してみる

CI/CDツールとしてjenkinsを導入してみました(個人開発アプリに)
今までデプロイは手動でターミナルからサーバーにログインしてコマンドを叩いていたのですが、それすらもめんどくさくなったのでjenkins画面上からボタン一つでデプロイできるように設定してみます

現在運用しているウェブサービスはtraefikをリバースプロキシとして利用し、書くサービスをDockerコンテナとして管理しているため、jenkinsについてもDockerコンテナで準備し、traefik管理下に配置します

docker composeファイル準備

jenkins用のdocker composeファイルを作成します

services:
  jenkins:
    image: jenkins/jenkins:lts-jdk21
    container_name: jenkins
    restart: unless-stopped
    user: "1000:1000"
    environment:
      - TZ=Asia/Tokyo
      # 2GB VPS向けにJVMメモリを最大512MBに抑制し、SerialGCでオーバーヘッドを極小化
      - JAVA_OPTS=-Xms128m -Xmx512m -XX:+UseSerialGC
    deploy:
      resources:
        limits:
          memory: 768M
    labels:
      - traefik.enable=true
      - traefik.http.routers.jenkins.rule=Host(`jenkinsのサブドメイン`)
      - traefik.http.services.jenkins.loadbalancer.server.port=8080
      - logging=promtail
    volumes:
      - jenkins-data:/var/jenkins_home
    networks:
      ride_webservice_network:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  jenkins-data:

networks:
  ride_webservice_network:
    external: true

レンタルしているVPSのメモリが少ないため、jenkinsが利用するメモリも768MBに制限しています
また、jenkinsコンテナからホストに設置されているデプロイスクリプトを起動させる必要があるためextra_hostsを利用してDockerのホストを参照できるようにしています
(docker.sockを直接マウントする方法もあるが、こちらはコンテナホストにroot権限を渡すことに等しく、jenkinsに侵入された際の被害が大きいので採用しなかった)

パイプラインの準備

jenkinsではパイプラインをコードで準備できるようになっていました
(自分が勤務していた会社ではフリースタイルプロジェクトにシェルをガリガリ書いて運用していた)

こちらのファイルは一部お見できない情報も含んでいるため、参考程度にご覧ください

pipeline {
    agent any

    options {
        // Jenkins コンテナ内に巨大リポジトリ(200MB超の Git LFS ファイル等)をチェックアウトするのをスキップ
        // (ホスト側で SSH 経由で pull・デプロイするため Jenkins 側への clone は不要)
        skipDefaultCheckout(true)
    }

    parameters {
        choice(
            name: 'TARGET_SERVICE',
            choices: ['Service1', 'Service2', 'Service3', 'ALL'],
            description: 'リリース対象のサービスを選択してください'
        )
        string(
            name: 'BRANCH',
            defaultValue: 'main',
            description: 'Git取得元ブランチ'
        )
        booleanParam(
            name: 'CONFIRM_DEPLOY',
            defaultValue: false,
            description: '【重要】本番リリースを実行する場合はチェックを入れてください'
        )
    }

    stages {
        stage('Release Confirmation') {
            steps {
                script {
                    if (!params.CONFIRM_DEPLOY) {
                        error('本番リリースチェックが無効です。誤操作防止のためジョブを中止しました。')
                    }
                }
            }
        }

        stage('Execute Deploy') {
            steps {
                script {
                    def targets = []
                    if (params.TARGET_SERVICE == 'ALL') {
                        targets = ['Service1', 'Service2', 'Service3']
                    } else {
                        targets = [params.TARGET_SERVICE]
                    }

                    sshagent(credentials: ['設定したクレデンシャル']) {
                        for (target in targets) {
                            echo "========================================="
                            echo "Deploying target service: ${target}"
                            echo "========================================="
                            sh """
                                ssh -o StrictHostKeyChecking=no user@host.docker.internal '
                                    set -e
                                    cd /home/user/${target}
                                    echo "Working directory: \$(pwd)"
                                    if [ -d .git ]; then
                                        echo "Pulling latest git changes..."
                                        git pull origin ${params.BRANCH} || git pull || true
                                    fi
                                    echo "Executing deploy.sh..."
                                    ./deploy.sh
                                '
                            """
                        }
                    }
                }
            }
        }
    }

    post {
        success {
            echo "✓ [${params.TARGET_SERVICE}] 本番リリース処理が正常に完了しました!"
        }
        failure {
            echo "✗ [${params.TARGET_SERVICE}] 本番リリース処理に失敗しました。ログを確認してください。"
        }
    }
}

設定ファイルはGroovyで記述します(これはAIに作ってもらった)

agent anyの指定で空いているjenkinsノードにジョブを割り当てます

jenkinsのジョブが開始されると、デフォルトでリポジトリをCloneする動作となるが、今回はDockerホスト側で個別にCloneを行うためデフォルト動作が起こらないようにskipDefaultCheckout(true)を設定しています

パイプラインの動作としては
・どのサービスをデプロイするのか選択
・本番リリース承認チェックボックスの状態を確認し、チェックが有ればデプロイを実行
・docker.host.internalでホストにSSHで接続し、デプロイ用シェルを実行
という動作を行います

jenkins画面上での動作設定

jenkinsの初回起動時、起動ログ内に管理者用パスワードが出力されているので、このパスワードを使用してjenkinsにログインします

まずはクレデンシャルを準備します
今回のパイプラインでは、以下のクレデンシャルが必要となりました
・DockerホストへSSH接続するためのクレデンシャル
・Githubよりリポジトリをクローンするためのクレデンシャル(今回はプライベートリポジトリのため)

クレデンシャルを作成するにはjenkins画面の右上にある歯車マークをクリックします

セキュリティの中にあるクレデンシャルをクリックします

右上のAdd Credentialsをクリックします

最初にSSHユーザーと秘密鍵 を選択し、NEXTをクリックします

このクレデンシャル情報はjenkins DockerコンテナからホストへSSH接続を行うための情報なので、ホストにSSH接続できるユーザーの情報を設定します

主な設定内容は
・スコープ:グローバルのまま
・ID:パイプラインに指定したID(sshagent(credentials: [‘設定したクレデンシャル’])の「設定したクレデンシャル」の部分)
・説明:説明なのでわかりやすい内容を記載
・ユーザー名:ホストへ接続するためのユーザー
・Treat username as secret:コンソールに出力されるユーザー名もマスク(****)表示する場合はチェック※注意あり
・秘密鍵:直接入力をチェックすると、入力用の欄が表示されるので秘密鍵の情報を入力

Treat username as secretについては、単純にコンソールに出力される文字列の一致でマスクを掛けているようで、よくある単語(userなど)を利用している場合、ファイルパスなどもマスクされてしまうそうです
例:ユーザー名として「user」を指定した場合
  コンソール出力で/home/user/projects→/home/*****/projestsと表示されてしまう

次にGithubからリポジトリをクローンするためのクレデンシャルを設定します
事前に、Github側でFine-grained personal access tokensを準備しておきます

ユーザー名とパスワードを選択し、NEXTをクリック

主な設定内容は以下になります
・スコープ:グローバルのまま
・ユーザー名:Githubのユーザー名
・Treat username as secret:必要に応じてチェック
・パスワード:Fine-grained personal access tokensを設定
・ID:わかりやすいID
・説明:わかりやすい内容を設定

jenkinsジョブを作成する

デプロイを行うジョブを設定します
デプロイ時に実際に行う作業(コマンド一覧)はパイプラインに設定されているため、ジョブでは利用するパイプラインの取得方法や、どのクレデンシャルを利用するのか、といった情報を設定するイメージになります

jenkinsトップページより「新規ジョブ作成」をクリックします

ジョブ名を入力し、パイプラインを選択して画面下のOKをクリックします

設定内容は以下のようにしました
説明にはわかりやすい説明を入力
ビルドのパラメータ化については、一旦チェックは入れず、ジョブを動かすことでjenkins側が自動でパラメータを認識します
(なので、1度ジョブを動かす必要あり)

トリガーブロックについては、どの項目にもチェックは入れませんでした
(今回はデプロイタイミングは手動のため)

パイプラインは以下のように設定しました
・Pipeline script from SCMを選択
・SCM:Gitを選択
・リポジトリURL:pipelineファイルが管理されているGithubリポジトリのURL
・認証情報:クレデンシャル設定で作成したGithub用の設定を指定

パイプライン後半の設定は以下のようにしました
・ブランチ識別子:デフォルトのブランチ(こちらはアプリのリリース用ブランチ)を指定します
・Script Path:パイプラインが定義されたファイルまでのパスを指定します

設定は以上です

jenkinsからデプロイしてみる

以上の設定でデプロイが動くはずなので、実行してみます

jenkinsトップページに追加したジョブが表示されます
ジョブ名をクリックします

パラメータ付きビルドをクリックします

パイプライン内に設定したパラメータが表示されるので、内容を設定してビルドをクリックします

ビルドを開始すると、左側に進捗が表示されます
時間の部分をクリックすると、詳細画面へ遷移します

Console Outputをクリックすれば、コンソール出力が閲覧できます
エラーなどが発生した場合はここから確認できます

以上です

ということで、jenkinsを導入してみた内容でした
これで、アプリのリリースはボタン一つで行えるようになったので、より運用が楽に行えるかと思います

こういった自動化は運用の負担が減るだけでなく、手作業によるミスもなくなるため一石二鳥ですね

コメントを残す