GKE でサービスを HTTPS と HTTP/2 に対応する(kube-lego 編)


## 注意

https://github.com/jetstack/kube-lego/blob/master/README.md#kube-lego

> ⚠️
> kube-lego is in maintenance mode only. There is no plan to support any new features. The latest Kubernetes release that kube-lego officially supports is 1.8. The officially endorsed successor is cert-manager.
> 
> If you are a current user of kube-lego, you can find a migration guide here.
> ⚠️

kube-lego は既に [cert-manager](https://github.com/jetstack/cert-manager) によって置き換えられており、この記事は単なる歴史的文書です。
既に少し古いですが、 [GKE で TLS 証明書を自動管理(cert-manager DNS-01 編)](https://qiita.com/apstndb/items/3a39a1e6acacbbc30765) で紹介しています。

## 本文

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

[@apstndb](http://qiita.com/apstndb) です。
今は株式会社アカツキのライブエクスペリエンス事業部にて新規事業で技術検証を行うというミッションのもと、 GKE でコンテナベースのサービスを本番運用するヨイトマケと唄えないタイプのインフラエンジニアをしています。 

実際に GKE の上で動かしているサービスのアーキテクチャについては [@shimpeiws](http://qiita.com/shimpeiws) が7日目に書いた [ReactでSSR(サーバサイドレンダリング)している話](http://qiita.com/shimpeiws/items/f0bf909079b939b354e3) をご参照ください。

今回は Google Container Engine(GKE) で HTTPS に対応する手段の1つとして、 kube-lego を使って Let's Encrypt からの証明書の取得の自動化を行います。

## TL;DR

- 暗号化必須の時代
	- 無料の認証局である Let's Encrypt
- Kubernetes Ingress と Let's Encrypt を連携して自動的に証明書を取得、更新する kube-lego
- GKE での kube-lego の使用方法

## 前置き

ここからしばらくポエムです。適宜読み飛ばして下さい。

### 暗号化必須の時代へ

> 実を言うと平文 HTTP はもうだめです。
> 突然こんなこと言ってごめんね。
> でも本当です。

Google は「[より安全なウェブを目指して](https://googledevjp.blogspot.jp/2016/09/moving-towards-more-secure-web.html)」という記事で、2017年1月にリリース予定の Chrome 56 では HTTPS を使っておらずクレジットカードやパスワードの入力欄のあるサイトにデフォルトで "Not Secure"、安全ではないサイトだと表示すると予告しています。
ゆくゆくは暗号化されていなければ入力フォームがなくても赤い警告を出すとしています。そんな警告が出ているサイトはエンドユーザは使いたがりませんね。
気が変わるんじゃないかと思っている人も居るかもしれませんが、最終更新が2016年12月8日の「[Avoiding the Not Secure Warning in Chrome](https://developers.google.com/web/updates/2016/10/avoid-not-secure-warn)」で対応方法を書くくらいです。

> 「えーマジ HTTP ！？」
> 「HTTP が許されるのは2016年までだよねー」

という考えなのは間違いありません。彼らは本気です。

「そんなセンシティブな情報は無いしまだうちは大丈夫…」と思っている人も居るかもしれませんが、 HTTPS に移行する理由は日々増えつつあります。いくつか上げましょう。

- HTTPS で接続しているサイトから HTTP のリソースを取得すると警告
- 各検索エンジンは HTTPS に対応しているかどうかを検索順位決定要因として使用するとしているため SEO 上不利
- iOS アプリの通信も来年から HTTPS 必須
- 1つでも HTTPS に対応していないサブドメインがあると HSTS preload の障害になる
- HTTP/2 を使うには TLS が事実上必須であり、速度的に不利
	- 平文で HTTP/2 ネゴシエーションする h2c に対応しているブラウザは存在しない

こうなってくると、もはやエンドユーザが見ない開発用のサーバであってもデフォルトで HTTPS を使おうという気持ちになってきます。

しかし、 HTTPS を使うには TLS 証明書が必要で、認証局から TLS 証明書の取得をするにはコストが掛かり、ギョーム上稟議書を書かなければならないなど気軽に取得できるものではありませんでした。

そこで現れたのが [Let's Encrypt](https://letsencrypt.org/) です。

### Let's Encrypt

Let's Encrypt は、無料で証明書を発行する非営利の認証局です。運営資金は多くのインターネット関連企業がスポンサーとなることで賄われています。ありがたい話ですね。

#### 信用できるのか？

「無料の証明書なんて信用できるのか？」という疑問を持つ人も居るかもしれません。主に気になるのは下記の2点でしょう。

- 現在 Let's Encrypt はルート認証局ではないため発行されるのは中間証明書
- 発行できるのは Domain Validation(DV) 証明書のみ

中間証明書であることについては、ルート証明局である IdenTrust からの証明書に対応していない環境は死滅しつつありますし、長期的には Let's Encrypt 自身がルート認証局となる流れがあります。

- [FAQ: Let's Encrypt の証明書はブラウザで信頼済みとして扱われますか？](https://letsencrypt.jp/faq/#Browser)
- [公式ブログの和訳: Mozilla が Let's Encrypt Root を認証局として信頼](https://letsencrypt.jp/blog/2016-08-05.html)

DV 証明書しか発行しないことについては、実際にエンドユーザが安心して利用できるようにするために OV 証明書でも足りずに EV 証明書が必要なビジネスはあります。 DV 証明書で十分なところにはコストを掛けずに、 EV 証明書が必要なところに EV 証明書を使えるようにすると良いでしょう。

#### 証明書の取得方法

Let's Encrypt の証明書の発行手続きは [ACME プロトコル](https://ietf-wg-acme.github.io/acme/) で提供されており、取得から更新まで自動化することができます。自動化することを前提しているため、有効期間は3ヶ月と短くなっています。

ACME プロトコルは自分で実装することもできますが、公式の certbot をはじめ、[多くの実装](https://letsencrypt.org/docs/client-options/)があります。

ユーザ数の多い言語のクライアントはかなり揃ってきていますし、なければないで一番槍のチャンスです。(GitHub にはあるかもしれませんが)
ただ、言語ごとの実装は既に多くありますが、コンテナクラスタで CI/CD と連携させて、などを考えはじめると難しくなってきます。
コンテナクラスタと連携して自動化する方法が必要です。

#### [kube-lego](https://github.com/jetstack/kube-lego)


[kube-lego](https://github.com/jetstack/kube-lego) は Kubernetes の中で動作するソフトウェアです。
Kubernetes には [Ingress](http://kubernetes.io/docs/user-guide/ingress/) という HTTP/HTTPS のロードバランサをプロビジョニングする機能があります。kube-lego は Ingress と連携して、自動的に Let's Encrypt から [ACME HTTP-01](https://ietf-wg-acme.github.io/acme/#rfc.section.7.2) を使ってドメイン証明書を取得・更新し、ロードバランサを HTTPS 対応にすることができます。

## GKE で kube-lego を使ってみる

前置きは終わりにして、ここからは kube-lego を使用して、 HTTPS に対応したサービスをインターネットからアクセスできるようにします。 Kubernetes のクラスタとして Google Container Engine, GKE を使用します。
手順は Google の Kelsey Hightower が書いた [GCE の静的 IP アドレスを Kubernetes Ingress から使用するチュートリアル](https://github.com/kelseyhightower/ingress-with-static-ip)を参考にしています。 Thank you Kelsey!


注: この記事では `gcloud` コマンドそのものの設定や GCP の API の有効化等はされていることとして省略します。

### 使用する GKE クラスタの立ち上げ

GKE のクラスタを利用できる状態にしましょう。まだ GKE クラスタを作っていない方は下記のようにして `sandbox-gke` という名前のクラスタを作ることができます。

```
$ gcloud container clusters create sandbox-gke
```

しばらく待つと、 `sandbox-gke` クラスタが立ち上がります。
`sandbox-gke` クラスタに Kubernetes クライアントである `kubectl` コマンドでアクセスできるようにするには次のようにします。

```
$ gcloud components install kubectl
$ gcloud container clusters get-credentials sandbox-gke
```

下記コマンドでクラスタの情報が取得できるようになったら GKE を使用する準備完了です。

```
$ kubectl cluster-info
```

注: 既に kubectl を設定していた人はデフォルトの context が変更されてしまいますので、元のクラスタにアクセスしたい場合は適宜 `kubectl use-context` 等を使ってください。

### kube-lego の準備

kube-lego を使って証明書を取得するには、 kube-lego を Kubernetes にあらかじめデプロイする必要があります。

kube-lego のコンテナは [jetstack/kube-lego](https://hub.docker.com/r/jetstack/kube-lego/) として DockerHub 上に存在しますが、GitHub 上にデプロイに関する設定の例が一通りあるので、それをベースにします。
まず、レポジトリの内容を取得します。

```
$ git clone https://github.com/jetstack/kube-lego.git
```

GCE の HTTPS ロードバランサと連携する設定は下記のパスの中にあります。

```
kube-lego/examples/gce/lego/
```

#### kube-lego の ConfigMap の編集

kube-lego から Let's Encrypt を使うためにメールアドレスを設定します。 ` kube-lego/examples/gce/lego/configmap.yaml
` ファイルを編集して `lego.email` を適切に設定します。

#### kube-lego のデプロイ

下記のようにして kube-lego をデプロイします。

```
$ kubectl apply -f  kube-lego/examples/gce/lego
namespace "kube-lego" created
configmap "kube-lego" created
deployment "kube-lego" created
```

これらは全て `kube-lego` という Namespace 内に作成されます。実際に作成されたものは下記のようにして確認できます。

```
$ kubectl get deployment,replicaset,pod,configmap --namespace kube-lego
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/kube-lego   1         1         1            1           1h
NAME                     DESIRED   CURRENT   READY     AGE
rs/kube-lego-937511810   1         1         1         1h
NAME                           READY     STATUS    RESTARTS   AGE
po/kube-lego-937511810-e7q19   1/1       Running   0          1h
NAME           DATA      AGE
cm/kube-lego   2         1h
```

これで、 kube-lego の準備ができました。これからサービスのことを考えていきましょう。

### 対象とするサブドメインの用意

TLS 証明書を取得するには、証明の対象のドメイン名が必要です。また、 Let's Encrypt の ACME HTTP-01 認証のためにそのドメイン名に実際にアクセスできる必要があります。

#### 静的 IP アドレスの取得

GCE の HTTPS ロードバランサで使用するグローバルアドレスを準備します。
`kubernetes-ingress` という名前で作成するには下記のようにします。

```
$ gcloud compute addresses create kubernetes-ingress --global
```

作成した静的 IP アドレスは下記のコマンドで確認出来ます。

```
$ gcloud compute addresses describe kubernetes-ingress --global --format='value(address)'
```

#### DNS の設定

上で取得した IP アドレスを適当な使えるドメイン名の A レコードに設定しましょう。

DNS 周りの具体的な設定については割愛しますが、使えるドメイン名を持っていない方は数ヶ月間で失効する短命なドメイン名を無料で取得できる [freenom](http://www.freenom.com/) などを利用すると良いでしょう。
運良く空いていた `example.cf` が取得できたので、今回の例ではサブドメイン `lego-example.example.cf` を使用します。

注: DNS で解決できる名前が欲しい場合に便利な [xip.io](http://xip.io) は Let's Encrypt の [Rate Limit](https://letsencrypt.org/docs/rate-limits/) に引っかかるため証明書が取得できません。( [ステージング](https://letsencrypt.org/docs/staging-environment/)を利用することは一応可能 )


### 実際にサービスを動かす


#### Kubernetes 上の Service の準備

サービスの実体となる Kubernetes の Service を準備します。  DockerHub のオフィシャルコンテナである [nginx](https://hub.docker.com/_/nginx/) から Pod を生成し、 Service として expose します。

```
$ kubectl run nginx --image nginx:1.11 --port 80 --replicas=3
$ kubectl expose deployment nginx --type NodePort
```

`nginx` Service が立ち上がりました。この Service はポート 80 の HTTP しか提供していないことに注目してください。

```
$ kubectl get service nginx
NAME      CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
nginx     10.27.241.241   <nodes>       80/TCP    1m
```

#### kube-lego を使う Ingress の作成

Ingress を作成する専用のコマンドはないので、 yaml を使って作成しましょう。
注: 2箇所存在する `lego-example.example.cf` は適宜設定したドメイン名に書き換えて下さい。

```
$ cat > nginx-ing.yaml << EOF
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "kubernetes-ingress"
    kubernetes.io/ingress.class: "gce"
    kubernetes.io/tls-acme: "true"
spec:
  tls:
  - secretName: kubernetes-ingress-tls
    hosts:
      - lego-example.example.cf
  rules:
    - host: lego-example.example.cf
      http:
        paths:
        - path: /
          backend:
            serviceName: nginx
            servicePort: 80
  backend:
    serviceName: nginx
    servicePort: 80
EOF
$ kubectl apply -f nginx-ing.yaml
```

実際に設定されるまでしばらく時間がかかります。
`Networking → Load balancing` や下記コマンドで見られる kube-lego のログを眺めるなどして待ちましょう。

```
$ kubectl logs -f --namespace kube-lego $(kubectl get pod --namespace kube-lego -l app=kube-lego -o name)

```


#### ブラウザでの接続確認

しばらく経つと鍵が取得され HTTPS でアクセスできるようになります。[HTTP/2 and SPDY indicator](https://chrome.google.com/webstore/detail/http2-and-spdy-indicator/mpbpobfflnpcgagjijhmgnchggcjblin) を入れれば下のキャプチャのように HTTP/2 が有効になっていることも確認できます。

![screenshot-https-kube-lego.png](https://qiita-image-store.s3.amazonaws.com/0/1936/7a61ce06-3cd3-c5f0-e909-e2061c0883f7.png "screenshot-https-kube-lego.png")

### 後片付け

実際に今回の記事の内容を実行した人は、そのままにしておくと無駄に課金されてしまうことになります。使い終わったものは削除しましょう。 
GKE クラスタを単に削除しただけではロードバランサと静的 IP アドレスに課金され続けてしまうので、 GKE クラスタを削除する前に Ingress と静的 IP アドレスを削除します。 

```
$ kubectl delete ingress nginx
$ gcloud compute addresses delete kubernetes-ingress --global
```

Ingress から作られたロードバランサが削除されたことを確認してから、 GKE クラスタを削除します。

```
$ gcloud container clusters delete sandbox-gke
```

## まとめとさらなる展望

kube-lego を使うことで、ドメイン名で解決できるグローバル IP アドレスさえ用意すれば Kubernetes 側の設定だけで HTTPS に対応したサービスを提供することができました。
Ingress の生成に先立って GCE や Cloud DNS の API を使えば全てを自動化することも可能でしょう。

このように便利な kube-lego ですが、 Ingress に依存しているため自由度が低いという欠点があります。例えば下記のような場合です。

- HTTP 以外を TLS 対応したい
- 自前で TLS ターミネーションしたい

Kubernetes と Let's Encrypt の連携手段は kube-lego だけではありません。
後日、下記の実装も試してみようと思います。

- https://github.com/kelseyhightower/kube-cert-manager
- https://github.com/PalmStoneGames/kube-cert-manager