Help us understand the problem. What is going on with this article?

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

はじめに

CNCFのIncubating Projectsであるコンテナリポジトリ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の詳細はこちらをご参照。

$ 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を利用することも可能です。

デプロイ先の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の検証
を参考に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 --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

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

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

スクリーンショット 2019-01-14 22.00.47.png

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

スクリーンショット 2019-01-14 22.01.01.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

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のデータベースを利用して、脆弱性チェックを行います。

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

スクリーンショット 2019-02-05 22.30.44.png

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

スクリーンショット 2019-02-05 22.33.48.png

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

スクリーンショット 2019-02-05 22.29.44.png

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

スクリーンショット 2019-02-05 22.37.16.png

おわりに

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away