Gitbucketをkubernetesにデプロイしてみた

# はじめに

Gitbucketはdockerで稼動させる事についてはドキュメントが充実していて、Kubernetesでの稼動についてはissuesが上がっていたり、簡単に動かすことができるかよく分かりませんでした。

付属のH2 databaseを利用する分には問題なさそうでしたが、外部のdatabaseと連携させようとすると、設定ファイルを外部から与えるための工夫が必要になりました。

初期設定は極力自動化する方針で環境を構築したので、その際のメモを残しておきます。

# 参考文献

* [qiita.com::GitBucketをMySQL8で立ち上げるdocker-composeファイル](https://qiita.com/bunaImage/items/46f3176b93e5a26c847e)
* [(MySQL8.0 認証方式を変更する(Laravel5)](https://qiita.com/ucan-lab/items/3ae911b7e13287a5b917)
* [hub.docker.com::docker-gitbucket](https://hub.docker.com/r/gitbucket/gitbucket)
* [www.xmisao.com::nginxのproxy_passの注意点](https://www.xmisao.com/2014/05/09/nginx-proxy-pass.html)

# 環境

* Kubernetes v1.16.9 (kubespray v2.12.6)
* Nginx (CentOS7 nginx-1.16.1-1.el7) - ReverseProxy Server
* Gitbucket - docker.io/gitbucket/gitbucket:4.29.0
* MySQL - docker.io/mysql:5.6

```text
+----------+           +-------+          +--------------+    +------+
| Intranet |-- https --| nginx |-- http --| LoadBalancer |----| Pods |
+----------+           +-------+          +--------------+    +------+

                https://xxxx/gitbucket/           http://external-ip/
```

直接K8sのLoadBalancerに接続せず、外部からは正式な証明書によるTLS接続を行ない、内部はnon-TLS接続でURL(context-root)も変更しています。

# Setup

あらかじめ次のようなserviceを定義しておきます。

```yaml:service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: bucket
spec:
  type: LoadBalancer
  loadBalancerIP: "192.168.100.196"
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
      name: http
    - port: 29418
      protocol: TCP
      name: ssh
  selector:
    app: bucket
    tier: webapp
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: bucket
spec:
  ports:
    - port: 3306
  selector:
    app: bucket
    tier: mysql
  clusterIP: None
```

## Mysql

あらかじめUser: rootのパスワードや、接続に利用するパスワードを設定しておきます。
コマンドはMakefileに設定しています。

```Makefile:Makefile
NS = gitbucket

.PHONY: setup-secret gen-secret

setup-secret:
        echo -n `openssl rand -hex 16` > mysql-password
        echo -n `openssl rand -hex 16` > bucket-password

gen-secret:
        kubectl -n $(NS) create secret generic mysql-pass --from-file=mysql-password
        kubectl -n $(NS) create secret generic bucket-pass --from-file=bucket-password

show-secret:
        kubectl -n $(NS) get secret mysql-pass -o yaml| grep mysql-password | cut -d: -f2 | sed -e "s/ //g" | base64 -d
        @echo ""
        kubectl -n $(NS) get secret bucket-pass -o yaml| grep bucket-password | cut -d: -f2 | sed -e "s/ //g" | base64 -d
        @echo ""
```

手動で```$ echo -n $(openssl rand -hex 16) > mysql-password```のように実行してもよいのですが、Makefileにタスクとして含めました。

```bash:Makefileの実行
$ make setup-secret
$ make gen-secret
$ rm mysql-password bucket-password
```

ファイルを消した後で設定したパスワードを確認するには、稼動しているPodに入って環境変数を(env,setコマンドなどで）確認する方法もありますが、secretの内容をデコードするタスクをMakefileに設定しています。

MySQLの設定はConfigMapとDeploymentのYAMLファイルで行ないます。

```yaml:configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: gitbucket-database-conf
data:
  database.conf: |
    db {
      url = "GITBUCKET_DB_URL"
      user = "GITBUCKET_DB_USER"
      password = "GITBUCKET_DB_PASSWORD"
    }
  mysql.cnf: |
    [mysqld]
    default-authentication-plugin = mysql_native_password
    character-set-server = utf8mb4
    collation-server = utf8mb4_unicode_ci
```

```yaml:deploy-mysql.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
  namespace: gitbucket
  labels:
    app: bucket
spec:
  selector:
    matchLabels:
      app: bucket
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: bucket
        tier: mysql
    spec:
      containers:
      - image: mysql:8.0.26
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: mysql-password
        - name: MYSQL_DATABASE
          value: gitbucket
        - name: MYSQL_USER
          value: bucketuser
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: bucket-pass
              key: bucket-password  
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql 
        - name: database-auth-conf  
          mountPath: /etc/mysql/conf.d/local.cnf
          subPath: mysql.cnf
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pvc
      - name: database-auth-conf
        configMap:
          name: gitbucket-database-conf
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
  namespace: gitbucket
spec:
  accessModes: [ "ReadWriteOnce" ]  
  storageClassName: rook-ceph-block 
  resources:
    requests:
      storage: 20Gi
```

この結果、User: bucketuserに自動的に設定される権限設定は次のようになっています。

```text:mysqlコマンドライン
mysql> show grants;
+-----------------------------------------------------------+
| Grants for bucketuser@%                                   |
+-----------------------------------------------------------+
| GRANT USAGE ON *.* TO `bucketuser`@`%`                    |
| GRANT ALL PRIVILEGES ON `gitbucket`.* TO `bucketuser`@`%` |
+-----------------------------------------------------------+
2 rows in set (0.00 sec)
```

## SSHアクセスを許可するためのGatewayへの設定

Kubernetesのサーバーは、クライアントから直接接続できない場所に配置しています。
そのため、NginxのReverse Proxyサーバーが稼動しているホストに、SSH用ポート (29418) でのアクセスを許可するための設定を加えておきます。

```yaml:Ansible設定の抜粋
- name: firewalld enable services for external
  firewalld:
    zone: internal
    masquerade: yes
    permanent: true
    state: enabled

- name: firewalld external config for the harbor port forwarding
  firewalld:
    zone: external
    rich_rule: rule family=ipv4 forward-port port=29418 protocol=tcp to-port=29418 to-addr=192.168.100.78
    permanent: yes
    immediate: yes
    state: enabled
```

フロントエンドのNginxはCentOS上で稼動しているので、同様の操作をしようとすると、``firewall-cmd`` を通して設定を加えることになります。確認していませんが、次のような設定になると思われます。

```bash:firewall-cmdによる29418ポートを転送する設定例(未検証)
$ sudo firewall-cmd --zone=external --add-masquerade --permanent
$ sudo firewall-cmd --zone=external --add-rich-rule="rule family=ipv4 forward-port port=29418 protocol=tcp to-port=29418 to-addr=192.168.1.20" --permanent
```

## Gitbucket

```yaml:deployment-gitbucket.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: gitbucket
  name: bucket
  labels:
    app: bucket
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bucket
      tier: webapp
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: bucket
        tier: webapp
    spec:
      initContainers:
      - name: init-database-conf
        image: busybox:1.31.1
        command: ['sh', '-c', 'sed -e s:GITBUCKET_DB_USER:$GITBUCKET_DB_USER: -e s:GITBUCKET_DB_PASSWORD:$GITBUCKET_DB_PASSWORD: -e s,GITBUCKET_DB_URL,$GITBUCKET_DB_URL, /config/database.conf > /gitbucket/database.conf']
        volumeMounts:
        - name: git-data
          mountPath: /gitbucket
        - name: database-conf
          mountPath: /config
        env:
        - name: GITBUCKET_DB_URL
          value: "jdbc:mysql://mysql/gitbucket?useUnicode=true\\&characterEncoding=utf8"
        - name: GITBUCKET_DB_USER
          value: bucketuser
        - name: GITBUCKET_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: bucket-pass
              key: bucket-password  
      - name: wait-database
        image: busybox:1.31.1
        command: ['sh', '-c', 'until (echo |telnet mysql 3306) ; do echo "waiting mysql..."; sleep 4; done;']
      containers:
      - name: gitbucket
        image: gitbucket/gitbucket:4.38.4
        startupProbe:       
          exec:              
            command:          
              - curl        
              - http://localhost:8080/gitbucket/
          failureThreshold: 300                          
          periodSeconds: 10         
        startupProbe:           
          httpGet:        
            path: /   
            port: 8080        
          failureThreshold: 60   
          periodSeconds: 5 
        livenessProbe:  
          httpGet:                     
            path: /          
            port: 8080        
          failureThreshold: 1  
          periodSeconds: 15        
        readinessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 7
          periodSeconds: 7
        ports:
        - containerPort: 8080
          name: bucket
        volumeMounts:
        - name: git-data
          mountPath: /gitbucket
        - name: database-conf
          mountPath: /config
        env:
        - name: GITBUCKET_HOME
          value: /gitbucket
        - name: GITBUCKET_BASE_URL
          value: https://example.com/gitbucket
      volumes:
      - name: git-data
        persistentVolumeClaim:
          claimName: git-data-pvc
      - name: database-conf
        configMap:
          name: gitbucket-database-conf
          items:
          - key: database.conf
            path: database.conf
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: git-data-pvc
  namespace: gitbucket
spec:
  accessModes: [ "ReadWriteOnce" ]
  storageClassName: rook-ceph-block
  resources:
    requests:
      storage: 20Gi
```

# 遭遇したエラー

## Gitbucketで環境変数に設定したデータベースにアクセスしない

環境変数でdatabase.confの設定を上書きできるとあったので外部のMySQLに接続するよう構成を変更したが、うまく動作しないため、起動時に/gitbucket/database.confを上書きするよう変更しました。

```text:
13:58:23.798 [main] INFO  com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
13:58:24.844 [main] ERROR com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Exception during pool initialization.
java.sql.SQLNonTransientConnectionException: Could not connect to address=(host=mysql)(port=3306)(type=master) : Connection refused (Connection refused)
```

このためinitContainersでbusyboxのsedを利用しています。

## MySQLが起動してもネットワークアクセスを受け付けない

Service定義が間違っているのかと見直しましたが、Kubernetes公式チュートリアルを参考にlivenessProbe(mysqladmin ping)を追加したところ定期的に失敗する様子が観察されました。

```text:Pod(mysql-8.0.20)のメッセージ
Events:
  Type     Reason            Age                  From               Message
  ----     ------            ----                 ----               -------
  Warning  FailedScheduling  3m5s                 default-scheduler  persistentvolumeclaim "mysql-pvc" not found
  Warning  FailedScheduling  3m3s (x3 over 3m4s)  default-scheduler  pod has unbound immediate PersistentVolumeClaims (repeated 4 times)
  Normal   Scheduled         3m                   default-scheduler  Successfully assigned gitbucket/mysql-59d8c88f55-2b6xp to u109ls04
  Warning  FailedMount       2m59s                kubelet, u109ls04  Unable to attach or mount volumes: unmounted volumes=[mysql-persistent-storage], unattached volumes=[mysql-persistent-storage database-auth-conf default-token-txprc]: error processing PVC gitbucket/mysql-pvc: PVC is not bound
  Warning  Unhealthy         105s (x3 over 2m4s)  kubelet, u109ls04  Liveness probe failed: mysqladmin: connect to server at 'localhost' failed
error: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)'
Check that mysqld is running and that the socket: '/var/run/mysqld/mysqld.sock' exists!
  Normal   Killing  105s                 kubelet, u109ls04  Container mysql failed liveness probe, will be restarted
  Normal   Created  29s (x4 over 2m36s)  kubelet, u109ls04  Created container mysql
  Normal   Started  28s (x4 over 2m35s)  kubelet, u109ls04  Started container mysql
```

初期化中にlivenessProbeが起動してしまい、初期化途中でプロセスが再起動したため、その後に正常に稼動を続けることができなくなったことが原因でした。livenessProbeを外すと、私の環境では初期化に4分以上かかっていました。

対応としては、当初、livenessProbeのinitialDelaySeconds値を300に設定しましたが、Gitbucketでも同様の問題が発生したので、v1.18以降ではstartupProbeを設定することを予定しています。

現状での副作用として、readinessProbeも同様に動き出す時間が遅くなっているため、initialDelaySecondsに設定した時間が経過した後でないと、GitbucketのinitContainersで設定している3306番ポートチェックが通らないため、Gitbucketの起動処理も遅くなります。最終的にはGitbucketが正常に稼動した後も、1800秒が経過するまでreadinessProbeが実行されないため、80番ポートはGitbucketのPodに全体の起動時間が大きく遅くなっています。


## Timestamp関連の警告と初期化の失敗

```text:Podのメッセージ
2020-05-30 01:56:29+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.30-1debian10 started.
2020-05-30 01:56:29+00:00 [Note] [Entrypoint]: Initializing database files
2020-05-30T01:56:29.420558Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2020-05-30T01:56:29.422063Z 0 [ERROR] --initialize specified but the data directory has files in it. Aborting.
2020-05-30T01:56:29.422084Z 0 [ERROR] Aborting
```

類似の問題は[Stackoverflowの投稿](https://stackoverflow.com/questions/52601684/kubernetes-mysql-image-persistent-volume-is-non-empty-during-init)で報告されています。

これは一時的にmysql:5.7を利用したために発生した問題だったので、mysql:8.0.20に戻して再発しないようにしました。（”--ignore-db-dir=lost+found"を指定するためのオプションはmysql8では削除されています）



## Gitbucketがサービスを開始しない

最初の起動時にGitbucketはデータベース上にテーブルを作成するなどの初期化を行ないますが、手元の環境ではサービスを開始するまでに、少し時間がかかる状況がありました。

```text
07:57:40.070 [main] INFO  g.core.servlet.InitializeListener - Check version
07:57:40.070 [main] INFO  g.core.servlet.InitializeListener - Start schema update
...
08:09:47.584 [main] INFO  g.core.servlet.InitializeListener - Extract bundled plugins...
08:09:47.592 [main] INFO  g.core.servlet.InitializeListener - Extract to /gitbucket/plugins/gitbucket-notifications-plugin-1.8.0.jar
...
2020-05-31 08:12:04.421:INFO:oejs.Server:main: Started @867127ms
```

起動してからWebサーバーが起動するまで、およそ15分が経過しています。この間にGitbucketのPodを再起動した場合、不整合が発生し、Deployment定義を削除して、手動で全テーブルを削除した後、再度Deploymentを読み込み初期化作業を行なう以外には対応できなくなってしまいました。

こういった現象への対応は公式ガイドに記述があります。
残念ながらv.1.18以降でないと利用できないので、現状ではinitialDelaySeconds値を相当に長い時間(1800秒)に設定しています。

* [https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)

v1.18以降ではstartupProbeを設定したいと思います。