前回のつづき。前回は、preemptible instance を使ってできるだけ安上がりなGKE(k8s)クラスタを作ってみた。
せっかくGKEクラスタを作ったので、その上でなにかを動かしてみたい。なにかとはつまりWebアプリケーション。そして2018年も6月なので、最初から https 化された状態を目指してみたい。今回はそれを試してみたもののメモ。
Helm & cert-manager のインストール
少し調べてみたところ、今回のようなケースではcert-managerというものを活用するのが良いようだった。cert-manager は「k8s環境での Let's encrypt を使った SSL/TLS 証明書の取得・管理フローをうまいことやってくれる君」、といったツールのようだった(Automatically provision and manage TLS certificates in Kubernetes
)。cert-manager のインストールには Helm を使う。
$ brew install kubernetes-helm
Helm は、k8s のパッケージマネージャみたいなものらしい。Helm にまつわるものとして、chart
と tiller
というものがある。
chart
はパッケージ(今回のケースだと「cert-manager の chart を使う」、というかんじ)、tiller
はクラスタ側に配置するデプロイ用のコンポーネントらしい。この tiller はHelm を使って chart をインストールするときには必須となるもののようで、以下のコマンドを実行すると tiller のインストールも含めてセットアップされる様子。
$ helm init --upgrade Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.
has been installed into your Kubernetes Cluster
、あらそうでしたか。GKEだと最初からセットアップされてるみたい、失礼しました。
では続いて cert-manager のインストール。
$ curl -sL https://github.com/jetstack/cert-manager/archive/v0.3.0.tar.gz | tar xv $ helm install --name cert-manager --namespace kube-system cert-manager-0.3.0/contrib/charts/cert-manager Error: release cert-manager failed: namespaces "kube-system" is forbidden: User "system:serviceaccount:kube-system:default" cannot get namespaces in the namespace "kube-system": Unknown user "system:serviceaccount:kube-system:default"
エラーが出た。Unknown user、なるほど? 今回の作業のためのサービスアカウントを作る必要がありそう。
$ kubectl create serviceaccount cert-manager --namespace kube-system $ kubectl create clusterrolebinding cert-manager --clusterrole=cluster-admin --serviceaccount=kube-system:cert-manager
こうして作成したサービスアカウントを指定して、再度 helm init
。んで helm install
。
$ helm init --service-account cert-manager --upgrade Tiller (the Helm server-side component) has been upgraded to the current version. Happy Helming! $ helm install --name cert-manager --namespace kube-system cert-manager-0.3.0/contrib/charts/cert-manager NAME: cert-manager LAST DEPLOYED: Sat Jun 23 14:57:30 2018 NAMESPACE: kube-system STATUS: DEPLOYED (中略) NOTES: cert-manager has been deployed successfully! In order to begin issuing certificates, you will need to set up a ClusterIssuer or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer). More information on the different types of issuers and how to configure them can be found in our documentation: http://cert-manager.readthedocs.io/en/latest/reference/issuers.html For information on how to configure cert-manager to automatically provision Certificates for Ingress resources, take a look at the `ingress-shim` documentation: http://cert-manager.readthedocs.io/en/latest/reference/ingress-shim.html
できたっぽい。
証明書の作成
これまで僕が自分で Let's encrypt を使った証明書生成をおこなった際には、対象のドメイン・特定のエンドポイントに対してアクセスを受けられる環境を用意して......、という方法での取得だった(この方式は HTTP-01チャレンジ、というものらしい)。今回はこの方式ではなく、「DNS の TXT レコードを動的に生成、突き合わせることでドメイン所有の確認をおこなう」という DNS-01チャレンジ方式を取ってみたいと思う。
今回証明書の取得対象となる僕のドメインのDNSには Route53 を使っているのだけど、幸い cert-manager は Route53 にも対応しているとのことなので、あとは cert-manager が Route53 に対するアクセス権限を持つ必要がある。
ここの公式ドキュメントを見ると、IAM ユーザーを作って AccessKeyID & SecretAccessKey を取得しておけばよさそうなので、まずはその作成をAWS側でおこなう。手順は割愛するけど、必要なアクセスポリシーも先述のドキュメントに記載されていて、以下の通り。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "route53:GetChange", "Resource": "arn:aws:route53:::change/*" }, { "Effect": "Allow", "Action": "route53:ChangeResourceRecordSets", "Resource": "arn:aws:route53:::hostedzone/*" }, { "Effect": "Allow", "Action": "route53:ListHostedZonesByName", "Resource": "arn:aws:route53:::hostedzone/*" } ] }
ちなみに、このポリシーのまま動かしてみたら「route53:ListHostedZonesByName
の resource は *
にしろや」って怒られた。素直に *
にしたらうまくいったので、ここはこれでいいのかもしれない?
認証情報の作成ができたら、SecretAccessKey の方を kubectl create secret
コマンドを使って登録しておく。これのイメージとしては heroku の 環境変数みたいなかんじかな。
$ kubectl create secret generic prod-route53-credentials-secret --from-literal=secret-access-key=<secret+key/id> secret "prod-route53-credentials-secret" created
generic
のうしろは名前、--from-literal
オプションは key=value
形式での指定になるんだけど、名前とkeyについてもさっきの公式ドキュメントに記載されているものを踏襲してみている。
ここまでできたら、Issuer(証明書を生成する CA 設定)を生成するため、以下のようなことをおこなう。
$ kubectl apply -f - << EOF apiVersion: certmanager.k8s.io/v1alpha1 kind: Issuer metadata: name: letsencrypt-prod spec: acme: # The ACME server URL server: https://acme-v01.api.letsencrypt.org/directory # Email address used for ACME registration email: admin@example.com # Name of a secret used to store the ACME account private key privateKeySecretRef: name: letsencrypt-prod # Enable the HTTP-01 challenge provider http01: {} # ACME dns-01 provider configurations dns01: # Here we define a list of DNS-01 providers that can solve DNS challenges providers: - name: prod-dns route53: region: ap-northeast-1 # optional if ambient credentials are available; see ambient credentials documentation accessKeyID:<ACCESS_KEY_ID> secretAccessKeySecretRef: name: prod-route53-credentials-secret key: secret-access-key EOF issuer "letsencrypt-prod" created
んー、Route53 だけどリージョン指定いるのか......? とりあえず東京リージョンにしてみたけども。
ちゃんと作成できたかどうかは以下のコマンドで確認できる。
$ kubectl describe issuer Name: letsencrypt-prod Namespace: default Labels: <none> (中略) Status: Acme: Uri: https://acme-v02.api.letsencrypt.org/acme/acct/12345678 Conditions: Last Transition Time: 2018-06-23T06:53:30Z Message: The ACME account was registered with the ACME server Reason: ACMEAccountRegistered Status: True Type: Ready Events: <none>
Status: True
Type: Ready
のあたりが大丈夫そうな雰囲気がある。
そして、ここで作った Issuer を使って、証明書・Certificate を作成する。
$ kubectl apply -f - <<EOF apiVersion: certmanager.k8s.io/v1alpha1 kind: Certificate metadata: name: www-example-com namespace: default spec: secretName: cert-manager-tls issuerRef: name: letsencrypt-prod commonName: www.example.com dnsNames: - www.example.com acme: config: - dns01: provider: prod-dns domains: - www.example.com EOF certificate "www-example-com" created
同じく以下のコマンドで状況確認。
$ kubectl describe certificate Name: www-example-com Namespace: default Labels: <none> (中略) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CreateOrder 8s cert-manager Created new ACME order, attempting validation...
なるほど。しばらく待ってみる。
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CreateOrder 2m cert-manager Created new ACME order, attempting validation... Normal DomainVerified 1m cert-manager Domain "www.example.com" verified with "dns-01" validation Normal IssueCert 1m cert-manager Issuing certificate... Normal CertObtained 1m cert-manager Obtained certificate from ACME server Normal CertIssued 1m cert-manager Certificate issued successfully
Certificate issued successfully
!! 🎉
ちょっと長かったけど、これで証明書の作成までが完了。
つづいて、クラスタ上で動かすためのWebアプリケーションをつくる。
"小さなWebアプリケーション" をつくる
Go でしゅっと作った。こんな↓かんじで。
package main import ( "log" "net/http" "github.com/a-know/small-app/handlers" "github.com/go-chi/chi" ) func main() { r := chi.NewRouter() r.Get("/heartbeat", handlers.HandleHeartbeat) log.Printf("small-app started.") http.ListenAndServe(":8080", r) }
package handlers import ( "fmt" "net/http" ) func HandleHeartbeat(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "OK") }
8080
ポートに /heartbeat
に GET でアクセスしたら OK
と返すだけのアプリケーション。さいきん go-chi/chi
を使ってみているので、ここでもそれを使っている。
作ったアプリケーションをコンテナに固める
久々にDockerfileを書いた。こんな感じでいいのかな......。
# for build FROM golang:1.10-alpine AS build RUN apk update && apk upgrade \ && apk add curl git RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh WORKDIR /go/src/github.com/a-know/small-app COPY . . RUN dep ensure RUN go build -o small-app # for artifacts FROM golang:1.10-alpine COPY --from=build /go/src/github.com/a-know/small-app/small-app /bin/small-app EXPOSE 8080 RUN chmod +x /bin/small-app CMD /bin/small-app
コンテナをビルドして Google Container Registry に push する
GKEクラスタにコンテナをデプロイすることを考えると、GCR に push してあるほうが何かと楽なので......。
$ docker build -t asia.gcr.io/<PROJECT_ID>/small-app:latest . $ gcloud docker -- push asia.gcr.io/<PROJECT_ID>/small-app:latest
ビルドしたコンテナをクラスタ上の配備
以下のような yaml ファイルを作っておいて......。
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: small-app labels: run: small-app spec: replicas: 10 template: metadata: labels: run: small-app spec: containers: - name: small-app image: asia.gcr.io/<PROJECT_ID>/small-app:latest readinessProbe: httpGet: path: /heartbeat port: 8080 initialDelaySeconds: 3 periodSeconds: 3 ports: - containerPort: 8080
今回作ったアプリケーションはあまりに小さすぎて、200 なレスポンスを返せるエンドポイントが /heartbeat
ないので、 readinessProbe
でそこを指定しておく。
以下のコマンドでデプロイ。
$ kubectl create -f deployment.yml deployment "small-app" created
クラスタ上のアプリケーションに http でのアクセスができるようにする
次は以下のコマンドで NordPort の作成。
$ kubectl expose deployment small-app --target-port=8080 --type=NodePort service "small-app" exposed
続いて静的IPアドレスを取得。
$ gcloud compute addresses create small-app-ip --global $ gcloud compute addresses describe small-app-ip --global --format='get(address)' xx.xxx.x.xx
上記コマンドで表示されるアドレスを確認して、設定対象のドメインにAレコードを設定する。この作業、今回僕の場合は Route53 での作業になるので、それもここでは割愛。
最後に Ingress を作成する。多分これをやった時点で「格安GKEクラスタ」ではなくなると思うので、その点は注意w(多分¥2,000/月 くらいプラスで掛かることになる。それでも僕は運用しつづけるけどね!w)
kubectl apply -f - << EOF apiVersion: extensions/v1beta1 kind: Ingress metadata: name: small-app annotations: kubernetes.io/ingress.global-static-ip-name: "small-app-ip" kubernetes.io/ingress.class: "gce" spec: tls: - secretName: cert-manager-tls hosts: - www.example.com backend: serviceName: small-app servicePort: 8080 EOF
Ingress の状況を以下のコマンドで確認。--watch
オプションを指定することで、一定間隔で結果をポーリングしてくれるっぽい。
$ kubectl get ingress --watch NAME HOSTS ADDRESS PORTS AGE small-app * 80, 443 58s small-app * xx.xxx.x.xx 80, 443 1m
ADDRESS にさっき取得した静的IPアドレスが出た!
さらに、Ingress の詳細な状態を確認する。
$ kubectl describe ingress Name: small-app Namespace: default Address: xx.xxx.x.xx (中略) Annotations: forwarding-rule: k8s-fw-default-small-app--b0bac8830034cfc9 https-forwarding-rule: k8s-fws-default-small-app--b0bac8830034cfc9 ssl-cert: k8s-ssl-default-small-app--b0bac8830034cfc9 url-map: k8s-um-default-small-app--b0bac8830034cfc9 backends: {"k8s-be-31982--b0bac8830034cfc9":"Unknown"} https-target-proxy: k8s-tps-default-small-app--b0bac8830034cfc9 target-proxy: k8s-tp-default-small-app--b0bac8830034cfc9 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ADD 3m loadbalancer-controller default/small-app Normal CREATE 2m loadbalancer-controller ip: xx.xxx.x.xx Normal Service 2m (x2 over 2m) loadbalancer-controller default backend set to small-app:31982
Annotations の backends のところが Unknown
になっている。どうやらこのタイミングで、Deployment の際に指定した 各コンテナのreadinessProbe に対してヘルスチェックのようなことをおこなってるっぽい。(Deployment に readinessProbe の指定がない場合、 /
へのアクセスが 200 になることが期待される。https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/cluster-loadbalancing/glbc#limitations)
しばらく待って、もう一度コマンドを実行してみる。
$ kubectl describe ingress Name: small-app Namespace: default Address: xx.xxx.x.xx (中略) Annotations: backends: {"k8s-be-31982--b0bac8830034cfc9":"HEALTHY"} forwarding-rule: k8s-fw-default-small-app--b0bac8830034cfc9 https-forwarding-rule: k8s-fws-default-small-app--b0bac8830034cfc9 ssl-cert: k8s-ssl-default-small-app--b0bac8830034cfc9 target-proxy: k8s-tp-default-small-app--b0bac8830034cfc9 url-map: k8s-um-default-small-app--b0bac8830034cfc9 https-target-proxy: k8s-tps-default-small-app--b0bac8830034cfc9 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ADD 10m loadbalancer-controller default/small-app Normal CREATE 9m loadbalancer-controller ip: xx.xxx.x.xx Normal Service 3m (x4 over 9m) loadbalancer-controller default backend set to small-app:31982
HEALTHY
になった!喜び勇んで、https://www.example.com/heartbeat
にアクセスしてみる。
めでたい 🎉🎉🎉 これで今回の目的は達成!
まとめ
- GKE 上でWebアプリケーションを動かすための第一歩として、小さなWebアプリケーションを作ってコンテナとしてビルド、デプロイをしてみた
- GKEで動くk8sクラスタに証明書をセットアップするために、cert-manager を活用した
- cert-manager をインストールするためには Helm を使用する
- DNS-01 チャレンジ方式を採る場合は、cert-manager がDNSに対するアクセスをおこなえる権限を与える必要がある
- 証明書は Ingress に対してセットアップ可能
- デプロイしたコンテナが
/
へのアクセスに対して 200 を返せない場合は、別途readinessProbe
を指定しておく必要がある点は注意
今回作成・デプロイしたのは本当に小さいアプリケーションだけども、とはいえそれでも立派なWebアプリケーション。今度何かしらWebアプリを作ったときには、ぜひGKE上で動かしてみたい!