git-sync で Kubernetes Pod 内のデータを GitHub から同期する


この記事は [Kubernetes Advent Calendar 2016](http://qiita.com/advent-calendar/2016/kubernetes) の22日目の記事です。

Kubernetes を使っていると、 Pod 内のコンテナで動くプロセスから使われるデータを GitHub repository の HEAD に同期して更新したいというような場合が出てきます。
例えば、下記のような場合があります。

- CDN を使わずに小規模に配信するコンテンツ
- DBMS に入れるほどではないデータファイル

このような場合に、プロセスだけでなくデータに対してもデプロイフローを作ると複雑になるので、できれば避けたいところです。
今回は、プロセスが使うデータを自動的に GitHub と同期する方法を紹介します。(実際は GitHub に限らず Git 一般に対応可能です。)

## gitRepo Volume では駄目なのか

Kubernetes には組み込みの [gitRepo　Volume](http://kubernetes.io/docs/user-guide/volumes/#gitrepo) という機能があります。
下記のように指定した Git リポジトリの特定リビジョンの内容を自動的に取得して Volume としてマウントすることが可能です。(例より引用)

> ```
> apiVersion: v1
> kind: Pod
> metadata:
>   name: server
> spec:
>   containers:
>   - image: nginx
>     name: nginx
>     volumeMounts:
>     - mountPath: /mypath
>       name: git-volume
>   volumes:
>   - name: git-volume
>     gitRepo:
>       repository: "git@somewhere:me/my-git-repository.git"
>       revision: "22f1d8406d464b0c0874075539c1f2e96c253775"
```

しかし、 `repository` と `revision` 以外の指定ができないので、特定ブランチの HEAD と自動同期するような用途に使えないだけでなく、プライベートリポジトリにも対応できません。

> In the future, such volumes may be moved to an even more decoupled model, rather than extending the Kubernetes API for every such use case.

という記述もあり、 gitRepo ボリュームの今後の発展はあまり見込めません。

## git-sync

先程の gitRepo には下記の記述がありました。

> more decoupled model

これを実現するものの一つが今回の記事で紹介する [git-sync](https://github.com/kubernetes/git-sync) だと考えられます。
git-sync は 特定のパスが Git の指定したブランチもしくはタグもしくはリビジョンの内容を持つように維持し続けるプログラムです。Kubernetes コミュニティにより開発されていますが、Kubernetes API には依存せず、サイドカーコンテナとして使われることを想定しています。


## git-sync の動作確認

ここからは例として nginx コンテナにマウントされたボリュームに属する `/usr/share/nginx/html` 以下を GitHub リポジトリの master ブランチと同期することで、Web コンテンツを配信する Pod を作って動作確認します。
例として実際にはプライベートではない `apstndb/static-content` というリポジトリを使用していますが、各自必要に応じて作成してください。

まず、今回使用する Namespace を用意します。

```
$ kubectl create namespace git-sync
```


次に、特定のプライベートリポジトリにアクセスできるようにするために [Deploy Key](https://developer.github.com/guides/managing-deploy-keys/#deploy-keys) に公開鍵を登録します。
なお、今回は次のように生成したキーペアを使用しました。

```
$ ssh-keygen -t rsa -b 4096 -C "git-sync" -N '' -f ~/.ssh/git-sync
```

作成した Namespace 内に秘密鍵を Secret としてデプロイします。

```
$ kubectl create secret generic git-sync-creds --from-file=ssh=${HOME}/.ssh/git-sync --namespace git-sync
```

下記のような nginx と git-sync を含む Deployment とそれを指す Service を定義したファイルを作成します。


```
$ cat git-sync-demo.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: git-sync-demo
  namespace: git-sync
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: git-sync-demo
    spec:
      containers:
      - name: nginx
        image: nginx:1.11-alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: git-sync-volume
          mountPath: /usr/share/nginx
      - name: git-sync
        image: gcr.io/google_containers/git-sync:v2.0.3
        volumeMounts:
        - name: git-sync-volume
          mountPath: /usr/share/nginx
        - name: git-secret
          mountPath: /etc/git-secret
        env:
        - name: GIT_SYNC_REPO
          value: git@github.com:apstndb/static-content.git
        - name: GIT_SYNC_BRANCH
          value: master
        - name: GIT_SYNC_ROOT
          value: /usr/share/nginx
        - name: GIT_SYNC_DEST
          value: html
        - name: GIT_SYNC_SSH
          value: "yes"
      volumes:
      - name: git-sync-volume
        emptyDir: {}
      - name: git-secret
        secret:
          secretName: git-sync-creds
---
kind: Service
apiVersion: v1
metadata:
  name: git-sync-demo
  namespace: git-sync
spec:
  type: LoadBalancer
  selector:
    app: git-sync-demo
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
```

`GIT_SYNC_*` の環境変数と、各ボリュームの定義を見れば大体設定は分かっていただけると思います。
詳しくは [`main.go`](https://github.com/kubernetes/git-sync/blob/master/cmd/git-sync/main.go#L40) に書かれています。
なお、他のコンテナの起動の時点で必ず git-sync の同期が終わっている必要があるなら `GIT_SYNC_ONE_TIME` を設定した git-sync を [init-container](http://kubernetes.io/docs/user-guide/pods/init-container/) で動かすと良いでしょう。

ファイルの定義から Kubernetes 上にリソースを作成します。

```
$ kubectl apply -f git-sync-demo.yaml
```

GCE TCP LB が構成されグローバル IP アドレスが取得されるのを待ちます。(IP アドレスは環境によって異なります。)

```
$ kubectl get service --namespace git-sync --watch
NAME            CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
git-sync-demo   10.3.247.189   <pending>     80/TCP    13s
NAME            CLUSTER-IP     EXTERNAL-IP       PORT(S)   AGE
git-sync-demo   10.3.247.189   104.199.164.118   80/TCP    1m
```

`EXTERNAL-IP` に書かれている IP アドレスから git-sync 対象に設定したファイルが読めます。

```
$ wget http://104.199.164.118/README.md -O -
```

確認できたファイルを GitHub に対して push などをすると、実際に更新されることが確認できます。
その際の動作中のログは下記のようになります。(Pod 名は実行するごとに変わります。)

```
$ kubectl logs --namespace git-sync git-sync-demo-832684302-8r3ks git-sync
I1222 08:31:46.542207       1 main.go:162] starting up: ["/git-sync"]
I1222 08:31:48.944960       1 main.go:324] cloned git@github.com:apstndb/static-content.git
I1222 08:31:48.946235       1 main.go:268] syncing to HEAD (5a696ce5f46a3c6c8ee4b14992f13d8077d84719)
I1222 08:31:51.762885       1 main.go:281] added worktree /usr/share/nginx/rev-5a696ce5f46a3c6c8ee4b14992f13d8077d84719 for origin/master
I1222 08:31:51.764538       1 main.go:301] reset worktree /usr/share/nginx/rev-5a696ce5f46a3c6c8ee4b14992f13d8077d84719 to 5a696ce5f46a3c6c8ee4b14992f13d8077d84719
I1222 08:33:57.839532       1 main.go:375] update required
I1222 08:33:57.839556       1 main.go:268] syncing to HEAD (94e6886da482e31fe27b21afb1d6fbd237db99d1)
I1222 08:34:00.202251       1 main.go:281] added worktree /usr/share/nginx/rev-94e6886da482e31fe27b21afb1d6fbd237db99d1 for origin/master
I1222 08:34:00.204707       1 main.go:301] reset worktree /usr/share/nginx/rev-94e6886da482e31fe27b21afb1d6fbd237db99d1 to 94e6886da482e31fe27b21afb1d6fbd237db99d1
```

なお、ファイルの多いリポジトリで `git pull` をするとワークツリーの更新途中で新しいリビジョンと古いリビジョンのファイルが混ざる不整合な状態になることがありますが、 git-sync ではリビジョンごとに別のパスにチェックアウトした上で、シンボリックリンクをアトミックに張り替えるので不整合な状態にはなりません。