パパエンジニアのポエム

奥さんと娘ちゃんへの愛が止まらない

GKE で Let's Encrypt (ワイルドカード編)

まえがき

cert-manager は使わないです。
理由は、kube-lego で時間を無駄にしてしまったから。
これから kubernetes がどう変わっていくかわからないのと、
シンプルでプリミティブなシステムが好きなので、公式の方法で行いました。
なので、 3ヶ月に1回手動対応 です。

certbot インストール

brew install certbot

certbot 実行

sudo certbot certonly --manual \
  --preferred-challenges dns-01 \
  --server https://acme-v02.api.letsencrypt.org/directory \
  -m hoge@fuga.com \
  -d sample.com \
  -d *.sample.com

こんな感じで、メインドメインワイルドカードサブドメインを引数に渡し実行。

DNSにTXTレコード追加

-------------------------------------------------------------------------------
Please deploy a DNS TXT record under the name
_acme-challenge.sample.com with the following value:

lgfkjaldfjlasdkgjjakdflasjflaslasdjf

Before continuing, verify the record is deployed.
-------------------------------------------------------------------------------

↑の値をTXTレコードに追加します。

DNS名:_acme-challenge.sample.com 値:lgfkjaldfjlasdkgjjakdflasjflaslasdjf

追加したら、Enter を押す。 登録前に押すとこけます。
あと、DNSにメインドメイン以外のAレコードがあるとこけました(ちょっとハマった)

tls用の Secret 作成

↑で作成された証明書を元にSecret作成

sudo kubectl create secret tls tls-certs \
  --key /etc/letsencrypt/live/sample.com/privkey.pem \
  --cert /etc/letsencrypt/live/sample.com/fullchain.pem

IngressでSecretを参照

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ing-dev
spec:
  tls:
  - secretName: tls-certs
  backend:
    serviceName: svc-dev
    servicePort: 80

↑みたいに tls-certs を追加する。
Ingress デプロイ後、2,3分間 経過すると反映されてます。
サブドメインDNSに登録しワイルドカードが聞いているか確認してみましょう。

まとめ

公式のやり方でサクッと対応できました。
3ヶ月に1回の更新もシェルスクリプトで対応できそうです。
cert-manager に対応して、破壊的変更リスクとバグリスクを抱えるより、
3ヶ月に1回の更新コストをとったという感じです。

Container-Optimized OS で Redisをホストする

GCE(Container-Optimized OS)作成

インスタンスの作成と設定  |  Container-Optimized OS  |  Google Cloud

コンソールのブートディスクからContainer-Optimized OSを選択する
後は、任意でポチポチやるだけ

f:id:yuki-toida:20180319160931p:plain

Redis コンテナ起動

コンテナ名を redis-master
コンテナイメージを redis:4-alpine
ポートを 6379
永続化(AOF)フォルダを /home/redis/data
とすると、下記コマンドでRedisコンテナ起動

docker run --name redis-master -p 6379:6379 -v /home/redis/data:/data -d redis:4-alpine redis-server --appendonly yes

GCE の startup script 設定

再起動時に redis-master コンテナを起動するようにする
インスタンスredis-master-dev とすると、下記コマンドで設定完了
sh gcloud compute instances add-metadata redis-master-dev --metadata startup-script='#! /bin/bash docker start redis-master'

コンテナ環境前提でGCEを使う場合 Container-Optimized OS 入れとけばおkです
コンテナ最高すぎる

GKE LBで、Ingressを使ってTLS/SSL対応する

追記

kube-legoがオワコンになったのでこの記事もオワコンになりました
GitHub - jetstack/kube-lego: Automatically request certificates for Kubernetes Ingress resources from Let's Encrypt

kube-legoのセットアップ

Kubernetes Ingress リソースを使って GCP上にHTTPSロードバランサを構築する

kube-legoを使う
github.com

コンテナクラスタは作成されている前提で進める
yuki-toida.hatenablog.com

kube-lego をクローンする

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

lego 作業フォルダに移動

cd kube-lego/examples/gce/lego/

configmap.yamlを編集して、lego.email を適切に設定する

kube-legoのリソースを作成する

kube-lego/examples/gce/lego/配下にあるリソースを作成する

kubectl create -f ./

namespace "kube-lego" created
configmap "kube-lego" created
deployment "kube-lego" created

kube-legonamespace に作成されているか確認する

kubectl get deployment,replicaset,pod,configmap --namespace kube-lego

Ingressリソースを作成する

DeploymentやServiceは作成されているものとする

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-dev
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "ingress-dev-ip"
    kubernetes.io/ingress.class: "gce"
    kubernetes.io/tls-acme: "true"
spec:
  tls:
  - hosts:
    - sample.com
    - sub.sample.com
    secretName: ingress-dev-tls
  rules:
  - host: sample.com
    http:
      paths:
      - path:
        backend:
          serviceName: service-dev
          servicePort: 80
  - host: sub.sample.com
    http:
      paths:
      - path:
        backend:
          serviceName: service-dev
          servicePort: 80

ここで注意することは、
kubernetes.io/ingress.allow-http: "false"アノテーションを入れないこと、HTTPで認証を行っているので失敗する

作成後、ロードバランサーの疎通と認証が通るまで15分くらい時間かかる
kube-legoのログをみながら認証が通ったか確認する

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

認証が通ったらhttps://ドメインで疎通確認

GCEインスタンスの自動起動停止をスケジューリングする

適切なサービスアカウントを作成する

インスタンスのサービス アカウントの作成と有効化  |  Compute Engine ドキュメント  |  Google Cloud
GCEインスタンスをマネジメントするだけならCompute Adminのみでおk
今回はCloudSQLも管理したいので、Cloud SQL 管理者も追加する
Terminal Clientサービスアカウントを作成する

ターミナルサーバ(GCE)作成

cronを動かすだけなので、f1-micro(vCPU x 1、メモリ 0.6 GB)でおk
サービスアカウントに先程作ったTerminal Clientを設定する
作ったインスタンスにログインしZONEを設定する

gcloud compute ssh terminal-dev
sudo gcloud config set compute/zone asia-northeast1-a

最後に gcloud の config を確認する

gcloud config list

gcloudでGCEインスタンスを起動停止するスクリプト

redis-dev インスタンスを起動するスクリプト
terminal-cron-start.shとして保存

gcloud compute instances start redis-dev

redis-dev インスタンスを停止するスクリプト
terminal-cron-stop.shとして保存

gcloud compute instances stop redis-dev

cron で定時にスクリプトが実行されるようにする

crontab を設定する

sudo crontab -e

editorに何を使うか選ぶ
3の vim を選んだ(nano使えないので)
下記2行を追加、UTCタイムであることに注意
10:00(JST)に起動し、19:00(JST)に停止する

0 1 * * 1-5 sh /home/terminal-cron-start.sh
0 10 * * 1-5 sh /home/terminal-cron-stop.sh

Cloud SQL の起動停止も追加する

terminal-cron-start.shterminal-cron-stop.shにそれぞれ以下を追加する
Cloud SQL起動スクリプト

gcloud sql instances patch db-$ENV --activation-policy ALWAYS

Cloud SQL停止スクリプト

gcloud sql instances patch db-$ENV --activation-policy NEVER

これでGCEインスタンスとCloud SQL自動起動停止スクリプトをスケジューリングできる

GCEのDocker-Redisサーバに起動時実行スクリプトを設定する

yuki-toida.hatenablog.com
前回の記事でredisコンテナは作成されている前提ですすめる
今回はインスタンス起動時に停止しているredisコンテナを起動する

GCEにstartup-scriptを設定する

新しいインスタンスに設定する場合はcreate

gcloud compute instances create redis-dev --metadata startup-script='#! /bin/bash
docker start redis'

既存のインスタンスに設定する場合はadd-metadata

gcloud compute instances add-metadata redis-dev --metadata startup-script='#! /bin/bash
docker start redis'

これだけでインスタンス起動時にredisコンテナが起動する

インスタンスSSH接続し、redisにアクセスできるか確認する

gcloud compute ssh redis-dev

redis-cli ping
pong

GKEにデプロイしHTTPロードバランシングする

Container RegistryにアプリケーションDockerイメージをプッシュする

https://cloud.google.com/container-registry/docs/pushing-and-pulling?hl=jacloud.google.com

自前でアプリケーションをビルドしたDockerfileを作成する
ビルドしContainer Registryにプッシュする

GCP_PROJECT_ID="sample-dev"
GCP_IMAGE=asia.gcr.io/$GCP_PROJECT_ID/sample:0.0.1

# build
docker build -f dockerfiles/Dockerfile -t $GCP_IMAGE .

# push
gcloud docker -- push $GCP_IMAGE

これで準備完了
kubectlを使いGKEにデプロイする

deployment.yamlを記載する

以下、レプリカ数2で作ったdeployment.yaml
詳細は省くがポイントはDBの環境変数に登録したcredentialsを使用していること

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: deployment-dev
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
      - name: sample
        image: asia.gcr.io/sample-dev/sample:0.0.1
        ports:
        - containerPort: 4000
        args: ["foreground"]
        env:
          - name: DB_HOST
            value: "127.0.0.1"
          - name: DB_PORT
            value: "3306"
          - name: DB_USER
            valueFrom:
              secretKeyRef:
                name: cloudsql-db-credentials
                key: username
          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: cloudsql-db-credentials
                key: password
      - name: cloudsql-proxy
        image: gcr.io/cloudsql-docker/gce-proxy:1.09
        command: ["/cloud_sql_proxy", "--dir=/cloudsql",
                  "-instances=sample-dev:asia-northeast1:db-dev=tcp:3306",
                  "-credential_file=/secrets/cloudsql/credentials.json"]
        volumeMounts:
          - name: cloudsql-instance-credentials
            mountPath: /secrets/cloudsql
            readOnly: true
          - name: ssl-certs
            mountPath: /etc/ssl/certs
          - name: cloudsql
            mountPath: /cloudsql
      volumes:
        - name: cloudsql-instance-credentials
          secret:
            secretName: cloudsql-instance-credentials
        - name: ssl-certs
          hostPath:
            path: /etc/ssl/certs
        - name: cloudsql
          emptyDir:

デプロイする

kubectl create -f deployment.yaml

実際にrsとpodが作成されているか確かめる

kubectl get deployment,rs,pod
NAME                    DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/deployment-dev   2         2         2            2           20m

NAME                          DESIRED   CURRENT   READY     AGE
rs/deployment-dev-763713461   2         2         2         20m

NAME                                READY     STATUS    RESTARTS   AGE
po/deployment-dev-763713461-69r7v   2/2       Running   0          20m
po/deployment-dev-763713461-7jrmx   2/2       Running   0          20m

こんな感じでPodがRunningで出力されてればおk

service.yamlを記載する

ポイントはセレクタにdeployment.yamlで記載したappラベルを記載すること
ポート4000を80にポートフォワードする

apiVersion: v1
kind: Service
metadata:
  name: service-dev
spec:
  type: NodePort
  selector:
    app: sample
  ports:
  - protocol: TCP
    port: 80
    targetPort: 4000

Service作成

kubectl create -f service.yaml

ingress.yamlを記載する

Ingress での HTTP 負荷分散の設定  |  Kubernetes Engine のチュートリアル  |  Google Cloud
作成したservice-devに対してIngressを作成する

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-dev
spec:
  backend:
    serviceName: service-dev
    servicePort: 80

Ingress作成

kubectl create -f ingress.yaml

しばらくするとヘルスチェックが完了し動作する
実際に確認する

kubectl describe ing
Name:            ingress-dev
Namespace:      default
Address:        ************
Default backend:    service-dev:80 (************,************)
Rules:
  Host  Path    Backends
  ----  ----    --------
  * *   service-dev:80 (************,************)
Annotations:
  backends:     {"k8s-be-30189--16e23e70a11044da":"HEALTHY"}
  forwarding-rule:  k8s-fw-default-ingress-dev--16e23e70a11044da
  target-proxy:     k8s-tp-default-ingress-dev--16e23e70a11044da
  url-map:      k8s-um-default-ingress-dev--16e23e70a11044da
Events:
  FirstSeen LastSeen    Count   From            SubObjectPath   Type        Reason  Message
  --------- --------    -----   ----            -------------   --------    ------  -------
  8m        8m      1   loadbalancer-controller         Normal      ADD default/ingress-dev
  7m        7m      1   loadbalancer-controller         Normal      CREATE  ip: ************
  7m        2m      5   loadbalancer-controller         Normal      Service default backend set to service-dev:30189

backendsがHEALTHYになっていたらおk
これは / に GETリクエストを投げて200を確認している

外部IPにアクセスする

Address:     ************

ここにアクセスしてアプリケーションの起動と名前解決を確認する

GKEコンテナからCloudSQLにプロキシ接続する

シークレットを作成する

Google Kubernetes Engineから接続する  |  Cloud SQL for MySQL  |  Google Cloud

シークレットを作成

kubectl create secret generic cloudsql-instance-credentials \
                       --from-file=credentials.json=[PROXY_KEY_FILE_PATH]

ユーザーとパスワードを登録

kubectl create secret generic cloudsql-db-credentials --from-literal=username=[USER] --from-literal=password=[PASSWORD]

deployment.yamlを作成しデプロイする

deployment.yamlファイルを記述する
DB_USER, DB_PASSWORD 環境変数を設定

- name: DB_USER
  valueFrom:
    secretKeyRef:
      name: cloudsql-db-credentials
      key: username
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: cloudsql-db-credentials
      key: password

CloudSQLインスタンス接続コマンド記載

- image: gcr.io/cloudsql-docker/gce-proxy:1.09
  name: cloudsql-proxy
  command: ["/cloud_sql_proxy", "--dir=/cloudsql",
            "-instances=[INSTANCE_CONNECTION_NAME]=tcp:3306",
            "-credential_file=/secrets/cloudsql/credentials.json"]
  volumeMounts:
    - name: cloudsql-instance-credentials
      mountPath: /secrets/cloudsql
      readOnly: true
    - name: ssl-certs
      mountPath: /etc/ssl/certs
    - name: cloudsql
      mountPath: /cloudsql
volumes:
  - name: cloudsql-instance-credentials
    secret:
      secretName: cloudsql-instance-credentials
  - name: ssl-certs
    hostPath:
      path: /etc/ssl/certs
  - name: cloudsql
    emptyDir:

アプリケーション側でDB_USER, DB_PASSWORDを読み込む用実装する
次回Elixirアプリケーションも記載したdeployment.yamlを作成する