👨‍🦰

docker + docker composeでJenkinsを使ってみる

に公開

はじめに

今更ですが、試しにJenkinsを使う環境を構築したので、備忘録として残します。
(GitHub Actionsの方が良いのかもしれませんが。。。)

agent(node)側のdocker imageについて

ネットで調べるとjenkinsci/ssh-slaveの記事が結構ヒットしたので、を使おうとしていたのですが、Deprecatedようですね。
それに気づくのに時間を使ってしまいました。。。

現在はjenkinsciではなく、jenkinsにあるagentのイメージを使えば良さそうです。
せっかくなので、本記事はjenkins/inbound-agentjenkins/ssh-agentを使ってみます。

前提環境

  • Docker: 26.1.3
  • Docker Compose: v2.27.0

事前準備:フォルダとファイル構成の作成

作業用フォルダを作成します。(今回はjenkins)
その配下は下図のように構成します。

jenkins/
├── compose.yml
├── .env
├── controller/
│   ├── Dockerfile
│   └── jenkins_home/
├── ssh-agent/
│   └── Dockerfile
├── inbound-agent/
│   └── Dockerfile
└── secrets/

ssh鍵の作成

ssh-agentとの接続で使用するssh鍵を作成します。
(-Nはパスフレーズを指定するオプションです。)

# ssh-keygen -t ed25519 -b 4096 -C "" -f secrets/jenkins_agent_key -N ""
# chmod 600 secrets/jenkins_agent_key
# chmod 644 secrets/jenkins_agent_key.pub

作成されたpubキーの中身を.envファイルに記載します。
(ssh-agentが起動したときにauthorized_keysに自動で登録される。)

.envファイル
JENKINS_AGENT_SSH_PUBKEY=<pubキーの内容>

ディレクトリ、ファイルの所有者の変更

jenkinsイメージ上でjenkinsユーザーがアクセスするディレクトリやファイルはオーナーを変更しておかないとエラーとなります。
jenkinsユーザーは(UID 1000)のようです。

# chown -R 1000:1000 controller/jenkins_home
# chown 1000:1000 secrets/jenkins_agent_key

dockerとdocker composeファイル作成

Controler

controller/Dockerfile
FROM jenkins/jenkins:lts

# 推奨:Docker-in-DockerやSSHを使う場合は、以下を許可
USER root

# 必要に応じてツールを追加
RUN apt-get update && \
    apt-get install -y ssh && \
    apt-get install -y openssh-client

# Jenkins用SSHディレクトリ作成(実行ユーザーに権限を与える)
RUN mkdir -p /var/jenkins_home/.ssh && \
    chown -R jenkins:jenkins /var/jenkins_home/.ssh && \
    chmod 700 /var/jenkins_home/.ssh

#COPY --chown=jenkins:jenkins secrets/jenkins_agent_key /var/jenkins_home/.ssh/id_rsa
#RUN chmod 600 /var/jenkins_home/.ssh/id_rsa

# 言語を日本語に設定
#RUN localedef -i ja_JP -f UTF-8 ja_JP.UTF-8 && \
#    echo 'LANG="ja_JP.UTF-8"' > /etc/locale.conf
RUN echo 'LANG="ja_JP.UTF-8"' > /etc/locale.conf
ENV LANG ja_JP.UTF-8

# 日付を日本語に設定
#RUN echo 'ZONE="Asia/Tokyo"' > /etc/sysconfig/clock && \
#    rm -f /etc/localtime && \
#    ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
USER jenkins

Node(ssh-agent)

ssh-agent/Dockerfile
FROM jenkins/ssh-agent

# 必要であれば、Node.jsやJava、Docker CLIなどを追加
USER root
RUN apt-get update && \
    apt-get install -y curl

# 言語を日本語に設定
#RUN localedef -i ja_JP -f UTF-8 ja_JP.UTF-8 && \
#    echo 'LANG="ja_JP.UTF-8"' > /etc/locale.conf
RUN echo 'LANG="ja_JP.UTF-8"' > /etc/locale.conf
ENV LANG ja_JP.UTF-8

# 日付を日本語に設定
#RUN echo 'ZONE="Asia/Tokyo"' > /etc/sysconfig/clock && \
#    rm -f /etc/localtime && \
#    ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
#USER jenkins

Node(inbound-agent)

inbound-agent/Dockerfile
FROM jenkins/inbound-agent

# 必要であれば、Node.jsやJava、Docker CLIなどを追加
USER root
RUN apt-get update && \
    apt-get install -y curl

# 言語を日本語に設定
#RUN localedef -i ja_JP -f UTF-8 ja_JP.UTF-8 && \
#    echo 'LANG="ja_JP.UTF-8"' > /etc/locale.conf
RUN echo 'LANG="ja_JP.UTF-8"' > /etc/locale.conf
ENV LANG ja_JP.UTF-8

# 日付を日本語に設定
#RUN echo 'ZONE="Asia/Tokyo"' > /etc/sysconfig/clock && \
#    rm -f /etc/localtime && \
#    ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
USER jenkins

compose

compose.yml
services:
  jenkins-controller:
    build:
      context: ./controller
    container_name: jenkins-controller
    hostname: jenkins-controller
    ports:
      - "8080:8080"
      - "50000:50000"  # エージェントとの接続に使用
    tty: true
    volumes:
      - ./controller/jenkins_home:/var/jenkins_home
      - ./secrets/jenkins_agent_key:/var/jenkins_home/.ssh/id_rsa:ro
    networks:
      jenkins-net:
        ipv4_address: 192.168.1.2
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/login"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s

  inbound-agent:
    build:
      context: ./inbound-agent
    container_name: inbound-agent
    hostname: inbound-agent
    depends_on:
      jenkins-controller:
        condition: service_healthy
    environment:
      - JENKINS_AGENT_WORKDIR=${JENKINS_AGENT_WORKDIR}
      - JENKINS_URL=${JENKINS_URL}
      - JENKINS_SECRET=${JENKINS_SECRET}
      - JENKINS_NAME=${JENKINS_NAME}
    tty: true
    networks:
      jenkins-net:
        ipv4_address: 192.168.1.3

  ssh-agent:
    build:
      context: ./ssh-agent
    container_name: ssh-agent
    hostname: ssh-agent
    depends_on:
      jenkins-controller:
        condition: service_healthy
    environment:
      - JENKINS_AGENT_SSH_PUBKEY=${JENKINS_AGENT_SSH_PUBKEY}
    tty: true
    networks:
      jenkins-net:
        ipv4_address: 192.168.1.4

networks:
  jenkins-net:
    ipam:
      driver: default
      config:
        - subnet: 192.168.1.0/24

初回起動

コンテナ起動

docker compose up -dコマンドでコンテナ起動をします。
-dオプションを付けないとコンテナ内のログはコンソールへ出力され続けます。
今回は別コンソールでdocker compose logs -fコマンドで確認しています。

# docker compose up -d
[+] Building 1.6s (25/25) FINISHED                                              docker:default
 => [jenkins-controller internal] load build definition from Dockerfile                   0.0s
 => => transferring dockerfile: 1.08kB                                                    0.0s
...
[+] Running 4/4
 ✔ Network jenkins_jenkins-net   Created                                                  0.2s
 ✔ Container jenkins-controller  Healthy                                                 22.9s
 ✔ Container inbound-agent       Started                                                 23.6s
 ✔ Container ssh-agent           Started                                                 23.5s
#

docker psコマンドで全てのコンテナが起動していることを確認します。

jenkins-controller内のknow_hostsにssh-agentを追加するため、下記コマンドを実行しておきます。
コンテナーを新しくするたびにこの作業は必要となります。(毎回新しい鍵が作成されるため)

# docker exec jenkins-controller bash -c "ssh-keyscan -t rsa ssh-agent >> /var/jenkins_home/.ssh/known_hosts"

(上記はcompose.yml内でssh-agent -> jenkins-controllerという順に起動するように設定すれば、Dockerfile内などで自動設定することが可能そうです。)

Webアクセス

次に、ControllerへWebアクセスして、Nodeの設定と変数の確認をしていきます。
その前に管理者アカウントのパスワードを下記コマンドで確認しておきます。

# cat controller/jenkins_home/secrets/initialAdminPassword

http://<dockerマシンのipアドレスまたはhostname:8080/へアクセスすると下図のような画面が表示されるので、上記で確認したパスワードを入力して「Continue」をクリックします。

Customize Jenkins画面が表示されます。今回は「Install suggested plugins」を選択します。

インストールされるまで待ちます。

Create First Admin User画面が表示されますが、今回はskipします。
Instance COnfiguration画面が表示されるので、URLを確認して「Save and Finish」をクリックします。
するとJenkinsダッシュボードへアクセスできるようなります。

Node設定

ssh-agent

Jenkinsダッシュボード画面で「Jenkinsの管理」>「Nodes」と遷移します。
「New Node」をクリックします。
New node画面で下記の値を設定して「Create」をクリックします。

  • ノード名:ssh-agent
  • Type:Permanent Agentにチェック

ノードの詳細を設定する画面で下記の値を設定して、「保存」をクリックします。

  • リモートFSルート:/home/jenkins
  • ラベル:ssh-agent
  • 起動方法:SSH経由でUnixマシンのスレーブエージェントを起動
    • ホスト:ssh-agent
    • 認証情報:「追加」>「Jenkins」とクリック。
      • 認証情報:
        • 種類:SSHユーザー名と秘密鍵
        • スコープ:グローバル
        • ID:jenkins
        • ユーザー名:jenkins
        • 秘密鍵:作成した秘密鍵を直接コピペ
          (Host Key Verification Strategyは「Non verifying Verification Strategy」の方がknown_hostsをチェックしないからラク?)

上記の設定でノードとしてssh-agentが追加され、同期中になっていれば大丈夫です。
もし、下記のようなエラーで同期されなかった場合は、jenkins-controller内の.sshやknown_hostsのオーナーおよびパーミッションを確認してみてください。

/var/jenkins_home/.ssh/known_hosts [SSH] No Known Hosts file was found at /var/jenkins_home/.ssh/known_hosts. Please ensure one is created at this path and that Jenkins can read it.
Key exchange was not finished, connection is closed.

他に、下記のようなエラーメッセージが出力される場合は、ssh-agentコンテナー内のjenkinsユーザーの.ssh/authorized_keysへpubキーが登録されているか確認してください。

ERROR: Server rejected the 1 private key(s) for jenkins (credentialId:jenkins/method:publickey)

inbound-agent

ssh-agentと同様に追加していきます。
Jenkinsダッシュボード画面で「Jenkinsの管理」>「Nodes」と遷移します。
「New Node」をクリックします。
New node画面で下記の値を設定して「Create」をクリックします。

  • ノード名:inbound-agent
  • Type:Permanent Agentにチェック

ノードの詳細を設定する画面で下記の値を設定して、「保存」をクリックします。

  • リモートFSルート:/home/jenkins
  • ラベル:inbound-agent
  • 起動方法:Launch agent by connecting it to the controller

ノードが作成されますが、同期されていないと思います。
ノード名のリンクをクリックし、Agent inbound-agent画面へ進み、Run from agent command lineの情報をコピーしておきます。

curl -sO http://<ip or hostname>:8080/jnlpJars/agent.jar
java -jar agent.jar -url http://<ip or hostname>:8080/ -secret <secret value> -name "inbound-agent" -webSocket -workDir "/home/jenkins"

上記の値を.envファイル内に記載します。

.env
JENKINS_AGENT_SSH_PUBKEY=<pubキーの内容>
JENKINS_URL=http://jenkins-controller:8080/
JENKINS_AGENT_WORKDIR=/home/jenkins
JENKINS_SECRET=<secret value>
JENKINS_NAME=inbound-agent

上記の設定をして、コンテナーを再起動または再作成(known_hosts関連の再設定が必要)します。

再起動の場合
# docker compose stop
# docker compose up -d
再作成の場合
# docker compose down
# docker compose up -d

pipelineを試しに動かしてみる

試しにジョブを作成して動かしてみます。
Jenkinsダッシュボード上で「新規ジョブ作成」をクリックします。

  • ジョブ名:pipeline-test
  • Select an item type:パイプライン
  • General
    • 定義:Pipeline script
    • Script:下記のJenkinsfileの内容
Jenkinsfile
pipeline { // Declarative pipelineであることを宣言する
    //agent any // 環境の指定(anyなので指定なし)
    //agent { label 'jenkins-controller'} //実行する環境を指定
    agent { label 'ssh-agent'}
    stages{
        stage('Hello') {
            steps {
                sh '''
		        TEST_FILE_NAME=file_$(date +"%Y%m%d%I%M%S").txt
		        touch ${TEST_FILE_NAME}
		        echo 'Hello World' > ${TEST_FILE_NAME}
		        echo "hostname: $(hostname)" >> ${TEST_FILE_NAME}
		        echo "user: $(whoami)" >> ${TEST_FILE_NAME}
		        echo "time: $(date)" >> ${TEST_FILE_NAME}
		        echo "printenv: $(printenv)" >> ${TEST_FILE_NAME}
		    '''
	    }
	    post { //ステップ終了処理
		always { //常に実行
		    sh '''
		    echo "end Hello stage"
		    '''
		}
		success { //成功時
		    sh '''
            echo "success Hello stage"
		    '''
		}
		failure{ //失敗時
            echo "========Fail……========"
		}
      }
	}
	stage("check"){
	    steps{
	        echo 'checking'
	    }
	}
    }
    post{
        always{
	    echo "========Finish========"
	}
    }
}

その他

今回のcompose.ymlファイルではcontroller->agentの順で起動するように指定しましたが、
agent->controllerの方が良いのかな?agentからcontrollerへの接続は試行してくれないということもどっかの記事で見た気がするが。。。

あと、それぞれのコンテナで日本語と日本時間の設定をするようにしたのですが、うまく動作していない。。。

参考

Discussion