Google Container Engine上でLet's Encryptによる証明書を自動更新する（kube-lego）

# 背景

GKE上でLet's Encrypt（certbot）によるhttps化をするには、いくつか課題があります。
特に自動更新は厄介です。

本記事では[kube-lego](https://github.com/jetstack/kube-lego)を用いて、証明書の自動更新に対応する構成を記載します。
これを用いると、サービス側のファイルや設定をほぼいじらずにhttps化できます（ついでにhttp2も）。

なお、節約のためLet's Encryptで無料の証明書を取っていますが、これが複雑さの原因です。
証明書を買っている方は設定のみ行えば良いので、この記事の内容は一部しか役立ちません。
お金の力は偉大です。

# 前提

- [Railsを立てる](http://qiita.com/esplo/items/76a1ecaf09843c49cfaf)
- [独自ドメインを貼る](http://qiita.com/esplo/items/bd2e36cfae797d0d480a)
- [nginxを挟む](http://qiita.com/esplo/items/6a47f635556b085c975c)

上記記事の手順を踏んでいると仮定します。
なお、独自ドメイン周りは本記事の手順の後半で再度いじることになる可能性が高いです。


# できること

- Let's Encryptで証明書の無料＆自動更新

# 全体像

ingressでL7ロードバランサーを設定し、Let's Encrypt認証と通常のアクセスを振り分けます。
（[nginxを挟む](http://qiita.com/esplo/items/6a47f635556b085c975c)の手順に従っている場合は）通常のアクセスではnginxを挟み、httpであればhttpsにリダイレクトします。

Let's Encryptの認証処理や一部設定は、[kube-lego](https://github.com/jetstack/kube-lego)が行います。
kube-legoはアプリケーションとはnamespaceから区切り、独立したサービスとして立てます。

# 手順

## （オプション）http->httpsのリダイレクト

2016年の現状、GCEのL7ロードバランサーではリダイレクトができません。
そのため、nginxを挟んで処理してね！とドキュメントに書かれています。

https://github.com/kubernetes/contrib/tree/master/ingress/controllers/gce#tls

http->httpsのリダイレクトを行う場合は、[nginxを挟む](http://qiita.com/esplo/items/6a47f635556b085c975c)に沿って設定を行ってください。

## Service typeの変更

serviceのタイプは`LoadBalancer`として設定していましたが、これを`NodePort`にします。
これにより、サービスにロードバランサーを割り当てず、こちらで設定ができます。

```yaml
apiVersion: v1
kind: Service
metadata:
  name: yourservice
  labels:
    app: yourservice
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
  selector:
    app: yourservice
```

上記で設定後`kubectl get svc`で見てみると、`EXTERNAL IP`が`<nodes>`となっています。

## ingressの設定

GCPなので某ゲームが真っ先に浮かびますが、一般的な名詞（入場権とか）として使われているようです。
これは、L7ロードバランサーの設定をするもので、サービスへのアクセスルールを設定することができます。
[公式ドキュメント](http://kubernetes.io/docs/user-guide/ingress/)に分かりやすいアスキーアートがあります。

ingressの設定は以下のようになります。

```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: yourservice
  namespace: default
  annotations:
    kubernetes.io/tls-acme: "true"
    kubernetes.io/ingress.class: "gce"
spec:
  tls:
  - hosts:
    - yourhost.jp
    secretName: yourservice-tls
  rules:
  - host: yourhost.jp
    http:
      paths:
      - path: /*
        backend:
          serviceName: yourservice
          servicePort: 80
```

これは以下の処理を行っています。

- TLSターミネーション
    - `yourservice-tls`という名前のkubernetesのsecretを取り出します
    - ここには.crtなどが入っています
- アクセスの割り振り
    - ここでは/*（要はデフォルト）のみを設定しています
    - Let's Encryptの/.well-known/...はどうするの？となりますが、kube-legoが作ってくれます

ingressを使うと、自動でL7ロードバランサーが割り当てられます。
詳細は[Google HTTP(S) Load Balancer](https://cloud.google.com/compute/docs/load-balancing/http/)。

applyすると、`kubectl get ing`でステータスが確認できます。


## kube-legoを立てる

[kube-legoによるGCEのサンプル](https://github.com/jetstack/kube-lego/tree/master/examples/gce)が用意されているので、この手順通りに進めます。

まずはkube-lego用のnamespaceを作ります。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: kube-lego
```

次に、configMapを設定します。
ここでLet's Encryptのサーバーを指定しますが、まずはテスト用のAPIから取得し、上手く行ってから本番のAPIに切り替えましょう。
[Staging Environment](https://letsencrypt.org/docs/staging-environment/)

なお、本番APIにconfigMapを切り替えた際は、kube-lego周りのsecretを一旦削除しないと正しく取得をしてくれないので要注意です。

```yaml
apiVersion: v1
metadata:
  name: kube-lego
  namespace: kube-lego
data:
  lego.email: "your@mail.address"
  lego.url: "https://acme-staging.api.letsencrypt.org/directory"
kind: ConfigMap
```

最後に、サービスのdeploymentです。

```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: kube-lego
  namespace: kube-lego
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: kube-lego
    spec:
      containers:
      - name: kube-lego
        image: jetstack/kube-lego:0.1.3
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        env:
        - name: LEGO_EMAIL
          valueFrom:
            configMapKeyRef:
              name: kube-lego
              key: lego.email
        - name: LEGO_URL
          valueFrom:
            configMapKeyRef:
              name: kube-lego
              key: lego.url
        - name: LEGO_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: LEGO_POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          timeoutSeconds: 1
```

`jetstack/kube-lego`をポート8080で立て、各種値をconfigMapから拾ったり設定したりしています。
特に他サービスに依存しないため、書き換える必要はありません。

## 確認

### kube-legoの確認

`kubectl describe ing`で詳細を見てみると、
`/.well-known/acme-challenge/* 	kube-lego-gce:8080 (<none>)`というものが追加されています。
無事アクセスが分かれてくれそうですね。

追加した時点から、kube-legoは認証の確認を行っています。
`kubectl --namespace=kube-lego get pod`でポッドIDを確認し、
`kubectl --namespace=kube-lego log <pod-id>`
でログを見てみましょう。

上手く行っていない場合は原因がログに出ているので対処します。

成功すると、以下のようなログが出ています。
`msg="creating new secret" context=secret name=yourservice-tls namespace=default`
kube-legoが`yourservice-tls`というsecretに設定をしてくれた模様です。

`msg="cert expires in 90.0 days, no renewal needed"`
というログも出ており、期限に近づくと改めて認証を行い、証明書を取得してくれます。

### サービスの確認

最後に、取得したドメインにhttpsでアクセスしてみます。
ページが見つからないと言われたりする場合は、ingressで割り振られた新しいIPにドメインが正しく紐付いているか確認してください。

- ingressを一旦削除
- Cloud DNSの設定、IPを確認
- IPで正しくアクセスできるか確認
- Static IPを解放してやり直す
- などなど...

正しくアクセスできると、「この接続は信頼されていません...」というメッセージが出てくるかと思います。
これはテスト用のLet's Encrypt APIを使ったためです。

configMapを本番用のAPI URL（`https://acme-v01.api.letsencrypt.org/directory`）に書き換え、applyをします。

```bash
kubectl delete secret yourservice-tls
kubectl --namespace=kube-lego delete secret --all
kubectl --namespace=kube-lego delete pod --all
```

を行い、secretの削除＆podの再起動をしてすぐさま再取得をしてもらいます。

証明書が発行され、サービスに警告無しでhttpsアクセスできたら成功です！:tada: