Dockerイメージを格納するため、HelmでHarborをデプロイしてみた

# はじめに

これまでDockerHubの有料プランを利用してきましたが、さすがに開発用のイメージまで置く余裕はないので、Harborをデプロイして試すことにしました。今回はHelmを利用しています。

> この記事を投稿したタイミングでは、DockerHubはPrivateコンテナの数に応じた料金体系でした。現在は最低限の有料プランでコンテナ数の制限はなくなっていますので、再びDockerHubをメインで利用しています。しかしDockerHubの事故によって外部に流出した場合に問題となる可能性のあるデータを含むコンテナについては、インターネットを中継しないようイントラネットのHarborを利用しています。

# 環境

* Harbor-Helmを利用してk8s環境にデプロイ
* ユーザー認証はLDAPを利用
* HarborへのアクセスにはLoadBalancerを利用
* Rook/Cephを利用しているためBlock Storageの利用にStorageClassの指定が必要
* Private LAN内部では、利用している192.168.x.xのIPについて、DNSのホスト名⇔IPの解決が可能 (dnsmasqを利用)

今回はIntranetからアクセスできるよう境界NWに設置したReverse-Proxyサーバー(nginx)を経由してHarborにアクセスし、Harbor自身はIntranetから直接アクセスできないようにしています。

## ユースケース1 - NginxをFrontend Reverse-Proxy Serverとして利用する

nginxをLANとの境界に設置して、IntranetとHarborが動作するk8sクラスターを設置するLANを分離しています。

この場合はdocker login/push を実行する時に通信に、正規のTLS証明書が利用できます。利用者側での特別な設定は必要ありません。もし自己証明TLS鍵を利用するのであれば、後述するPrivate Network内で利用する方法と同様に利用者側でCAの証明書情報を管理する必要があります。

内部の第三者が接続できるため、adminパスワードの管理には特に注意が必要です。

## ユースケース2 - Private Netowrk内から利用する

この場合はDockerからコンテナを登録する場合と、k8sからコンテナイメージをpullする場合に考慮が必要です。

通常はdockerコマンドからHarborに接続するためのTLS証明書について、独自CAが発行する自己認証鍵になるはずです。このような構成では、独自のCAの証明書をdockerやk8sクラスターに登録する必要があります。TLS鍵の準備には次の2つの方法があります。

1. TLS鍵をharborに自動生成させる。生成されたCAの証明書をdockerやクライアントPC(openssl-client)などに登録する
2. Private LAN内部で利用するDomain用にeasy-rsa3を利用してCAを構築し、Harbor用のサーバー証明書を作成し利用する。(1)と同様にeasy-rsa3が生成したCA局の証明書を利用側に登録する

自己証明鍵を利用する場合には、docker login や docker push でエラーになります。利用するシステムの /etc/docker/certs.d/<harbor ホスト名>/ca.crt にCAの証明書を配置する必要がある点に注意してください。

Kubernetesがdockerを利用していれば、同様にk8sクラスターでもCA証明書を配置する必要があります。

# 導入までの流れ

以下では手元にユースケース1やユースケース2-2を想定して、手元にTLS証明書のセット(ca.crt, {tls.crt, tls.key})があることを前提に説明を進めます。

## kubectlコマンドが実行できる場所でgit clone

```bash:基本的なセットアップ
$ git clone https://github.com/goharbor/harbor-helm.git
$ cd harbor-helm
## git tag で最新版を確認し、checkoutします。
$ git checkout refs/tags/v1.3.0 -b my_v1.3.0
```

繰り返し作業をいろいろするので、作業用にMakefileを準備しておきます。

```bash:git-cloneを実行したディレクトリがCWD
$ cd ..   ## from harbor-helm
$ cat > Makefile
```

```makefile:Makefile

NAMESPACE = harbor
HELM_RELEASE = my-harbor

.PHONY: init install delete delete-ns check create-tls-secret

init:
        sudo kubectl create ns $(NAMESPACE)

install:
        (cd harbor-helm; sudo helm install --name $(HELM_RELEASE) --namespace $(NAMESPACE) .)

delete:
        (cd harbor-helm; sudo helm delete --purge $(HELM_RELEASE))

delete-ns:
        sudo kubectl delete ns $(NAMESPACE)

check:
        sudo helm -n $(NAMESPACE) list

create-tls-secret:
        sudo kubectl -n $(NAMESPACE) create secret generic nginx-tls-keys --from-file pki/ca.crt --from-file pki/tls.crt --from-file pki/tls.key
```

この部分をCopy&Pasteするとインデントはスペース8個になってしまいますが、タブ文字である点に注意してください。

## TLS鍵の配置

values.yamlのsecretName:を空にしたままにすると、TLS鍵が自動的に生成され、secret/__helm_release__-__namespace__-nginxに設定されます。今回はca.crt, tls.crt, tls.keyファイルをpki/ディレクトリに準備し、**secret/nginx-tls-keys**に格納します。

```bash
$ make init
$ mkdir pki
$ cp .../ca.crt .../harbor.example.com.crt .../harbor.example.com.key pki/
$ openssl rsa -in pki/harbor.example.com.key -out pki/harbor.example.com.nopass.key
$ ln -s harbor.example.com.crt pki/tls.crt
$ ln -s harbor.example.com.nopass.key pki/tls.key
$ make create-tls-secret
```

## values.yamlの編集

git cloneしているので、```git diff```の結果を添付します。これはProxy(proxy.example.com)を経由せずに、直接Private LAN内部でアクセスする場合のものです。

Proxy Serverを経由する場合には、externalURL:に外部に公開するProxy ServerのURLを指定します。commonNameにはPrivate LAN内部でDNS名前解決できるホスト名を指定しています。

```yaml:harbor-helm/values.yaml
diff --git a/values.yaml b/values.yaml
index 474ffdb..74f9fa5 100644
--- a/values.yaml
+++ b/values.yaml
@@ -2,7 +2,7 @@ expose:
   # Set the way how to expose the service. Set the type as "ingress",
   # "clusterIP", "nodePort" or "loadBalancer" and fill the information
   # in the corresponding section
-  type: ingress
+  type: loadBalancer
   tls:
     # Enable the tls or not. Note: if the type is "ingress" and the tls
     # is disabled, the port must be included in the command when pull/push
@@ -16,14 +16,14 @@ expose:
     # "ca.crt" - the certificate of CA (optional), this enables the download
     # link on portal to download the certificate of CA
     # These files will be generated automatically if the "secretName" is not set
-    secretName: ""
+    secretName: "nginx-tls-keys"
     # By default, the Notary service will use the same cert and key as
     # described above. Fill the name of secret if you want to use a
     # separated one. Only needed when the type is "ingress".
     notarySecretName: ""
     # The common name used to generate the certificate, it's necessary
     # when the type isn't "ingress" and "secretName" is null
-    commonName: ""
+    commonName: "harbor.example.com"
   ingress:
     hosts:
       core: core.harbor.domain
@@ -73,7 +73,7 @@ expose:
     # The name of LoadBalancer service
     name: harbor
     # Set the IP if the LoadBalancer supports assigning IP
-    IP: ""
+    IP: "192.168.1.43"
     ports:
       # The service port Harbor listens on when serving with HTTP
       httpPort: 80
@@ -98,7 +98,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://harbor.example.com
 
 # The persistence is enabled by default and a default StorageClass
 # is needed in the k8s cluster to provision volumes dynamicly.
@@ -120,19 +120,19 @@ persistence:
       # Specify the "storageClass" used to provision the volume. Or the default
       # StorageClass will be used(the default).
       # Set it to "-" to disable dynamic provisioning
-      storageClass: ""
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
-      size: 5Gi
+      size: 20Gi
     chartmuseum:
       existingClaim: ""
-      storageClass: ""
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
       size: 5Gi
     jobservice:
       existingClaim: ""
-      storageClass: ""
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
       size: 1Gi
@@ -140,7 +140,7 @@ persistence:
     # be ignored
     database:
       existingClaim: ""
-      storageClass: ""
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
       size: 1Gi
@@ -148,7 +148,7 @@ persistence:
     # be ignored
     redis:
       existingClaim: ""
-      storageClass: ""
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
       size: 1Gi
```

## Harborのデプロイなどの一連の作業

Makefileを配置したディレクトリは次のようになっています。

```bash
$ ls -l
total 32
-rw-rw-r-- 1 yasu yasu   219 Jan  9 12:33 Makefile
drwxrwxr-x 8 yasu yasu  4096 Jan  9 14:10 harbor-helm
```

namespaceは既に存在する(make initした)ものとして、helm installを実行します。

```bash
$ make install
$ make check
NAME            REVISION        UPDATED                         STATUS          CHART           APP VERSION  NAMESPACE
my-harbor       1               Thu Jan  9 21:37:55 2020        DEPLOYED        harbor-1.3.0    1.10.0       harbor 
```

もしharborを削除したい場合には、make delete, make delete-nsを実行します。delete-nsを実行しない場合には、PVCはnamespace内に残ります。

```bash:削除する場合
$ make delete
$ make check
## my-harborが削除されていれば、delete-nsを実行
$ make delete-ns
```

正常に稼動すればWeb UIなどでログインすることができるようになります。Namespaceのsecretにca.crt,tls.crt,tls.keyを登録済みであれば不要ですが、secretName:を空("")にしてhelmが自動生成する証明書を利用する場合には、ca.crtファイルを抽出して、docker push, kubectl を実行するホスト上に登録する必要があります。

## アップグレード

v1.8.3からv1.9.6にアップグレードした時の作業手順をメモしておきます。

```bash:現在の導入状況を確認
$ sudo helm list --namespace harbor
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART         APP VERSION
my-harbor       harbor          6               2022-08-30 03:49:02.246171523 +0000 UTC deployed        harbor-1.8.3  2.4.3      
```

続いて、git を使って新しい tag を checkout していきます。

```bash:
$ cd harbor-helm

## 未保存の作業内容を全てコミットする
$ git add .
$ git commit -m 'save all performed work.'

## masterブランチに移動し、最新にする
$ git checkout
$ git pull

## タグ名を確認し、n+1版の最新版をダウンロードする
$ git tag
$ git checkout refs/tags/v1.9.6 -b my_v1.9.6

## 前バージョンのvalues.yamlとの差分を確認し、追記する
$ git diff my_v1.8.3 values.yaml

## existingClaim: に追記するPVCの名前を確認し、existingClaim: を更新する
$ sudo kubectl -n harbor get pvc
```

values.yamlの内容を正しく記述しないとエラーになってしまうため気をつけること。

差分を再度確認し、問題がなければアップグレードします。

```bash:
$ git diff my_v1.8.3
$ sudo helm -n harbor upgrade my-harbor --force --set fullnameOverride=my-harbor-harbor .
```

再度状況を確認します。実際にはvalues.yamlの編集ミスがあったので、再度 helm upgrade を実行しているので、REVISIONが 6 から 8 に更新されています。

```bash:
$ sudo helm list --namespace harbor
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART         APP VERSION
my-harbor       harbor          8               2023-03-05 12:02:33.732748916 +0000 UTC deployed        harbor-1.9.6  2.5.6 
```

結果として無事にアップグレードが完了しました。

### values.yaml 

参考までに実際に編集した v1.9.6 の values.yaml は次のような変更を実施しています。
(※パスワードなどは無関係な文字列に置換しています)

```diff:harbor-helm/values.yaml
diff --git a/values.yaml b/values.yaml
index b7c2e20..00076dd 100644
--- a/values.yaml
+++ b/values.yaml
@@ -1,7 +1,7 @@
 expose:
   # Set how to expose the service. Set the type as "ingress", "clusterIP", "nodePort" or "loadBalancer"
   # and fill the information in the corresponding section
-  type: ingress
+  type: loadBalancer
   tls:
     # Enable TLS or not.
     # Delete the "ssl-redirect" annotations in "expose.ingress.annotations" when TLS is disabled and "expose.
type" is "ingress"
@@ -20,7 +20,7 @@ expose:
     auto:
       # The common name used to generate the certificate, it's necessary
       # when the type isn't "ingress"
-      commonName: ""
+      commonName: "harbor.example.com"
     secret:
       # The name of secret which contains keys named:
       # "tls.crt" - the certificate
@@ -97,7 +97,7 @@ expose:
     # The name of LoadBalancer service
     name: harbor
     # Set the IP if the LoadBalancer supports assigning IP
-    IP: ""
+    IP: "192.168.1.43"
     ports:
       # The service port Harbor listens on when serving HTTP
       httpPort: 80
@@ -122,7 +122,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://harbor.example.com
 
 # The internal TLS used for harbor components secure communicating. In order to enable https
 # in each components tls cert files need to provided in advance.
@@ -188,7 +188,7 @@ internalTLS:
 ipFamily:
   # ipv6Enabled set to true if ipv6 is enabled in cluster, currently it affected the nginx related component
   ipv6:
-    enabled: true
+    enabled: false
   # ipv4Enabled set to true if ipv4 is enabled in cluster, currently it affected the nginx related component
   ipv4:
     enabled: true
@@ -211,53 +211,53 @@ persistence:
     registry:
       # Use the existing PVC which must be created manually before bound,
       # and specify the "subPath" if the PVC is shared with other components
-      existingClaim: ""
+      existingClaim: "my-harbor-registry"
       # Specify the "storageClass" used to provision the volume. Or the default
       # StorageClass will be used (the default).
       # Set it to "-" to disable dynamic provisioning
-      storageClass: ""
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
-      size: 5Gi
+      size: 50Gi
       annotations: {}
     chartmuseum:
-      existingClaim: ""
-      storageClass: ""
+      existingClaim: "my-harbor-chartmuseum"
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
-      size: 5Gi
+      size: 50Gi
       annotations: {}
     jobservice:
-      existingClaim: ""
-      storageClass: ""
+      existingClaim: "my-harbor-jobservice"
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
-      size: 1Gi
+      size: 10Gi
       annotations: {}
     # If external database is used, the following settings for database will
     # be ignored
     database:
-      existingClaim: ""
-      storageClass: ""
+      existingClaim: "database-data-my-harbor-database-0"
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
-      size: 1Gi
+      size: 10Gi
       annotations: {}
     # If external Redis is used, the following settings for Redis will
     # be ignored
     redis:
-      existingClaim: ""
-      storageClass: ""
+      existingClaim: "data-my-harbor-redis-0"
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
-      size: 1Gi
+      size: 10Gi
       annotations: {}
     trivy:
-      existingClaim: ""
-      storageClass: ""
+      existingClaim: "data-my-harbor-trivy-0"
+      storageClass: "rook-ceph-block"
       subPath: ""
       accessMode: ReadWriteOnce
-      size: 5Gi
+      size: 50Gi
       annotations: {}
   # Define which storage backend is used for registry and chartmuseum to store
   # images and charts. Refer to
@@ -361,7 +361,7 @@ updateStrategy:
 logLevel: info
 
 # The initial password of Harbor admin. Change it from portal after launching Harbor
-harborAdminPassword: "Harbor12345"
+harborAdminPassword: "ff0dfac092171002"
 
 # The name of the secret which contains key named "ca.crt". Setting this enables the
 # download link on portal to download the CA certificate when the certificate isn't
@@ -369,7 +369,7 @@ 
 harborAdminPassword: "Harbor12345"
 caSecretName: ""
 
 # The secret key used for encryption. Must be a string of 16 chars.
-secretKey: "not-a-secure-key"
+secretKey: "3713e2acf3d71ca3"
 
 # The proxy settings for updating trivy vulnerabilities from the Internet and replicating
 # artifacts from/to the registries that cannot be reached directly
@@ -565,10 +565,10 @@ registry:
   # If true, the registry returns relative URLs in Location headers. The client is responsible for resolving 
the correct URL.
   relativeurls: false
   credentials:
-    username: "harbor_registry_user"
-    password: "harbor_registry_password"
+    username: "474a32523383f006"
+    password: "ae630f3a5cba1461"
     # Login and password in htpasswd string format. Excludes `registry.credentials.username`  and `registry.c
redentials.password`. May come in handy when integrating with tools like argocd or flux. This allows the same 
line to be generated each time the template is rendered, instead of the `htpasswd` function from helm, which g
enerates different lines each time because of the salt.
-    # htpasswdString: $apr1$XLefHzeG$Xl4.s00sMSCCcMyJljSZb0 # example string
+    htpasswdString: "d17f469f791b3d13:$apr1$XLefHzeG$Xl4.s00sMSCCcMyJljSZb0"
   middleware:
     enabled: false
     type: cloudFront
@@ -750,7 +750,7 @@ database:
       repository: goharbor/harbor-db
       tag: v2.5.6
     # The initial superuser password for internal database
-    password: "changeit"
+    password: "3cefaedde881b07c"
     # The size limit for Shared memory, pgSQL use it for shared_buffer
     # More details see:
     # https://github.com/goharbor/harbor/issues/15034

```

# TLS接続時の検証エラーの回避

docker・kubectlコマンドから直接、Harborに接続する場合には、https://docs.docker.com/registry/insecure/ に記述があるように、自己署名CAをそのコマンドを実行するマシンの /etc/docker/certs.d/__hostname__/ca.crt に配置する方法がお勧めです。

WebブラウザからHarborのWeb UIにアクセスする場合には、そのシステム(Ubuntuであればca-certificatesの管理下)に自己署名CAを登録する方法がお勧めです。

自分が管理していない自己署名CAをシステムに登録する方法はお勧めしませんが、TLS鍵の検証を無視する設定は安直ですができるだけ避けるべきだと思います。自分の管理下にある自己署名CAを登録する方が、対象を限定できるので、お勧めです。

実際に鍵を配布する方法には、ansibleを利用して、全マシンに配布しています。

## 自己署名CAの抽出

kubectlが実行できるマシンでsecretに登録されているca.tlsの内容を取得します。1行で実行すると次のようになりますが、単純に```$ kubectl get secret my-harbor-harbor-nginx -o yaml```の出力からca.crt部分をコピーして、base64 -dの入力に与える方法もあります。

```bash
$ kubectl get secret my-harbor-harbor-nginx -o jsonpath='{.data.ca\.crt}' | base64 -d
-----BEGIN CERTIFICATE-----
.....
-----END CERTIFICATE-----
```

ここで画面に出力された内容を、ca.crtとして保存し、docker pushを実行するホスト、kubernetesの各ノードに登録(/etc/docker/certs.d/harbor.example.com/ca.crtへの配置)をします。

# Harborの稼動確認

ca.crtの内容がシステムに登録された後は、Web UIからログインします。

```bash
$ firefox https://harbor.example.com/
```

自己署名CAを利用している場合には警告がでますが、Acceptして次に進みます。

* ID: admin
* Password: Harbor12345

パスワードはログインした後の左上にあるAdminユーザーのメニューから、必ず変更するようにしましょう。LDAPを利用したいので、Configurationに進んで情報を登録しますが、今回は省略します。

## Projects "library" へのユーザーの登録

LDAPとの接続ができるようになったところで、自分のIDをProjects"libarary"のMembersタブに進み、"+ USER"ボタンからMasterとして登録しています。

libraryプロジェクトに一般ユーザーを登録したところで、イメージを登録します。

## Dockerでのイメージの作成と登録

ネーミングルールは、ホスト名/プロジェクト名/image[:TAG] となっているので、docker buildする場合には適切なtagをつけるか、手元でのテストでも利用すると思うので、次のような方法で、既存のイメージにHarbor登録用の名称を設定するという事になると思います。

```bash:実行例
$ docker login harbor.example.com
Username: user01
Password:

$ docker pull nginx:latest
$ docker tag nginx:latest harbor.example.com/library/mynginx:1.0
$ docker push harbor.example.com/library/mynginx:1.0
The push refers to repository [harbor.example.com/library/mynginx]
918efb8f161b: Pushed 
27dd43ea46a8: Pushed 
9f3bfcc4a1a8: Pushed 
2dc9f76fb25b: Pushed 
1.0: digest: sha256:2695d3e10e69cc500a16eae6d6629c803c43ab075fa5ce60813a0fc49c47e859 size: 1152
```

独自にProjectを登録している場合には、"library"の部分を適宜変更します。

最後にWeb UIから自分のIDでログインし、イメージが登録されていることを確認しておきます。

## Kubernetesでのデプロイメントのテスト

次の2つのYAMLファイルを準備します。

```yaml:deploy-mynginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: default
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      imagePullSecrets:
      - name: selfreg
      containers:
      - name: nginx
        image: harbor.example.com/library/mynginx:1.0
        ports:
        - containerPort: 80
```

```yaml:svc-mynginx.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: default
spec:
  type: LoadBalancer
  ports:
     -  port: 80
        protocol: TCP
        targetPort: 80
  selector:
    app: nginx
```

deploymentの中で参照(.spec.template.spec.imagePullSecrets.name)している、**selfreg**を作成します。

```bash:LDAPに登録しているパスワードを$regpasswd変数に格納
$ read -s regpasswd 
## 入力しているパスワードは非表示な点に注意
$ kubectl -n default create secret docker-registry selfreg --docker-username=user01 --docker-email=user01@example.com --docker-password="${regpasswd}"
```

これで作成した2つのYAMLファイルを登録します。

```bash
$ kubectl -n default apply -f deploy-mynginx.yaml
$ kubectl -n default apply -f svc-mynginx.yaml

## 状況の確認
$ kubectl -n default get all
NAME                                   READY   STATUS    RESTARTS   AGE
pod/nginx-deployment-55d5f94b5-nbw4v   1/1     Running   0          43m
pod/nginx-deployment-55d5f94b5-wkwb4   1/1     Running   0          43m


NAME                 TYPE           CLUSTER-IP    EXTERNAL-IP    PORT(S)        AGE
service/kubernetes   ClusterIP      10.233.0.1    <none>         443/TCP        75d
service/nginx        LoadBalancer   10.233.7.30   192.168.1.24   80:30243/TCP   59m


NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-deployment   2/2     2            2           59m

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-deployment-55d5f94b5   2         2         2       43m
```

正常にCAファイルが登録されていない場合には、次のようにエラーになります。

## LDAP設定時に注意する点

HarborのLDAP設定は複雑なことができないようになっていて柔軟性には欠けますが、反面、設定自体に困る事はあまりないと思います。

ただ、LDAPサーバーのURLを指定する部分では、次のように設定するとエラーになります。

```text:エラーとなるURL指定
ldaps://ldap.example.com:636
```

末尾の **:636** の部分は間違いではないはずですが、エラーとなるため ```ldaps://ldap.example.com``` のように指定する必要がありました。

### Kubernetesにca.crtが登録されていない場合のエラー

ファイルが正常に配置されていれば再起動は不要ですが、内容が正しくない場合には、次ようなエラーとなります。

```bash:
$ kubectl -n default describe pod -l app=nginx
...
Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  51s                default-scheduler  Successfully assigned default/nginx-deployment-55d5f94b5-km9ms to ibox01a2
  Normal   BackOff    18s (x2 over 43s)  kubelet, ibox01a2  Back-off pulling image "harbor.example.com/library/mynginx:1.0"
  Warning  Failed     18s (x2 over 43s)  kubelet, ibox01a2  Error: ImagePullBackOff
  Normal   Pulling    4s (x3 over 49s)   kubelet, ibox01a2  Pulling image "harbor.example.com/library/mynginx:1.0"
  Warning  Failed     4s (x3 over 44s)   kubelet, ibox01a2  Failed to pull image "harbor.example.com/library/mynginx:1.0": rpc error: code = Unknown desc = Error response from daemon: Get https://harbor.example.com/v2/: x509: certificate signed by unknown authority
  Warning  Failed     4s (x3 over 44s)   kubelet, ibox01a2  Error: ErrImagePull
```

およそ、ここまでで一般的なユースケースは満たせるのかなと思います。

# デプロイ後に遭遇した問題

ある日、気がつくとcoreプロセスが正常に起動しない状態になっていました。
postgresqlが稼動しているdatabaseに接続できない事が理由と分かったので、ログを確認すると次のようなメッセージが表示されていました。

```bash:ログを表示
$ kubectl -n harbor logs statefulset.apps/my-harbor-harbor-database
FATAL:  data directory "/var/lib/postgresql/data" has group or world access
DETAIL:  Permissions should be u=rwx (0700).
```

今回はこれを修復する方法として、initContainersを利用します。

```bash:SatetefulSetの定義を修正
$ kubectl -n harbor edit statefulset.apps/my-harbor-harbor-database
```

```yaml:表示されたstatefulset.apps/my-harbor-harbor-databaseの定義を修正
      initContainers:
      - args:
        - -c
        - "chown -R postgres:postgres /var/lib/postgresql/data ; chmod 0700 /var/lib/postgresql/data"
        command:
        - /bin/sh
```

これで問題なく起動時にマウントした/var/lib/postgresql/dataのパーミッションを変更し、起動するようになりました。

以上