JenkinsをKubernetes上で動かして、パイプライン内でSkaffoldを使う

これはなに
----------
JenkinsをKubernetes上で動かして、CDパイプライン内でSkaffoldを使う手順を記します。

パイプラインの一連の処理は、Kubernetes上にコンテナとして展開されるJenkinsエージェントで実行します。Skaffoldはこのエージェントから、パイプラインの1ステップとして実行されます。
また、Skaffoldにより、コンテナのビルド、プッシュ、デプロイが行われますが、この時のデプロイ先として、Jenkins自身が動いているものと同じKubernetsクラスターを使います。

（Skaffoldって何？という方は[こちら](https://speakerdeck.com/hhiroshell/skaffoldwoshi-tutemita)を参照ください）


動作確認済みの環境
==================

- Ubuntu 16.04 LTS
- Kuberentes クラスター
    * minikube v0.25.2
    * Kubernetes v1.9.4
- kubectl v1.10
- helm v2.8.2
- JenkinsのHelm Chart
    * [このコミット](https://github.com/kubernetes/charts/commit/78ee4ba1546c0428756af2684d3c0eac82a3c04d)時点のチャートを利用（Helm Chartはタグやリリースで管理されていないため）


手順
====
それではやって見ます。全体の流れは、以下のような感じです。

1. JenkinsをKubernetesにデプロイする
2. ビルドパイプラインを作成する


1. JenkinsをKubernetes上にデプロイする
--------------------------------------
まずはKubernetes上でJenkinsを動かします。ラクをしたいので、公式のHelm Chartを利用します。

### 1.1 Helm Chartのパラメータの修正
基本はHelmを使ってJenkinsをデプロイしますが、あらかじめいくつか設定を変更しておきます。

まず、values.yaml（Helm Chartに与える設定情報を記述したファイル）をダウンロードします。

    wget https://raw.githubusercontent.com/kubernetes/charts/78ee4ba1546c0428756af2684d3c0eac82a3c04d/stable/jenkins/values.yaml

以降、values.yaml内のパラメータを変更していきます。

#### クラスター外からのアクセス方法の変更
ローカルのクラスターを使うので、クラスター外からのアクセスの方法としてServiceのNodePortタイプを使うように変更します。

```diff:values.yaml
   ServicePort: 8080
   # For minikube, set this to NodePort, elsewhere use LoadBalancer
   # Use ClusterIP if your setup includes ingress controller
-  ServiceType: LoadBalancer
+  ServiceType: NodePort
   # Master Service annotations
   ServiceAnnotations: {}
   #   service.beta.kubernetes.io/aws-load-balancer-backend-protocol: https
```

#### Jenkinsにインストールしておくプラグインの変更
既にいくつかのプラグインが書かれていますが、バージョンを更新したり、追加のプラグインを加える必要があります。

```diff:values.yaml
   # JMXPort: 4000
   # List of plugins to be install during Jenkins master start
   InstallPlugins:
-    - kubernetes:1.1
+    - kubernetes:1.5.2
     - workflow-aggregator:2.5
-    - workflow-job:2.15
-    - credentials-binding:1.13
-    - git:3.6.4
+    - workflow-job:2.21
+    - credentials-binding:1.16
+    - git:3.8.0
+    - ghprb:1.40.0
+    - blueocean:1.4.2
   # Used to approve a list of groovy functions in pipelines used the script-security plugin. Can be viewed under /scriptApproval
   # ScriptApproval:
   #   - "method groovy.json.JsonSlurperClassic parseText java.lang.String"
```

#### 永続化オプションの変更
ローカルのクラスターで簡単に動かすために、VolumeとしてEmplyDirを使うようにします。データ永続化は保証されない、本番では絶対利用できない方法なので、ご注意ください。

```diff:values.yaml
   # jenkins-agent: v1

 Persistence:
-  Enabled: true
+  Enabled: false
   ## A manually managed Persistent Volume and Claim
   ## Requires Persistence.Enabled: true
   ## If defined, PVC must be created manually before volume will be bound
```

### 1.2 HelmでJenkinsをデプロイする
対象のクラスターでHelmを使ったことがなければ、``helm init``しておきます。

    helm init

以下のコマンドで、修正済みのvalues.yamlを適用してJenkinsをデプロイします。併せて、jenkinsというNamespaceを作って、そこにデプロイしています。

    helm install -f values.yaml --name jenkins --namespace=jenkins stable/jenkins

kubectlのデフォルトのNamespaceをjenkinsに変更しておきます。

    kubectl config set-context $(kubectl config current-context) --namespace=jenkins

Jenkinsが起動するまで少し時間が必要です。数分待ってから、Deployment、Service、Podの状態を確認してください。

    kubectl get deployment,pod,service

Jenkinsの起動処理まで進めば、以下のような結果が返ってきます。

```
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
jenkins   1         1         1            1           7h

NAME                       READY     STATUS    RESTARTS   AGE
jenkins-5c89487645-s6mxv   1/1       Running   1          7h

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
jenkins         NodePort    10.96.215.81     <none>        8080:31590/TCP   7h
jenkins-agent   ClusterIP   10.110.217.211   <none>        50000/TCP        7h
```

Jenkinsのサーバーが立ち上がるまでには更に時間が必要です。上の状態になってからも、数分程度時間を置いてください。

### 1.3 JenkinsのGUIにアクセスする
管理者ユーザーのパスワードは、デプロイ時に自動生成された値がKubernetesのSecretオブジェクトに格納されています。以下のコマンドでそれを確認することができます。

    printf $(kubectl get secret --namespace jenkins jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

JenkinsサーバーのIPアドレス、ポート番号は以下のコマンドで確認することができます。

    printf $(kubectl get pod -o 'jsonpath={.items[0].status.hostIP}'):$(kubectl get service jenkins -o 'jsonpath={.spec.ports[0].nodePort}');echo

Webブラウザで、__http://[上で確認したIP]:[上で確認したポート番号]/__にアクセスすると、Jnekinsのログイン画面が表示されます。ユーザー名、パスワードには、以下の値を入力します。

- ユーザー名: admin
- パスワード: （上記コマンドで確認した文字列）

![](https://qiita-image-store.s3.amazonaws.com/0/108645/e99d5f2e-214f-b534-1e54-05a778642f97.png)

ログインに成功するとJenkinsのトップ画面が表示されます。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/fef4297e-e155-2d19-2da8-b5c573a8f37e.png)

以上で、JenkinsをKubernetes上にデプロイする作業は完了です。


2. ビルドパイプラインを作成する
-------------------------------
ここからは、あらかじめ作成しておいたサンプルアプリケーションとJenkinsfile（パイプラインの定義ファイル）を利用して、ビルドパイプラインを作成していきます。ここで利用するJenkinsfileでは、パイプライン中の1ステップの中でSkaffoldを実行します。Skaffoldによって、コンテナのビルド、プッシュ、デプロイを自動実行します。

### 2.1 サンプルアプリケーションの準備
まずは、Github上にサンプルアプリケーションのコードとJenkinsfileを準備していきます。

#### サンプルアプリケーションをForkする
サンプルアプリケーションのコード（Jenkinsfileを含む）は、[GitHubリポジトリ](https://github.com/hhiroshell/cowweb)に置いてあります。これをご自身のアカウントにForkしておいてください。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/6eddaaf7-9ae3-9d28-90c6-fa305a99e009.png)

このアプリケーションは、[cowsay](https://ja.wikipedia.org/wiki/Cowsay)というジョークアプリをWeb API化したものです。このアプリを起動して所定のURLにアクセスすると、cowsayと同様のアスキーアートが返ってきます。

#### パイプライン内で``git clone``するリポジトリを修正する
Forkしてできたリポジトリに含まれるJenkinsfileは、Fork元のリポジトリを``git clone``するように記述されています。これを、Fork後のリポジトリをcloneするように修正します。

Fork後のリポジトリをブラウザで表示し、Jenkinsfileの該当箇所（画像参照）を修正してください。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/2023f509-1afb-05f5-c402-3d3cb0186e46.png)

#### 接続先のコンテナレジストリを変更する
Skaffoldが呼び出されると、その内部処理で``docker build``や``docker push``が実行され、コンテナのビルド、プッシュを行います。このときのコンテナイメージのプッシュ先を、ご自身がプッシュ可能なコンテナレジストリに修正しておきます。

Fork後のリポジトリをブラウザで表示し、skaffold.yamlの該当箇所（画像参照）を修正してください。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/36641484-79b2-22f4-ed06-549779a7c354.png)

### 2.2. ビルドパイプラインを作成する
次に、JenkinsのUIからビルドパイプラインを作成します。

### コンテナレジストリの認証情報をJenkinsに登録する
Skaffoldの内部処理で実行される``docker push``において、コンテナレジストリへのアクセスのための認証情報が必要となります。ここで、あらかじめJnekinsに認証情報を設定しておきます。

Jenkinsのトップ画面から、__認証情報__ > __(global)__ > __認証情報を追加__ の順にクリックします。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/aea8b041-ab8b-9a58-0320-7c2f5dfcb904.png)

![](https://qiita-image-store.s3.amazonaws.com/0/108645/3cfdbbf6-9b58-34e3-f56e-c3e308ebcb24.png)

![](https://qiita-image-store.s3.amazonaws.com/0/108645/664efc42-4df9-4ef3-7cfb-a3be75dd5b31.png)

以下のように、イメージのプッシュ先のコンテナレジストリの認証情報を入力します。認証情報のID（Jenkins上で管理するための識別名となる値）は、docker\_idとしてください。

- 種類: ユーザー名とパスワード
- ユーザー名:（コンテナレジストリのアカウント名）
- パスワード:（コンテナレジストリのパスワード）
- ID: docker\_id

![](https://qiita-image-store.s3.amazonaws.com/0/108645/b922b9f8-9608-d8e6-4a73-f0e734624c82.png)

### 2.2 パイプラインを作る
ビルドパイプラインはJenkinsのBlue Cceanプラグインで作成します。Jenkinsのトップ画面から__Open Blue Ocan__をクリックします。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/d7fff0f7-49bc-3dd7-776c-51b2816f782c.png)

Blue Oceanの画面で、__Create a new Pipeline__をクリックします。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/171c96c1-8c1c-eee4-423d-4b2aa66dc6ac.png)

Pipelineの作成画面に遷移します。以降この画面の流れに従ってPipelineを作成していきます。

まずは、ソースコードをホストしているサービスを選択します。この手順ではGitHubを利用しているので__GitHub__をクリックします。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/1eb4cf75-3032-568d-02cf-e6a9e8835000.png)

次に、JenkinsからGitHubのAPIを呼び出すためのアクセストークンを設定します。__Create an access key here.__をクリックすると、GitHubの画面が新たなタブで開きます。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/bd20d892-96bf-3419-c039-602493e883c5.png)

GitHubのパスワード入力が求められたら、自身のアカウントのパスワードを入力します。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/0a64e5ff-bd84-c866-41d8-872c54e3f4f7.png)

新たなアクセストークンを発行する画面に遷移したら、__Token description__にトークンの用途の説明を入力します。ここではjenkinsと入力しておきます。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/6900f132-9ff0-8dc0-2375-e4485f43e10a.png)

他の設定はデフォルトのままにして、画面下の__Generate token__をクリックします。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/bbe6db0a-6988-18b4-28a0-80f392220b16.png)

作成されたアクセストークンが表示されたら、トークンの文字列の右横にあるアイコンをクリックして、クリップボードにトークンをコピーします。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/d0a78f6b-82d0-cd15-2b2e-c9c4bbbb8a4a.png)

JenkinsのPipeline作成画面に戻って、コピーしたトークンを入力欄にペーストし、__Connect__をクリックします。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/02c5b746-6fe3-7720-e839-16ca452ee9d9.png)

GitHubで複数のorganizationに対するアクセス権を持っている場合、Fork後のリポジトリがあるorganizationをここで指定する必要があります。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/50e34f51-ede6-3d34-9c81-05c23c7b2df6.png)

最後に、Choose a repositoryでサンプルアプリケーションをForkしたリポジトリを選択します（デフォルトのままであれば、リポジトリの名称はcowwebとなっています）。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/4a76366b-3b30-eb7e-eb65-dec3803eeffc.png)

ここまでの手順を完了すると、Jenkinsfileが自動的に読み込まれ、それを基にJenkinsにパイプラインが作成されます。さらに自動的にパイプラインが起動され、以下のような画面となります。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/767d1dbe-ae81-5d8c-732e-6edefd304993.png)

上の画像でハイライトした部分をクリックすると、パイプラインの実行状況の詳細を表示する画面に遷移します。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/6a73ee8a-3a6a-429f-7337-b0789fe1996d.png)

画面下方にあるテーブルの行をクリックすると、パイプラインの実行中のコンソール出力が表示されます。__Task skaffold__の__Shell Script__をクリックして展開すると、``skaffold run``コマンドが実行され、その後続の処理でコンテナのビルド、プッシュ等がおこなわれていることが確認できます。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/af9aaff5-96f3-d31e-7944-b322e521011c.png)

パイプラインが無事に終了すると、以下のような画面となります。

![](https://qiita-image-store.s3.amazonaws.com/0/108645/ea4e60b4-24a8-ae52-642d-9f63b3c65dba.png)

### 2.3 デプロイされたアプリケーションの動作確認をおこなう
すでにSkaffoldによって、ビルドされたアプリケーションがKubernetesにデプロイされています。以下のコマンドでPod、Serviceが作成されていることを確認してください。

    kubectl get -n cowweb pod,service

以下のようにアプリケーション本体のPodと、NodePortタイプのServiceが作成されているはずです。

    NAME      READY     STATUS    RESTARTS   AGE
    cowweb    1/1       Running   0          1m

    NAME      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
    cowweb    NodePort   10.100.135.47   <none>        8080:32370/TCP   1m

アプリケーションにアクセスするには、以下のコマンドを実行してアプリケーションにアクセスするためのIPアドレス、ポート番号を取得します。

    export COWWEB_HOST=$(kubectl get pod -n cowweb -o 'jsonpath={.items[0].status.hostIP}'):$(kubectl get service cowweb -n cowweb -o 'jsonpath={.spec.ports[0].nodePort}')

続いて、アプリケーションにリクエストを投げてみます。

    curl "http://$COWWEB_HOST/cowsay/say/?message=Hello_Jenkins"

正しく動作していれば、以下のようなcowsayアプリケーションのアスキーアートが返却されます（呼び出しの度にランダムに異なるアスキーアートが表示されます）。

     _______________
    < Hello_Jenkins >
     ---------------
            \   ^__^
             \  (oo)\_______
                (__)\       )\/\
                    ||----w |
                    ||     ||

以上で、JenkinsのパイプラインからSkaffoldを実行し、コンテナのビルド、プッシュ、デプロイをすることができました。またデプロイ先として、Jenkins自身が動いているものと同じKubernetsクラスターを使い、実際にアプリケーションの動作を確認しました。


3. 補足: Jenkinsfileの内容の解説
--------------------------------
ここでは、このエントリで利用したサンプルアプリケーションのJenkinsfile（パイプラインの定義ファイル）の内容について、簡単に解説します。

Jenkinsfileの全体は以下のようになっています。

```Groovy:Jenkinsfile
podTemplate(
  label: 'skaffold',
  containers: [
    containerTemplate(name: 'skaffold-insider', image: 'hhayakaw/skaffold-insider:v1.0.0', ttyEnabled: true, command: 'cat')
  ],
  volumes: [
    hostPathVolume(hostPath: '/var/run/docker.sock', mountPath: '/var/run/docker.sock')
  ]
) {
  node('skaffold') {
    withCredentials([
      usernamePassword(credentialsId: 'docker_id', usernameVariable: 'DOCKER_ID_USR', passwordVariable: 'DOCKER_ID_PSW')
    ]) {
      stage('Info') {
        container('skaffold-insider') {
          sh """
            uname -a
            whoami
            pwd
            ls -al
          """
        }
      }
      stage('Test skaffold') {
        git 'https://github.com/hhiroshell/cowweb.git'
        container('skaffold-insider') {
          sh """
            docker login --username=$DOCKER_ID_USR --password=$DOCKER_ID_PSW
            skaffold run -p release
          """
        }
      }
    }
  }
}
```

冒頭のpodTemplate配下には、パイプラインの処理を実行する際に利用する、Podの定義を記述します。

```Groovy:Jenkinsfile(L1-L9)
podTemplate(
  label: 'skaffold',
  containers: [
    containerTemplate(name: 'skaffold-insider', image: 'hhayakaw/skaffold-insider:v1.0.0', ttyEnabled: true, command: 'cat')
  ],
  volumes: [
    hostPathVolume(hostPath: '/var/run/docker.sock', mountPath: '/var/run/docker.sock')
  ]
) {
```

containersには、Podに含めるコンテナを記述します。このパイプラインでは、hhayakaw/skaffold-insider:v1.0.0という、Skaffoldがインストールされたコンテナを利用しています。このコンテナは、あらかじめ作成してDocker Hubにプッシュしてあります。

volumesには、コンテナにマウントするボリュームを記述します。Skaffoldは、内部でDockerコマンドを利用するため、Dockerコンテナ内でDockerコマンドを実行する、Docker in Docker(DinD)構成となります。このため、/var/run/docker.sockをマウントしています（DinDの詳細についてはWeb上に色々な記事がありますので、それらを参照下さい。このエントリの最後にリンクをまとめて記します）。

node配下には、パイプラインのステップの処理内容を定義します。

```Groovy:Jenkinsfile(L10-13)
  node('skaffold') {
    withCredentials([
      usernamePassword(credentialsId: 'docker_id', usernameVariable: 'DOCKER_ID_USR', passwordVariable: 'DOCKER_ID_PSW')
    ]) {
```

node配下のwithCredentialsには、ステップ内で利用するクレデンシャル情報を記述します。上記の記述により、JenkinsのGUIで設定した、docker\_idという名前の認証情報が利用可能になります。また、後続のステップの定義おいて、環境変数DOCKER\_ID\_USRによってユーザー名が、DOCKER\_ID\_PSWによってパスワードが参照できます。

さらに後続のstageの記述により、パイプラインのステップで実行される実際の処理を定義します。

```Groovy:Jenkinsfile(L14-L32)
      stage('Info') {
        container('skaffold-insider') {
          sh """
            uname -a
            whoami
            pwd
            ls -al
          """
        }
      }
      stage('Test skaffold') {
        git 'https://github.com/hhiroshell/cowweb.git'
        container('skaffold-insider') {
          sh """
            docker login --username=$DOCKER_ID_USR --password=$DOCKER_ID_PSW
            skaffold run -p release
          """
        }
      }
```

この記述では、Info、Test skaffoldという名前の2つのstage（ステップ）を定義しています。また、どちらもskaffold-insiderコンテナを使って処理を実行しています。

ステップInfoでは、``uname``等のコマンドを実行して、コンテナの基本的な情報をコンソールに出力しています（具体的なビルドの処理は行っていません）。

ステップTest skaffoldでは、アプリケーションのコードのチェックアウト、プッシュ先のDockerレジストリへのログインの後、Skaffoldを実行しています。``-p``オプションでは、Skaffoldのプロファイル名を指定しています。プロファイルは、ビルド、プッシュ、デプロイの設定のセットで、今回はJenkins内で実行するための設定情報を、releaseという名前で記述してあります。


4. 参考リンク一覧
-----------------

- Skaffold関連
    * [SkaffoldのGitHubリポジトリ](https://github.com/GoogleContainerTools/skaffold)
    * [Skaffoldを使ってみた](https://speakerdeck.com/hhiroshell/skaffoldwoshi-tutemita)
- Docker in Docker関連
    * [Docker in Dockerのメモ書き](https://amaya382.hatenablog.jp/entry/2016/10/05/030254)
    * [Docker in Docker のベタープラクティス](https://qiita.com/sugiyasu-qr/items/85a1bedb6458d4573407)
    * [Using Docker-in-Docker for your CI or testing environment? Think twice.](http://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/)
- Jenkins on Kubernetesでパイプラインを使う他の例
    * [JENKINS PIPELINES WITH KUBERNETES](https://radu-matei.com/blog/kubernetes-jenkins-azure/)
    * [Set Up a Jenkins CI/CD Pipeline with Kubernetes](https://akomljen.com/set-up-jenkins-ci-cd-pipeline-with-kubernetes/)