えいのうにっき

a-knowの日記です

GKEクラスタに小さなWebアプリケーションを配備して、ついでにhttps化してみる

前回のつづき。前回は、preemptible instance を使ってできるだけ安上がりなGKE(k8s)クラスタを作ってみた。

blog.a-know.me

せっかく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 にまつわるものとして、charttiller というものがある。

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 にアクセスしてみる。

f:id:a-know:20180624160122p:plain

めでたい 🎉🎉🎉 これで今回の目的は達成!

まとめ

  • GKE 上でWebアプリケーションを動かすための第一歩として、小さなWebアプリケーションを作ってコンテナとしてビルド、デプロイをしてみた
  • GKEで動くk8sクラスタに証明書をセットアップするために、cert-manager を活用した
    • cert-manager をインストールするためには Helm を使用する
    • DNS-01 チャレンジ方式を採る場合は、cert-manager がDNSに対するアクセスをおこなえる権限を与える必要がある
    • 証明書は Ingress に対してセットアップ可能
  • デプロイしたコンテナが / へのアクセスに対して 200 を返せない場合は、別途 readinessProbe を指定しておく必要がある点は注意

今回作成・デプロイしたのは本当に小さいアプリケーションだけども、とはいえそれでも立派なWebアプリケーション。今度何かしらWebアプリを作ったときには、ぜひGKE上で動かしてみたい!

Dockerによるアプリケーション開発環境構築ガイド

Dockerによるアプリケーション開発環境構築ガイド

めちゃくちゃ参考にした