(WIP) jenkins+Docker+GitBucket+Electronでのビルド環境構築

# TL;DR
GitBucketとJenkins、Dockerを使ってビルドやテストのためのCI環境を構築するための手順。

# まだ書いていないこと
* SSH鍵の登録方法
* Jenkinsの設定は一通りinit.groovyで行うようにしてしまう
* Dockerのディレクトリは-vで適切に設定する
* **Electronのサンプルソース**

# やりたいこと
1. GitBucketの任意のグループとしてプライベートリポジトリを作成し、ビルド用のJenkinsファイルを配置する。
2. Jenkins上からGitHub Organization folderを経由してGitBucketに接続し、ビルド対象リポジトリをスキャン
3. GitBucketのPullRequestのタイミングでJenkinsのビルドが起動
4. JenkinsのビルドはDockerPluginを通してDockerサーバーをslave起動して、ビルドを行う
5. 結果をGitBucketにStatusAPIを通して通知する

# イメージ
絵に書くとこんな感じになります。シンプルです。
![1f117eec-715a-42b8-babb-71ea790a7c09.png](https://qiita-image-store.s3.amazonaws.com/0/11214/13d8d5bd-5705-af87-6767-8832d4a8fce5.png)

# ゴール
**GItBucketでPullRequestを出したら実行可能なexe(Electronアプリ)が生成される**

エンドユーザー(**GItBucket**)からの要求を、SIer(**Jenkins**)が下請け(**Docker**)に流して働かせ、なにくわぬ顔で成果物を手に入れます。と言ったら一部の方には想像しやすいですかね?

# 主な登場人物

## Jenkins
CIサーバー。バージョンは2.39。AmazonLinux2016.09上に生息。

Master-Slave構成を構築します。SlaveはDockerインスタンスとして稼働させるため、実行時はMasterからDockerサーバーに接続します。
GitBucketと連携するために[GitHub Organization folder](https://wiki.jenkins-ci.org/display/JENKINS/GitHub+Organization+Folder+Plugin)を、DockerSlaveを利用するために、[Docker Plugin]()を今回利用します。

## GitBucket
GitHubクローン。バージョンは4.8。AmazonLinux2016.09上に生息。

 **GitHubは使えないけどGitBucketは社内で利用できる人** が今回の対象。GitHubが使える幸せな人は読む必要ありません。GitBucketのAPIをGitHub Organization Folderを利用して無理やり幸せになろうとしています。

なお、GitBucketのAPIの不足分を補うためにJekninsから直接GitBucketに接続するのではなく、
Jenkins <==> Nginx <==> GitBucketという流れを踏むことになります。

## Docker
コンテナベースの仮想環境。バージョンは1.12.1(APIは1.24)。CoreOS alpha (1214.0.0)上に生息。
alphaはたまたま。

Dockerfileを利用して事前に仮想環境を構築したイメージを準備しておきます。
Jenkinsから接続され、実際のビルドやテストはこのコンテナ内部で実行されます。

# 環境構築

| サーバー | バージョン | 同居するモジュール  |
|--------|----------|-------------|
| Jenkins   | 2.39 | Nginx |
| GitBucket | 4.8 | Jetty |
| Docker    | 1.12.1 | (コンテナ上に) nodejs |

## GitBucketサーバー

### インストールから起動
1. [GItBucket4.8](https://github.com/gitbucket/gitbucket/releases/tag/4.8)をダウンロードします。
2.  好きなアプリケーションサーバー(私はJetty)に配置して起動します。
3. `http://GITBUCKET_SERVER:8080`でアクセスできることを確認してください(ここでは便宜的に`GITBUCKET_SERVER`というhost名で進めていきます)。
4. ルーティング用にWebサーバーをインストールします(私はNginxを入れました)。
5. Nginxの設定を以下のように書き換えます
   (参考:[Multibranch PipelineによるJenkinsとGitBucketの連携](http://int128.hatenablog.com/entry/2016/10/06/224444))。

    ```/etc/nginx/nginx.conf
    # serverの定義に以下をlocation設定を追加します
    location ~ ^/[^/]+/[^/]+\.git.*$ {
            return http://GITBUCKET_SERVER:8080/git$request_uri;
    }

    location ~ ^/api/v3/users/(.*)$ {
            try_files $uri /api/v3/orgs/$1;
    }

    location / {
            proxy_pass http://GITBUCKET_SERVER:8080;
            client_max_body_size 10M;
            proxy_set_header Host $host;
    }
    ```
6. Nginxの設定を反映させます

    ```bash
    nginx -s reload
    ````
7. `http://GITBUCKET_SERVER/`でアクセスできることを確認します。
8. `root`/`root`(初期ユーザー)でログインし、任意のグループを作成します(ここでは`JGROUP`という名前のグループを作ります)。
8. Nginxを経由したことでGitBucketでは受け付けていなかった以下のAPIが実行できるか確認します。

    ```bash
    # jqはお好みで
    $ export GITBUCKET_SERVER=127.0.0.1
    $ export GROUP_NAME=JGROUP
    # nginxの設定をしていない場合は、{ "message": "Not Found" } が返る
    $ curl http://$GITBUCKET_SERVER:8080/api/v3/users/$GROUP_NAME | jq .
    {
      "message": "Not Found"
    }
    # nginxの設定をしている場合はグループの情報が返る
    $ curl http://$GITBUCKET_SERVER/api/v3/users/$GROUP_NAME | jq .
    {
      "login": "JGROUP",
      "email": "JGROUP@devnull",
      "type": "Organization",
      "site_admin": false,
      "created_at": "2016-01-19T00:55:00Z",
      "url": "http://GITBUCKET_SERVER:8080/api/v3/users/JGROUP",
      "html_url": "http://GITBUCKET_SERVER:8080/JGROUP",
      "avatar_url": "http://GITBUCKET_SERVER:8080/JGROUP/_avatar"
    }    
    ```

### ユーザー設定とリポジトリ作成
1. GitBucketにログインします。
2. Jenkins用のユーザーアカウントを追加します。
3. Jenkins用のSSH鍵を登録しておきます(TODO)。
    12. `http://GITBUCKET_SERVER:8080/jenkins/_ssh`へ移動
    13. 秘密鍵を2つ登録します(登録の仕方は省略)。  
        * jenkins => jenkinsマスターから接続を行うためのssh鍵情報です。
        * jenkins-slave => Docker上のインスタンスから接続を行うためのssh鍵情報です。
12. 上記で作ったJGROUPに作成したjenkisユーザーを追加し、グループに任意のリポジトリを作成します。
13. テスト用のリポジトリに以下のようなJenkinsfileを登録しておきます。

    ```groovy:Jenkinsfile
    node("ubuntu-base") {
        stage("hello") {
            echo "world"
        }
    }
    ```
これでGitBucket側の設定は終了です。

## Dockerサーバー

### Dockerサーバーの設定
1. 好きなDokcerのホストーサーバーを起動します(私の場合はCoreOSを選択)
2. `tcp`プロトコルで外部から接続可能なように設定を変更します  
    3. `docker-tcp.socket`を設定します。

        ```ini:/etc/systemd/system/docker-tcp.socket
        [Unit]
        Description=Docker Socket for the API
        
        [Socket]
        ListenStream=2376
        BindIPv6Only=both
        Service=docker.service
        
        [Install]
        WantedBy=sockets.target
        ```   
      ***注意事項*** (参考:[インターネットの向こう側にあるDockerを使う](https://beatsync.net/main/log20141212.html))
      * tcpでの接続を全許可していますのでセキュリティ的に問題があります。
      * 当初[Protect the Docker daemon socket](https://docs.docker.com/engine/security/https/)で設定しようとしたのですが、JenkinsのDockerPluginがtcpかunixプロトコルしか受け付けていなかったため、あきらめました(実際にはインフラレベルでIP単位でアクセス制限をかけるつもりです。まだやってないけど)。
    3. dockerのプロセスを再起動します

        ```bash
        $ sudo systemctl enable docker-tcp.socket
        $ sudo systemctl stop docker
        $ sudo systemctl start docker-tcp.socket
        $ sudo systemctl start docker
        ```
3. 接続確認をします。Jenkinsサーバーから`curl`が通るか確認します。

    ```bash
    $ curl http://127.0.0.1:2376/images/json
    ```

### ベースイメージの構築
1. Jenkinsから接続することが可能なベースとなるDockerImageを構築します。
     今回はこんな感じにベースとなるDockerfileを構築しました(雑です)。

     ```Dockerfile
    FROM ubuntu:16.10
     
    #-----------------------------------------------------------
     
    # 各種必要なライブラリのインストール
    # jenkinsユーザーの追加
    # ssh接続を許可
    # 一部後述するelectronの環境構築に必要なライブラリを含む
    RUN set -x \
      && apt-get update \
      && apt-get -y install --no-install-recommends \
          sudo \
          software-properties-common \
          openssh-server \
          curl \
          wget \
          git \
          unzip \
          zip \
          apt-transport-https \
      && mkdir ~/.ssh \
      && mkdir /var/run/sshd \
      && groupadd jenkins && useradd -d /home/jenkins -g jenkins -m jenkins \
      && mkdir /home/jenkins/.ssh \
      && rm -rf /var/lib/apt/lists/*

    # 22ポートを公開     
    EXPOSE 22
     
    # jenkinsからgitbucketへ接続するための認証情報を設定
    # http接続ができないのでsshで接続する必要があります
    COPY files/config ~/.ssh/config
    COPY files/config /home/jenkins/.ssh/config
    COPY files/id_jenkins_rsa.pub /home/jenkins/.ssh/authorized_keys
    COPY files/id_gitbucket_rsa /home/jenkins/.ssh/id_rsa

    RUN set -x \
      && chown -R jenkins:jenkins /home/jenkins/.ssh \
      && chmod 700 /home/jenkins/.ssh \
      && chmod 600 /home/jenkins/.ssh/*

     ```
2. インストールします

    ```bash
    $ cd ubuntu-base
    $ find .
    .
    ./files
    ./files/id_gitbucket_rsa
    ./files/id_jenkins_rsa
    ./files/config
    ./files/id_jenkins_rsa.pub
    ./files/id_gitbucket_rsa.pub
    ./Dockerfile
    ./README.md
    $ docker build -t ubuntu:ubuntu-base .
    ```
4. Electronのビルド環境構築のためのDockerImageを作ります

     ```Dockerfile
    FROM ubuntu:16.10

    #---------------------------
    WORKDIR /usr/local
     
    ENV NDENV_ROOT /usr/local/ndenv
    ENV PATH "$NDENV_ROOT/bin:$PATH"
     
    # ビルド用にndenvとyarnをインストールする
    RUN set -x \
      && git clone https://github.com/riywo/ndenv.git /usr/local/ndenv \
      && mkdir /usr/local/ndenv/plugins \
      && git clone https://github.com/riywo/node-build.git /usr/local/ndenv/plugins/node-build \
      && (\
          echo 'export NDENV_ROOT=/usr/local/ndenv'; \
          echo 'export PATH="$NDENV_ROOT/bin:$PATH"'; \
          echo 'eval "$(ndenv init -)"' \
        ) >> /etc/profile.d/ndenv.sh \
      && ndenv install v6.1.0 \
      && ndenv global v6.1.0 \
      && chmod +777 -R /usr/local/ndenv \
      && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - \
      && echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list \
      && sudo apt-get update \
      && sudo apt-get install yarn -y
    ```

2. インストールします

    ```bash
    $ cd ubuntu-electron
    $ find .
    .
    ./files
    ./files/id_gitbucket_rsa
    ./files/id_jenkins_rsa
    ./files/config
    ./files/id_jenkins_rsa.pub
    ./files/id_gitbucket_rsa.pub
    ./Dockerfile
    ./README.md
    $ docker build -t ubuntu:ubuntu-electron .
    ```

以上でDockerサーバーの設定は完了です。

## Jenkinsサーバー

### インストールから起動
1. [JenkinsのLTS Release](https://jenkins.io/)をダウンロードします。
2. お好きなアプリケーションサーバー(私はJetty)に配備します。ただしまだ起動はしません。
3. Jenkinsはデフォルトでは`$HOME/.jenkins`以下に必要なデータを構築しますので、事前に`$HOME/.jenkins`ディレクトリを作成して、`$HOME/.jenkins/init.groovy`という名前で以下のファイルを配置します。
    (参考:[Jenkinsの公式Dockerイメージ使ってみた](http://knjname.hateblo.jp/entry/2015/02/10/040833))

    ```init.groovy
    import hudson.model.*;
    import hudson.security.*
    import jenkins.model.*;

    def instance = Jenkins.instance

    //  ログインユーザーを自動生成
    // https://groups.google.com/d/msg/jenkinsci-users/Pb4QZVc2-f0/5hzCC-syNgAJ
    def hudsonRealm = new HudsonPrivateSecurityRealm(false)
    hudsonRealm.createAccount('admin','admin')
    instance.setSecurityRealm(hudsonRealm)

    def strategy = new FullControlOnceLoggedInAuthorizationStrategy()
    instance.setAuthorizationStrategy(strategy)
    instance.save()
    
    def uCenter = instance.updateCenter
    uCenter.updateAllSites()
    
    def checkIfPluginInstalled = { pluginid ->
      for(p in instance.pluginManager.plugins) {
        if(p.shortName == pluginid){
          return true
        }
      }
      return false
    }
    
    // Installs the following plugins if not installed.
    [
      "cloudbees-folder",
      "antisamy-markup-formatter",
      "build-timeout",
      "credentials-binding",
      "timestamper",
      "ws-cleanup",
      "gradle",
      "workflow-aggregator",
      "github-organization-folder",
      "pipeline-stage-view",
      "git",
      "ssh-slaves",
      "matrix-auth",
      "pam-auth",
      "ldap",
      "email-ext",
      "mailer",
      "docker-plugin",
      "ghprb"
    ].findAll({! checkIfPluginInstalled(it)}).each {
      uCenter.getPlugin(it).deploy()
    }
    
    // Set the number of executors (in master-node) to 0.
    instance.setNumExecutors(0)
    
    instance.reload()
    ```
4. Jenkinsを起動すると、init.groovyが読み込まれてadminユーザーと必要なプラグインがインストールされます。

### プラグインの設定
必要なプラグインの設定を行っていきます。

#### Docker Plugin
1. `http://JENKINS_SERVER:8080/configure`へ(Jenkinsの管理 => システム設定)。
1. `クラウド` => `追加` => `Docker`を選択
    * 各種設定を行います  
        <img width="400" alt="577c18fd-3e7f-4803-9d34-c2f4b1bafc9a.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/96ea8fa9-f643-f801-da06-2bb352e7f5b2.png">
        * Nameは任意
        * Docker URLは`tcp`あるいは`unix`のみが指定可能です。httpは不可
        * Docker API VersionはCoreOS上で`docker version`と打つことで確認できます。
    * `Images` => `Add Docker Template`で利用するDocker名を紐つけます。  
        <img width="689" alt="aab5a067-4347-4e2c-af50-c7499739e89b.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/2c00a95b-49a5-15bd-4359-4e0c15e25532.png">
        * `Docker Image` => Dockerの設定で作成したImage名を指定します。
        * `Remote Filing System Root` => Dockerfileで作成したjenkinsユーザーのhomeディレクトリを指定します
        * `Label` => Jenkinsfileのnodeの指定となるslave名です。
        * `用途` => `このマシーンを特定ジョブ専用にする`を選択します。
        * `Lanunch method` => Docker SSH computer launcherを選択します。認証情報は、Dockerfile内で定義した秘密鍵を使います。
    * Imagesの`Configuration setting...` => `Volumes`でキャッシュしたいディレクトリを永続化しておきます。
      今回はyarnのキャッシュフォルダとelectronのバイナリをホスト側にマウントするために以下の設定を追加しています。
      
      ```
      /home/core/cache/yarn:/home/jenkins/.cache/yarn/
      /home/core/cache/electron:/home/jenkins/.electron/
      ```

#### GitHub Organization Folder
1. `http://JENKINS_SERVER:8080/configure`へ(Jenkinsの管理 => システム設定)。
1. `GitHub Enterprise Servers`を設定します。  
    <img width="400" alt="60617ccd-9aed-4ba3-866e-0fdbac6c31c0.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/802ca48b-8156-2b72-5a40-b80ad0150b72.png">
    * `API endpoint` => `http://GITBUCKET_SERVER/api/v3/`
    * `Name` => 適当に(AWS-GITとつけました)

### JOB作成
1. `新規ジョブ`でGitHub Organization Folderを作成します。
    任意の名前をつけることが可能ですが、設定した名前がリポジトリのowner(ユーザーorグループ)になります。対象のリポジトリグループは後で変更可能です。<img width="400" alt="6a794f28-f792-4a63-b17c-11f82ba8fa5d.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/705d02d3-23e1-9302-dd71-eae0d6b0f1e7.png">
1. 設定画面が開くので、`Project Sources`から`Repository Sources` の設定を行います。<img width="400" alt="4901e517-357d-437e-84b5-88724943aee3.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/b30ed7fb-a41e-efdf-afbd-cf3bfe220faa.png">
    * Owner --- 指定したユーザーorグループのリポジトリ以下を探索します
    * API endpoint --- 接続先のサーバーをシステム設定で設定したGitBucketの名前を設定します。
2. うまくいけば指定したグループ配下がスキャンされ`Jenkinsfile`が存在するリポジトリが一覧に表示されます。
    * スキャン完了後<br /><img width="400" alt="9d32f5cb-06f4-4abd-90b4-84ce68bf2010.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/aaca994d-0058-0909-3241-056233131515.png">
    * ブランチ一覧<br /><img width="400" alt="e8576f72-36b5-471e-aa7a-b657b743bd2c.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/3c88965d-0298-8550-a0ca-7b2ec53f381e.png">
    * ビルド結果<br /><img width="400" alt="acffee48-dc5b-4dd9-9210-fe52531680fe.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/0c8a20f4-055d-234b-ffea-9479ae1b34df.png">

# ゴールを確認
やりたい事が出来ているか確認します。

1. GitBucketのリポジトリ上に簡単なElectronアプリをPUSHします。Jenkinsファイルには成果物を保存するところまで記載しておきます( TODO サンプルソース)。
2. Re-Scanして認識することを確認します。
3. GitBucket上のリポジトリ上にブランチを切り、PullRequestを出します。
4. PullRequestが出たタイミングでJenkinsビルドが走ることを確認します。
   <img width="400" alt="c8484347-6344-4fbb-9899-c66c8850894b.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/eac739e1-06a6-07ad-bd0d-64aa6f6d11f5.png">
   <img width="400" alt="842972e0-adb4-4e16-8fc4-7e003a6eaae8.png" src="https://qiita-image-store.s3.amazonaws.com/0/11214/8daadf43-f1db-3557-7fb0-69b2fae6305e.png">
5. Jenkinsビルドが完了後に成果物がダウンロードできることを確認します。

# ハマリポイント(まとめ)

* `GitHub Organization folder`でスキャンしてもGitBucketに繋がらない
    * 上記手順にしたがってWEBサーバーでルーティングをいじる必要があります。
* `DockerPlugin`から[Protect the Docker daemon socket](https://docs.docker.com/engine/security/https/)できない。
    * 現在のバージョン(DockerPlugin 0.16.2)だとhttpsは設定できません。
* Slaveとして起動しているDockerのインスタンスにJenkinsから接続出来ない。
    * DockerPluginの接続を確認しましょう。必要な秘密鍵はDockerに接続する認証情報ではなくて、Docker上に存在するインスタンスに接続するための秘密鍵です。
* Docker上からGitBucketのソースをclone出来ない
    * `https`プロトコルや`git`プロトコルでは接続出来ません。`ssh`で接続しましょう。

# オチ
GitHubは便利です。でも会社ではGitBucket使い続けていくつもりです。