コンテナリポジトリHarbor in Kubernetesの検証

#はじめに
CNCFの[Incubating Projects](https://landscape.cncf.io/project=incubating)であるコンテナリポジトリHarborを検証します。Harborは、Cloud Native Registryということで、Kubernetes上で運用することができるコンテナリポジトリです。プライベートクラウドなどで独自にコンテナリポジトリが欲しい方の選択肢の一つになるかと思います。機能としては、Multi-tenant、Image replication, コンテナイメージの脆弱性チェックなどの特徴を持っています。

# 検証
 今回の検証では、HarborをKubernetes上に構築、コンテナイメージをHarborへ格納、Kubernetesで格納したコンテナイメージをデプロイを行います。

##環境
検証に使う環境として以下の環境を使います。

- Kubernetes v1.13.2 (kubeadmで構築, Master x 1, Worker x 2)
- Helm v2.9.1


##事前準備
　Harborをセットアップする事前準備として、

- Ingress Controllerのセットアップ
- HarborのNamespace
- Harborのデータを格納するPersistentVolume(PV)

を準備します。

###Ingress Controllerのセットアップ
　本検証では、Harborへ外部からIngress経由でアクセスするようにします。
Ingress Controllerが設定出来ていない場合は、Ingress Controllerをデプロイします。ここでは、NGINX Ingress Controllerをデプロイします。
NGINX Ingress Controllerの詳細は[こちら](https://kubernetes.github.io/ingress-nginx/deploy/#prerequisite-generic-deployment-command)をご参照。

```
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml
```

次に、KubernetesのNodeのIP(`192.168.0.23`)をExternal IPとして利用します。ServiceのManifest(`service-nodeport.yaml`)を以下に示します。

```service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  externalIPs:
  - 192.168.0.23
  type: NodePort
  ports:
    - name: http
      port: 80
      targetPort: 80
      protocol: TCP
    - name: https
      port: 443
      targetPort: 443
      protocol: TCP
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
```

作成したManifestをデプロイします。

```
$ kubectl apply -f service-nodeport.yaml
```

デプロイされたIngress Controllerを確認します。

```
$ kubectl get pod -n ingress-nginx
NAME                                        READY   STATUS    RESTARTS   AGE
nginx-ingress-controller-565dfd6dff-9bfcf   1/1     Running   0          4h49m

$ kubectl get svc -n ingress-nginx
NAME            TYPE       CLUSTER-IP     EXTERNAL-IP    PORT(S)                      AGE
ingress-nginx   NodePort   10.99.41.249   192.168.0.23   80:30358/TCP,443:31371/TCP   4h47m
```

本検証では、NGINX Ingress Controllerを使っていますが、弊社のメンバが開発している[nghttpx Ingress Controller](https://github.com/zlabjp/nghttpx-ingress-lb)を利用することも可能です。

###デプロイ先のNamespaceの作成
　次に、HarborをデプロイするNamespaceを作成します。

```
$ kubectl create ns harbor

$ kubens harbor
```

以降、作成したNamespace(`harbor`)へPodをデプロイしていきます。

###Harborのデータを格納するPersistentVolume(PV)のセットアップ
　次に、Harborのデータの保存先として、本検証ではLocal Volumeをセットアップします。HarborのデータはDBMSのPostgreSQLなどに格納されるため、一般的にDBMSで推奨されているデータ保存先であるブロックストレージを利用する方がお勧めです。ここでは、[Kubernetes: Local Volumeの検証
](https://qiita.com/ysakashita/items/67a452e76260b1211920)を参考にLocal Volumeをセットアップします。
　まず、KubernetesのNodeにログインしLocal Volumeの準備をします。
以下の実行例では`k8s-node1`というNodeにLocal Volumeのマウント先であるディレクトリ(`/mnt/disks/vol1 ... vol5`)を作成します。

```
$ ssh k8s-node1
$ sudo bash
# for i in 1 2 3 4 5
do
  mkdir -p /mnt/disks/vol${i}
  chmod 777 /mnt/disks/vol${i}
done

# ls /mnt/disks/
vol1  vol2  vol3  vol4  vol5
# exit

$ exit
```

次に、Harborで必要となるchartmuseum, database, job service, redistribution, registry用のPV、PVCを作成します。
以下にそれぞれのManifestを示します。

```chartmuseum.pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: chartmuseum-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-node1
```

```chartmuseum.pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: chartmuseum-claim
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  resources:
    requests:
      storage: 5Gi
```

```database.pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: database-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol2
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-node1
```

```database.pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: database-claim
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  resources:
    requests:
      storage: 1Gi
```

```jobservice.pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jobservice-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol3
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-node1
```

```jobservice.pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: jobservice-claim
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  resources:
    requests:
      storage: 1Gi
```

```redis.pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: redis-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol4
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-node1
```

```redis.pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: redis-claim
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  resources:
    requests:
      storage: 1Gi
```

```registry.pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: registry-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol5
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-node1
```

```registry.pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: registry-claim
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-storage
  resources:
    requests:
      storage: 5Gi
```

上記のManifestをデプロイします。

```
$ kubectl create -f chartmuseum.pv.yaml 
$ kubectl create -f chartmuseum.pvc.yaml 

$ kubectl create -f database.pv.yaml 
$ kubectl create -f database.pvc.yaml 

$ kubectl create -f jobservice.pv.yaml 
$ kubectl create -f jobservice.pvc.yaml 

$ kubectl create -f redis.pv.yaml 
$ kubectl create -f redis.pvc.yaml 

$ kubectl create -f registry.pv.yaml 
$ kubectl create -f registry.pvc.yaml 
```

作成したPV, PVCを確認します。

```
$ kubectl get pv
NAME             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                      STORAGECLASS    REASON   AGE
chartmuseum-pv   5Gi        RWO            Delete           Bound    harbor/chartmuseum-claim   local-storage            3m16s
database-pv      1Gi        RWO            Delete           Bound    harbor/database-claim      local-storage            2m59s
jobservice-pv    1Gi        RWO            Delete           Bound    harbor/jobservice-claim    local-storage            2m48s
redis-pv         1Gi        RWO            Delete           Bound    harbor/redis-claim         local-storage            2m28s
registry-pv      5Gi        RWO            Delete           Bound    harbor/registry-claim      local-storage            2m11s

$ kubectl get pvc
NAME                STATUS   VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS    AGE
chartmuseum-claim   Bound    chartmuseum-pv   5Gi        RWO            local-storage   4m17s
database-claim      Bound    database-pv      1Gi        RWO            local-storage   4m1s
jobservice-claim    Bound    jobservice-pv    1Gi        RWO            local-storage   3m50s
redis-claim         Bound    redis-pv         1Gi        RWO            local-storage   3m28s
registry-claim      Bound    registry-pv      5Gi        RWO            local-storage   3m9s
```

##Harborのデプロイ
いよいよ、Harborをデプロイします。
まず、始めにGitHubからharbor-helmをダウンロードします。

```
$ git clone https://github.com/goharbor/harbor-helm.git

$ cd harbor-helm
```

次に、values.yamlを編集します。変更箇所を以下に示します。
Ingressのhostのドメイン名には、ワイルドカードDNSの(`xip.io`)を使います。
本検証では、coreサービスのホスト名に`core.harbor.192.168.0.23.xip.io`を notaryサービスのホスト名に`notary.harbor.192.168.0.23.xip.io`をそれぞれ指定します。externalURLも同様です。
PVCには、先程作成したLocal VolumeのPVCをそれぞれ指定します。

```diff
diff --git a/values.yaml b/values.yaml
index 3d49de5..b0f6653 100644
--- a/values.yaml
+++ b/values.yaml
@@ -24,8 +24,8 @@ expose:
     commonName: ""
   ingress:
     hosts:
-      core: core.harbor.domain
-      notary: notary.harbor.domain
+      core: core.harbor.192.168.0.23.xip.io
+      notary: notary.harbor.192.168.0.23.xip.io
     annotations:
       ingress.kubernetes.io/ssl-redirect: "true"
       nginx.ingress.kubernetes.io/ssl-redirect: "true"
@@ -76,7 +76,7 @@ expose:
 # the IP address of k8s node 
 # 
 # If Harbor is deployed behind the proxy, set it as the URL of proxy
-externalURL: https://core.harbor.domain
+externalURL: https://core.harbor.192.168.0.23.xip.io
 
 # The persistence is enabled by default and a default StorageClass
 # is needed in the k8s cluster to provision volumes dynamicly. 
@@ -93,7 +93,7 @@ persistence:
   persistentVolumeClaim:
     registry:
       # Use the existing PVC which must be created manually before bound
-      existingClaim: ""
+      existingClaim: registry-claim
       # Specify the "storageClass" used to provision the volume. Or the default
       # StorageClass will be used(the default).
       # Set it to "-" to disable dynamic provisioning
@@ -102,13 +102,13 @@ persistence:
       accessMode: ReadWriteOnce
       size: 5Gi
     chartmuseum:
-      existingClaim: ""
+      existingClaim: chartmuseum-claim
       storageClass: ""
       subPath: ""
       accessMode: ReadWriteOnce
       size: 5Gi
     jobservice:
-      existingClaim: ""
+      existingClaim: jobservice-claim
       storageClass: ""
       subPath: ""
       accessMode: ReadWriteOnce
@@ -116,7 +116,7 @@ persistence:
     # If external database is used, the following settings for database will 
     # be ignored
     database:
-      existingClaim: ""
+      existingClaim: database-claim
       storageClass: ""
       subPath: ""
       accessMode: ReadWriteOnce
@@ -124,7 +124,7 @@ persistence:
     # If external Redis is used, the following settings for Redis will 
     # be ignored
     redis:
-      existingClaim: ""
+      existingClaim: redis-claim
       storageClass: ""
       subPath: ""
       accessMode: ReadWriteOnce
```

設定したvalues.yamlを使いHarborをデプロイします。

```
$ helm install --name home-harbor .
...

==> v1beta1/Ingress
NAME                        HOSTS                                                              ADDRESS  PORTS  AGE
home-harbor-harbor-ingress  core.harbor.192.168.0.23.xip.io,notary.harbor.192.168.0.23.xip.io  80, 443  2s

NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://core.harbor.192.168.0.23.xip.io. 
For more details, please visit https://github.com/goharbor/harbor.
```

以上で、Harborのデプロイが完了です。

##KubernetesのコンテナリポジトリをHarborに変更
次に、デプロイしたHarborをKubernetesから利用できる設定します。

### Harbor WebUIへログイン
Webブラウザを開き、HarborのWebUIへログインします。
URLは先程externalURL `https://core.harbor.192.168.0.23.xip.io`です。
初期のアカウントは以下です。
- User Name: admin
- Password: Harbor12345

### ca.crtを取得
KubernetesのNodeへ設定する証明書(ca.crt)を取得します。
HarborのWebUIで、Configuration->System Settingsへ進み、Registry Root Certificate の`Download`をクリックし、ca.crtをダウンロードします。

![スクリーンショット 2019-01-14 20.40.09.png](https://qiita-image-store.s3.amazonaws.com/0/229880/7705a9d1-196f-ee64-6387-e9bfbabe0704.png)

### KubernetesのNodeへca.crtを配置
続いて、ダウンロードしたca.crtをKubernetesの全Nodeにコピーします。
以下は、k8s-masterのNodeでの配置例です。全てのNodeに同じようにca.crtを配置します。

```
$ scp ca.crt k8s-master:/tmp
```

Nodeにログインし、`/etc/docker/certs.d/[Harborのホスト名]`ディレクトリを作成し、ca.crtをコピーします。その後、dockerを再起動します。

```
$ ssh k8s-master
$ sudo mkdir -p /etc/docker/certs.d/core.harbor.192.168.0.23.xip.io
$ sudo mv /tmp/ca.crt /etc/docker/certs.d/core.harbor.192.168.0.23.xip.io
$ sudo systemctl restart docker
```

これで、ca.crtの配置は完了しました。

### Secretの作成
続いて、Harborにアクセスするユーザ情報をKubernetes上にデプロイします。
ここでは、検証のため初期ユーザ(`admin`)を設定していますが、環境にあわせて適宜変更してください。
`core.harbor.192.168.0.23.xip.io`のユーザ情報をSecretとして保存します。

```
$ kubectl create secret docker-registry harbor \
--docker-server=https://core.harbor.192.168.0.23.xip.io \
--docker-username=admin \
--docker-email="" \
--docker-password='Harbor12345'
```

以上で、Harborに格納されたコンテナイメージをKubernetesへデプロイできる環境が整いました。

## コンテナイメージの格納とKubernetesへのデプロイ

では、構築したHarborを使ってみたいと思います。

### Harborにプロジェクトを作成
まず、Harborへコンテナイメージを格納する前に、格納先のプロジェクトをHarborに作成します。
HarborのWebUIにログインし、`+NEW PROJECT` をクリックします。

![スクリーンショット 2019-01-14 22.00.06.png](https://qiita-image-store.s3.amazonaws.com/0/229880/54b772ce-6432-882f-d250-dcaa2026617e.png)

ダイアログに、プロジェクト名を入力します。
ここではプロジェクト名`sandbox`をPublicリポジトリとして作成します。

![スクリーンショット 2019-01-14 22.00.47.png](https://qiita-image-store.s3.amazonaws.com/0/229880/5e1278b4-a15f-9090-8976-ec71f82caaec.png)

プロジェクトが作成されるとProjectsに、作成した`sandbox`プロジェクトが表示されます。

![スクリーンショット 2019-01-14 22.01.01.png](https://qiita-image-store.s3.amazonaws.com/0/229880/b7796854-da99-f726-ba56-26a5e994febf.png)

###コンテナイメージをHarborへPush
次に、コンテナイメージをHarborへ格納(Push)していきます。
本検証では、適当なコンテナイメージをDocker HubからPullしてきます。

```
$ docker pull ubuntu:18.10
```

Pullしたコンテナイメージにtagをつけます。
このとき、`[HarborのCoreサービスのアドレス]/[プロジェクト名]/[イメージ名:タグ名]`としてください。

```
$ docker tag ubuntu:18.10 core.harbor.192.168.0.23.xip.io/sandbox/ubuntu:18.10
```

コンテナイメージをdockerコマンドにてHarborへログインし、Pushします。

```
$ docker login core.harbor.192.168.0.23.xip.io

$ docker push core.harbor.192.168.0.23.xip.io/sandbox/ubuntu:18.10
```

これで、コンテナイメージがHarborへPushされました。
HarborのWebUIでコンテナイメージ(`ubuntu:18.10`)がPushされていることを確認します。コンテナイメージが正しくHarborにPushされているかと思います。

![スクリーンショット 2019-01-14 22.16.41.png](https://qiita-image-store.s3.amazonaws.com/0/229880/5fc7184d-e254-c8fe-a1d3-c5dbed87624a.png)

###Harborからコンテナイメージをデプロイ
次に、KubernetesへHarborに格納したコンテナイメージをデプロイしてみます。
まず、先ほどHarborへPushしたコンテナイメージ(`ubuntu:18.10`)を使ったPodのMainfestを作成します。

```ubuntu-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
spec:
  containers:
  - name: ubuntu
    image: core.harbor.192.168.0.23.xip.io/sandbox/ubuntu:18.10
    command:
      - sleep
      - infinity
```

.spec.containers.imageに、先ほどPushしたHarborのコンテナイメージを指定します。作成したManifestをデプロイし確認します。


```
$ kubectl create -f ubuntu-pod.yaml 

$ kubectl get pod
NAME     READY   STATUS    RESTARTS   AGE
ubuntu   1/1     Running   0          58s

$ kubectl describe pod ubuntu
...
Events:
  Type    Reason     Age   From                Message
  ----    ------     ----  ----                -------
  Normal  Scheduled  85s   default-scheduler   Successfully assigned default/ubuntu to k8s-node2
  Normal  Pulling    84s   kubelet, k8s-node2  pulling image "core.harbor.192.168.0.23.xip.io/sandbox/ubuntu:18.10"
  Normal  Pulled     80s   kubelet, k8s-node2  Successfully pulled image "core.harbor.192.168.0.23.xip.io/sandbox/ubuntu:18.10"
  Normal  Created    80s   kubelet, k8s-node2  Created container
  Normal  Started    80s   kubelet, k8s-node2  Started container
```

kubectl describeコマンドで確認すると、`Events`にHarborからコンテナイメージがPullされたことが確認できます。

以上で、HarborへコンテナイメージをPushし、Kubernetesで利用することが確認できました。

## コンテナイメージの脆弱性チェック
おまけの検証として、Harborが備える機能のうち、コンテナイメージの脆弱性チェックを試してみます。
コンテナイメージは、常に注意しておかないとセキュリティパッチの当たっていないコンテナイメージを使ってしまいセキュリティホールになりかねません。そこで、コンテナイメージの脆弱性をチェックすることが重要です。Harborでは[Clair](https://github.com/coreos/clair)のデータベースを利用して、脆弱性チェックを行います。

HarborのWebUIにログインし、Projectsからコンテナイメージをクリックします。
すると、左上に、SCANのボタンがあるので、対象のコンテナイメージを選択し、SCANをクリックします。すると、脆弱性のチェックを行ってくれます。

<img width="1183" alt="スクリーンショット 2019-02-05 22.30.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/229880/eb7257b2-5aa0-54d0-97da-9217b0630b0f.png">

Vulnerabilityにフォーカスをあてると、詳細が確認できます。

<img width="1204" alt="スクリーンショット 2019-02-05 22.33.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/229880/4d84d26a-a6a0-2740-2f44-e5a48516ae8d.png">

また、この脆弱性チェックは、手動で実行するだけでなく決まった時刻に全イメージのチェックを行う設定もできます。

<img width="1150" alt="スクリーンショット 2019-02-05 22.29.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/229880/8a82ad3d-10b3-7cb6-d9fc-0410de50aeed.png">

さらには、各ProjectsのConfigurationにコンテナイメージをPushした際に、自動で脆弱性チェックを行う設定も可能です。

<img width="1181" alt="スクリーンショット 2019-02-05 22.37.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/229880/14dcf6d0-7081-2a91-408a-01277f858214.png">

#おわりに
　本検証では、Harborの動作を試してみました。パブリッククラウドで公開されているコンテナイメージを利用するのに抵抗がある組織も数多くあるかと思います。
Harborは、2018年にCNCFのIncubating Projectsに昇格するなどプロジェクトとして、勢いのあるコンテナリポジトリです。プライベートクラウドに専用のコンテナリポジトリが欲しい人には、一つの選択肢になるのではないでしょうか。
　また、コンテナイメージの脆弱性チェックなどセキュリティ面も考慮した機能もあります。プライベートクラウドでコンテナリポジトリを運用すると、ついついプライベートクラウドで作ったコンテナイメージだから安全と思い込みがちになってしまいます。そんなことは決してありません。コンテナイメージのベースが古いままだとセキュリティホールの温床となります。自分が普段利用するPCなどのウィルスチェックやOSのセキュリティパッチは注意深くチェックしていても、コンテナイメージまで気が回らない人も利用者には多いかと思います。より安全にコンテナイメージを管理・運用するために、定期的なコンテナイメージの脆弱性チェックをお勧めします。
　昨今、巷でおうちKubernetesが流行っていますが、ご家庭でもKubernetesだけでなく、ITリテラシーの高い運用をやってみるのは如何でしょうか。