Dockerコンテナ上のJenkinsからホスト上のgitリポジトリを直接指定しローカルでの開発サイクルを上げる

# 背景

Jenkins Pipelineは便利ですよね。そのスクリプトの開発&動作検証はDockerなどでローカル端末に構築したJenkinsサーバを利用し、それらを用いて以下の3パターンのどれかで進めている人が多いのではないでしょうか。

1. テキストボックスに直接追記、編集しながら動作検証。終わったらスクリプトファイル化する
2. Jenkinsの[Replay](https://jenkins.io/doc/book/pipeline/development/#replay) 機能を利用
3. スクリプトにはロジックをほとんど記載せず、そこから呼び出すShell ScriptやNodejsに処理を寄せて、それらのShell ScriptやNodejsを個別にテストする

一方で、3のパターンで呼び出している Shell ScriptやNodejsが、Pipelineスクリプトと強く依存していたり、それらのファイルの呼び出し方が分からなかったりするケースも多く、とはいえ編集内容をいちいちgit pushして外部のSCM経由ではなくもっと気軽に動作検証したいという場合も多いのではないでしょうか？ 

開発生産性の観点や、使い捨てできる環境を作るという面でも、本記事を役立ててもらえればと思います。

# 前提

* JenkinsのPipelineスクリプトはローカルのパスを指定できない
    * SCM[^1]（GitやSVN）のリポジトリを指定する必要がある
    * このため、dockerのVolumeマウントでホスト側のファイルを見る手段は取れない


[^1]: Software Configuration Management


# 環境構成

ホスト側にSSHを利用したGitサーバをたてて、コンテナ内のJenkinsサーバから参照する方法を取ります。

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6463/cdfa0b8c-0707-a279-65ec-e56b2d1871cc.png)


# 環境

`Windows10`、 `Git 2.23`、 `OpenSSH for Windows7.7`です。

```bash
>ver
Microsoft Windows [Version 10.0.18362.295]

>git --version
git version 2.23.0.windows.1

>ssh -V
OpenSSH_for_Windows_7.7p1, LibreSSL 2.6.5
````

Macの場合はホスト側のSSHサーバの構築がおそらくグッと楽になるくらいで、ほぼ同じ手順で再現できると思います。


# 💻ホスト側

今回の手順ではコンテナ上からはHTTP(s)ではなくSSHでgitリポジトリを参照させるため、まずWindows側にSSHサーバを構築します。

## OpenSSH

Windows本体に組み込まれているので機能を有効にすればOKです。

1. Windowsの設定からアプリを開く
2. アプリと機能のページにあるアプリと機能の欄のオプション機能の管理を開く
3. 機能追加を開く
4. OpenSSH Clientをインストール
5. `ssh localhost` で接続できたらOK.

## デフォルトShellの変更

OpenSSHのデフォルトShellはWindowsの(多分)コマンドプロンプトになっているため、このままではsshを使ったgitクライアントが認識してくれません。そのためデフォルトでShellを`git bash` に変更します。

1. PowerSellを管理者モードで開く
2. デフォルトShellを変更
  * `New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Program Files\Git\bin\bash.exe" -PropertyType String -Force`
3. OpenSSHの再起動
  * `Restart-Service sshd`
4. `ssh localhost` で接続先のコンソールが変更されていればOK.

ホスト側の設定は以上です。


## sshd_configの設定

コンテナからのssh接続をパスワード無しログインで連携したいため、設定を変更します。

sshd_configがどこにあるかで諸説ありますが、私の環境では `C:\ProgramData\ssh\sshd_config` が有効でした。

以下のように`PubkeyAuthentication yes` とコメントを外してyesに必要に応じて書き換えてください。
ついでに、いざというときの切り分けのためにsshdのログも出力するようにしておきます。（ログは、`C:\ProgramData\ssh\logs` 配下に出力されます）。

また、私の環境では、 `AuthorizedKeysFile` が以下のように `__PROGRAMDATA__` といった変なパスが指定されていたためコメントアウトします。

```apache:sshd_config
PubkeyAuthentication yes

SyslogFacility LOCAL0
LogLevel DEBUG

# コメントアウトする
# Match Group administrators
#       AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
```


# 🐋コンテナ側

JenkinsはバージョンLTSを利用します。まずはDockerfile(composeのYAML)を作成.

```yaml:docker-compose.yml
version: "3"
services:
  jenkins:
    container_name: localjenkins
    image: jenkins/jenkins:lts
    ports:
      - 8080:8080
    volumes:
      - ./jenkins_home:/var/jenkins_home
```

続いて、コンテナイメージを取得し実行します。

```bash:ビルド
docker-compose up -d
```

ここからコンテナに接続して(!)の作業です。
ホストと鍵交換さえできればよいので本来は `docker build` で完結できる気もしますが、build中にホストのネットワーク解決出来ないだろうと思ったので、調査する気に慣れず素直にコンテナ内作業です。

```bash:コンテナ作業へ
docker exec -it localjenkins /bin/bash
```

SSH接続のための設定を行います。

```bash:ssh設定作業
# 同じホスト内でしか接続させないので、全部空指定でOK
ssh-keygen

# ホスト側に公開鍵を登録
# <User>は各環境のものに書き換えてください。host.docker.internalはホストOSを指します
ssh-copy-id <User>@host.docker.internal

# SSH接続確認を実施(接続できたらexitとかでログアウトしてください)
# "Permission denied (publickey,keyboard-interactive)." が出る場合は鍵交換が間違ったか、SSHサーバの設定不備が考えられます
ssh <User>@localhost

# SSHが繋がっていれば、git接続を確認
git ls-remote <User>@host.docker.internal:/c/projects/juv/.git HEAD

# git cloneを確認. ホスト側は C:\projects 配下にGitHubなどからgit cloneしたリポジトリがあるとします
# もしPermission errorが出ていたらLinuxユーザの権限を確認
cd /home
git clone <User>@host.docker.internal:/c/projects/<Your Repository>/.git
```

ここまででDockerコンテナ内から、ホストOSのgitリポジトリを参照できたことになります🎉
以下のようなエラーが出た場合は、リポジトリパスが間違っている可能性があります。最後は`/.git` なのでご注意ください。 `<Your Repository>.git` ではないです。

```
fatal: 'C:/projects/<Your Repository>' does not appear to be a git repository
fatal: Could not read from remote repository.
```

ちなみに、最後のcloneは `git clone ssh://<User>@host.docker.internal:/c/projects/<Your Repository>/.git` でも成功します。
お好みでどちらでもどうぞ。


## 🎩Jenkins設定

Jenkins起動時に出力されるAdmin passwordを取得します。 `docker logs localjenkins` で見つけましょう。

```log:Dockerログ
docker logs localjenkins

*************************************************************

Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

This may also be found at: /var/jenkins_home/secrets/initialAdminPassword

*************************************************************
```

続いて、以下の手順でセットアップします。ここからは通常のJenkinsJobを作成する手順なので適時読み替えをお願いします。

1. `localhost:8080` にブラウザでアクセス
2. さきほど取得したAdmin passwordを入力
3. Setupで、リコメンド設定をクリック
4. 「Create First Admin User」で任意のユーザを作成
5. 新規ジョブ作成で、例として `パイプライン` を選択
6. パイプラインの定義> SCM> Git > リポジトリに、 `<User>@host.docker.internal:/c/projects/<Your Repository>/.git` を入力
  * エラーが出なければOK. なにか問題があればパスやRSAファイル、SSHサーバの設定を確認ください
  * credentials は指定不要です
7. Script Pathにリポジトリに含まれるGroovyファイルを指定

これでホスト側のgitリポジトリを参照するジョブが作成できました。


## 🚀実行

例として参照するホスト側のGitリポジトリ直下に以下のようなechoだけするファイルを置いています。

```goovy:hello.groovy
pipeline {
    agent any
    stages {
        stage('hello') {
            steps {
                echo 'hello world'
            }
        }
    }
}
```

このGroovyファイルを実行するJenkins Jobを作成してから、実行すると想定通り`hello world` が出力。

```log:Jenkins実行ログ1
(略)
First time build. Skipping changelog.
[Pipeline] }
[Pipeline] // stage
[Pipeline] withEnv
[Pipeline] {
[Pipeline] stage
[Pipeline] { (hello)
[Pipeline] echo
hello world
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
```

エディタでhello.groovyのecho部分を`hello world hello world`と書き換えて、`git commit -m "update"` し、ジョブを再実行。

```log:Jenkins実行ログ2
（略）
Commit message: "update"
 > git rev-list --no-walk c8c2af6dde9272a1049b9cdcb38f04952fee9682 # timeout=10
[Pipeline] }
[Pipeline] // stage
[Pipeline] withEnv
[Pipeline] {
[Pipeline] stage
[Pipeline] { (hello)
[Pipeline] echo
hello world hello world
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
```

...認識していますね🎉

ということで、ホスト側のファイルを `git commit` さえしてしまえば、コンテナ上のJenkinsがその変更を取得してくれました。

今回は簡略化のためgroovyファイルだけの差分ですが、groovyからShell ScriptやNodejsのファイルを読み込んでいたとしても同様に認識してくれるため、いちいちリモートへgit pushする手間を省いて作業できるため多少は効率的に開発ができると思います。

さらに上を目指したい方向けですが、ジョブのトリガーをHTTPにして、gitのcommit-hookスクリプトで、コミットした瞬間にJenkinsジョブをキックするという手法も考案されています[^2]。パラメータ付きのジョブには利用しにくいかもしれませんが、条件によっては非常に有用な手法だと思いますのでぜひご検討ください。

[^2]: https://stackoverflow.com/questions/36309063/how-can-i-test-a-change-made-to-jenkinsfile-locally


## まとめ

* JenkinsはDockerで簡単に建てられるし、Pipeline Scriptでちょっとした修正の動作確認する場合は"Replay" 機能が便利
* ローカルのPipeline Scriptから呼ばれるNode.jsなどのスクリプトをPipeline Script経由で動かしたい場合は、ローカルファイルパスを参照できず、GitHubやGitLabなどのSCM経由になりやや手間
* ホスト上でSSHサーバを動かしGitサーバとしてJenkinsから参照させれば、git pushの手間は回避でき、開発生産性が少しは上がるし、気軽に使い捨てることができる環境を作れる


## 参考

* https://stackoverflow.com/questions/36309063/how-can-i-test-a-change-made-to-jenkinsfile-locally
* https://qiita.com/a4_nghm/items/b5b339dce43824e07531