Istioで使うTLS証明書をcert-managerで作成する

Istioは0.7まではIstio IngressというIngress Controllerの一種を使用してトラフィックを受け入れていましたが、0.8以降はGatewayを使うようになりました。
Istio Ingressでは他のIngressと同様のやり方でTLS証明書を扱えたのでcert-managerと組み合わせるのも簡単でしたが、Gatewayと組み合わせる場合は少々複雑になってしまいました。
今回はGatewayでcert-managerが管理する証明書を使用する方法を紹介します。

また、cert-managerの使い方は既に紹介しているので、そちらを参照してください。
この記事の内容を把握していることを前提として進めます。

akaimo.hatenablog.jp

環境

  • GKE 1.9.7-gke.1
  • Istio 0.8
  • cert-manager v0.3.1 (DNS-01)
  • route53

GatewayTLS証明書を使う方法

TLS証明書を使う方法は厳密に定められており、これから外れてしまうと動きません。

  • istio-systemというnamespaceに証明書を入れたsecretを作成する
  • 証明書を入れたsecretの名前はistio-ingressgateway-certsでなければならない
  • Istioが証明書が入ったsecretを読み込み、/etc/istio/ingressgateway-certsに展開する

という仕様になっています。
詳細は公式ドキュメントに書かれています。

この仕様をクリアできるようにcert-managerの設定をしていきます。

cert-managerの設定

Istioの仕様をクリアするために、cert-managerに対して証明書が入ったsecretをistio-systemに作るように設定します。

cert-managerはIssuerとCertificateのあるnamespaceにsecretを作成するため、IssuerとCertificateのnamespaceをistio-systemにします。

metadata:
  namespace: istio-system

あとはCertificateにある作成するsecretの名前を決めるフィールドで、istio-ingressgateway-certsという名前を指定するだけです。

spec:
  secretName: istio-ingressgateway-certs

これでIstioから読み込む準備ができました。

IstioのGatewayからはドキュメントにある通りに記述すればhttpsで通信ができるようになります。

spec:
    tls:
      mode: SIMPLE
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
      privateKey: /etc/istio/ingressgateway-certs/tls.key

route53で管理するドメインの証明書をcert-managerで管理しGKEで使用する

GCPでKubernetesを使用する場合、AWSと違いTLS証明書を発行してくれるサービスが存在しません。
有料の証明書を購入すれば良い話ではありますが、機能は同じなので無料でいきたいところです。
そこで、今回はLet's Encryptを利用したいと思います。

しかし、Let's Encryptは90日で証明書の期限が切れてしまい管理が大変なので、Kubernetesのアドオンであるcert-managerを使用したいと思います。

github.com

環境

  • GKE 1.9.7-gke.1
  • cert-manager v0.3.1
  • route53

Let's Encryptの認証方法

cert-managerではLet's EncryptのACMEプロトコルであるHTTP-01とDNS-01に対応しています。
ACMEプロトコルについての解説は他に譲りますが、HTTP-01だとhttpアクセスを受け入れるため、ingressとの関わりが生まれてしまいます。
できるだけシンプルに管理したいので、今回はDNS-01を使用します。

また、DNS-01だとDNSのTXT レコードを操作するため、DNSへのアクセス権を付与する必要があります。
DNSへアクセスできない場合はHTTP-01で行うしかありません。

HTTP-01のやり方はこの記事(英語)がわかりやすいです。

github.com

cert-managerのインストール

cert-managerはhelmでインストールします。

$ helm install --name cert-manager --namespace kube-system stable/cert-manager

その他のパラメーターなどの詳細はこちらで確認できます。

github.com

route53へアクセスするための設定

IAMの作成

DNS-01で認証するのでroute53に書き込みができるIAMを作成します。

公式ドキュメントにIAMのポリシーが書かれていますが、古くなっているのか執筆時(2018/06/28)では動きませんでした。
ですが、すでにこの事がissueで議論されており、動作するポリシーが書かれているのでそれを利用します。

{
    "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": "*"
        }
    ]
}

このポリシーを適用したIAMユーザーの認証情報をダウンロードしておきます。

cert-managerが読み込むsecretの作成

cert-managerはAWSAccess Keyはyamlに生で書き込み、Secret KeyはKubernetesのsecretを経由して読み込みます。
そのため、まずはSecret Keyをsecretとして定義します。

$ echo -n 'youreSecretKey' | base64
apiVersion: v1
kind: Secret
metadata:
  name: prod-route53-credentials-secret
type: Opaque
data:
  secret-access-key: hogePiyoFaaaa==

base64エンコードしたsecretKeyをsecret-access-keyに入れます。

Issuerの作成

Issuerという名前の通り、Let's Encryptに発行を依頼するのに必要な情報を記述します。
ドメインの情報は、次に作成するCertificateで定義するのでそれ以外の情報をここで定義します。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: hoge@hoge.com
    privateKeySecretRef:
      name: letsencrypt-prod
    dns01:
      providers:
        - name: prod-dns
          route53:
            accessKeyID: YOUREROUTE53ACCESSKEYID
            secretAccessKeySecretRef:
              name: prod-route53-credentials-secret
              key: secret-access-key

emailとaccessKeyIDを正しいものに書き換えます。

Certificateの作成

作成する証明書のドメイン情報を定義します。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: sample-com
spec:
  secretName: hoge-tls
  issuerRef:
    name: letsencrypt-prod
  commonName: 'hoge.com'
  dnsNames:
    - hoge.com
  acme:
    config:
      - dns01:
          provider: prod-dns
        domains:
          - '*.hoge.com'
          - hoge.com

ドメインを正しいものに書き換えてください。
Certificateが存在するnamespaceにsecretNameで指定した名前でTLS証明書がsecretに加工された状態で作成されます。

証明書の作成

ここまでに作成したIssuerとCertificateをKubernetesに適用します。
そうするとcert-managerでの管理が始まり、証明書が作成されます。

作成状況はdescribeコマンドで確認できます。

$ kubectl describe certificate

...
Type     Reason                Message
----     ------                -------
...
Normal   CeritifcateIssued     Certificated issued successfully

このような出力になれば証明書の作成に成功しています。

この証明書はcert-managerによって管理され、失効まで30日を切ると更新されます。

Kubernetesのdashboardをingressから表示する

Kubernetesの状態が知りたいなと思い調べたところ、公式のWebUIであるdashboardをみつけました。
README通りにやれば簡単にインストールできますが、proxy経由でしか表示させる方法が書いてなく不便だったので、ingressを使用して表示させる方法を書きます。

証明書が入ったsecretの作成

dashboardは証明書がないとアクセスできない設定になっているので、まずは証明書を作成します。

dashboardのアクセスにドメインを使用する場合はLet's Encryptで簡単に証明書が取得できるので、取得しましょう。
Let's Encryptの使い方は他のサイトに譲ります。

ドメインがない場合は自己証明書を用意します。
このやり方はdashboardwikiに書いてあるので、それ通りにやればOKです。

Certificate management · kubernetes/dashboard Wiki · GitHub

証明書が準備できたらsecretの作成に入ります。

$ kubectl create secret tls kubernetes-dashboard-certs --key dashboard.key--cert dashboard.crt -n kube-system

dashboardはkube-systemというnamespaceに作成されるので、このnamespaceにsecretを作成する必要があります。

dashboardのインストール

dashboardのインストールは公式にある通りにapplyするだけです。

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml

ingressの設定

基本的には通常と同じようにingressを設定すれば問題ありませんが、1つだけ注意しなければいけないことがあります。

ingress-nginxの場合は、namespaceごとにingressを作成する必要があります。
そのため、今回はkube-systemというnamespaceにingressを作成します。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/secure-backends: "true"
  name: dashboard-ingress
  namespace: kube-system
spec:
  rules:
    - host: "sample.dash.jp"
      http:
        paths:
          - path: /
            backend:
              serviceName: kubernetes-dashboard
              servicePort: 443
  tls:
    - hosts:
      - "sample.dash.jp"
      secretName: kubernetes-dashboard-certs

これで指定したドメインにアクセスするとdashboardのUIが表示されます。

GKEにPrometheusを導入する

GKEにはPrometheusを導入するドキュメントが無いんですよね・・・
IstioやSpinnakerのドキュメントはあるのに、もっと需要がありそうなPrometheusのドキュメントが無いのはつらいです。

そんなわけで、自分が導入したときの手順を記録しておきます。

環境

GKE 1.9.7-gke.0

手順

結構やることが多いので簡単に内容をまとめます。

  1. Namespaceを作成
  2. ClusterRoleを作成
  3. 永続ディスクの作成
  4. 設定ファイルを登録
  5. Prometheusを起動

1. Namespaceを作成

Prometheusやその後に導入することになるGrafanaをメインのPodと同じNamespaceに入れてしまうと、稼働しているサービスのPodと監視用のPodが混ざってしまいあまり嬉しくないので、監視に使うPodは別のNamespaceに入れることにします。

今回はmonitoringというNamespaceを作成します。

$ kubectl create namespace monitoring

2. ClusterRoleを作成

PrometheusがKubernetes APIからメトリクスを取得するために先程作成したNamespaceにアクセス権を付与します。

ClusterRoleで必要な権限を指定して、ClusterRoleBindingで権限を与えたいNamespaceを指定する形になっています。

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: prometheus
rules:
- apiGroups: [""]
  resources:
  - nodes
  - nodes/proxy
  - services
  - endpoints
  - pods
  verbs: ["get", "list", "watch"]
- apiGroups:
  - extensions
  resources:
  - ingresses
  verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: prometheus
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus
subjects:
- kind: ServiceAccount
  name: default
  namespace: monitoring

3. 永続ディスクの作成

永続ディスクについては別の記事で解説しているので、それを見てください。

今回使ったyamlはこれです。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: prometheus-pv
  namespace: monitoring
  labels:
    app: prometheus-pv
spec:
  capacity:
    storage: 200Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  gcePersistentDisk:
    pdName: prometheus-disc
    fsType: ext4
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: prometheus-pvc
  namespace: monitoring
  labels:
    app: prometheus-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ""
  resources:
    requests:
      storage: 200Gi
  selector:
    matchLabels:
      app: prometheus-pv

4. 設定ファイルを登録

Prometheusの設定ファイルをConfig Mapを経由して登録します。

このファイルにはKubernetesで実行されているPodやServiceを動的に見つけるための設定が書かれています。
この設定によって、可変するPodなどを余すことなく監視することができます。

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-server-conf
  labels:
    name: prometheus-server-conf
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval:     15s
      evaluation_interval: 15s
    scrape_configs:
    - job_name: 'kubernetes-apiservers'
      kubernetes_sd_configs:
      - role: endpoints
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      relabel_configs:
      - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
        action: keep
        regex: default;kubernetes;https

    - job_name: 'kubernetes-nodes'
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      kubernetes_sd_configs:
      - role: node
      relabel_configs:
      - action: labelmap
        regex: __meta_kubernetes_node_label_(.+)
      - target_label: __address__
        replacement: kubernetes.default.svc:443
      - source_labels: [__meta_kubernetes_node_name]
        regex: (.+)
        target_label: __metrics_path__
        replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor

    - job_name: 'kubernetes-cadvisor'
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      kubernetes_sd_configs:
      - role: node
      relabel_configs:
      - action: labelmap
        regex: __meta_kubernetes_node_label_(.+)
      - target_label: __address__
        replacement: kubernetes.default.svc:443
      - source_labels: [__meta_kubernetes_node_name]
        regex: (.+)
        target_label: __metrics_path__
        replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor

    - job_name: 'kubernetes-service-endpoints'
      kubernetes_sd_configs:
      - role: endpoints
      relabel_configs:
      - action: labelmap
        regex: __meta_kubernetes_service_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        action: replace
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_service_name]
        action: replace
        target_label: kubernetes_name

    - job_name: 'kubernetes-services'
      metrics_path: /probe
      params:
        module: [http_2xx]
      kubernetes_sd_configs:
      - role: service
      relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - target_label: __address__
        replacement: blackbox-exporter.example.com:9115
      - source_labels: [__param_target]
        target_label: instance
      - action: labelmap
        regex: __meta_kubernetes_service_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_service_name]
        target_label: kubernetes_name

    - job_name: 'kubernetes-ingresses'
      metrics_path: /probe
      params:
        module: [http_2xx]
      kubernetes_sd_configs:
      - role: ingress
      relabel_configs:
      - source_labels: [__meta_kubernetes_ingress_scheme,__address__,__meta_kubernetes_ingress_path]
        regex: (.+);(.+);(.+)
        replacement: ${1}://${2}${3}
        target_label: __param_target
      - target_label: __address__
        replacement: blackbox-exporter.example.com:9115
      - source_labels: [__param_target]
        target_label: instance
      - action: labelmap
        regex: __meta_kubernetes_ingress_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_ingress_name]
        target_label: kubernetes_name

    - job_name: 'kubernetes-pods'
      kubernetes_sd_configs:
      - role: pod
      relabel_configs:
      - action: labelmap
        regex: __meta_kubernetes_pod_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        action: replace
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_pod_name]
        action: replace
        target_label: kubernetes_pod_name

5. Prometheusを起動

いよいよPrometheus本体の導入に入ります。

やることは単純で、これまでに作成した永続ディスクとConfig MapをPrometheusの公式イメージにマウントするだけです。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: prometheus-deployment
  namespace: monitoring
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: prometheus-server
    spec:
      containers:
        - name: prometheus
          image: prom/prometheus:v2.1.0
          args:
            - "--config.file=/etc/prometheus/prometheus.yml"
            - "--storage.tsdb.path=/prometheus/"
          ports:
            - containerPort: 9090
          volumeMounts:
            - name: prometheus-config-volume
              mountPath: /etc/prometheus/
            - name: prometheus-storage-volume
              mountPath: /prometheus/
      volumes:
        - name: prometheus-config-volume
          configMap:
            defaultMode: 420
            name: prometheus-server-conf
        - name: prometheus-storage-volume
          persistentVolumeClaim:
            claimName: prometheus-pvc
      securityContext:
        fsGroup: 2000
        runAsNonRoot: true
        runAsUser: 1000

これでPrometheusが可動します。
せっかくなので動作確認をしてみましょう。

コンテナをローカルマシンにつなげます。

$ kubectl port-forward prometheus-monitoring-**** 8080:9090

http://localhost:8080でPrometheusに接続できます。

ここまででコンテナのメトリクスが取得できるようになりました。しかし、まだKubernetesのNodeのメトリクスが取得できていません。
次回はNodeのメトリクスも取得できるように拡張していきます。

GKEでPersitentVolumeを使う

KubernetesでPersitentVolumeを使う方法はドキュメントに書いてありますが、使う環境によって永続ディスクが異なるためにスムーズにいかないことがあります。
そのため、今回はGKEでPersitentVolumeを使う方法を紹介します。

環境

GKE 1.9.7-gke.0

永続ディスクの用意

まずはGCPで永続ディスクを作成します。

GUICUIどちらでも大丈夫ですが今回はCUIで作成してみます。

$ gcloud compute --project={your-project} disks create sample-disk --zone=asia-northeast1-b --type=pd-ssd --size=100GB

このようになると思います。

f:id:akaimo3:20180523215724p:plain

これで永続ディスクの作成は完了です。

PersitentVolumeの作成

ここからはKubernetesの設定に入ります。

Podが永続ディスクを使用するためにはPersitentVolumeだけではなく、PersistentVolumeClaimというものも作成しなければいけません。

PersitentVolumeには先程作成した永続ディスクについての設定を記述します。
また、PersistentVolumeClaimを作成することで、PodがPersitentVolumeを見つけることができるようになります。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: sample-pv
  labels:
    app: sample-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  gcePersistentDisk:
    pdName: sample-disk
    fsType: ext4
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-pvc
  labels:
    app: sample-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ""
  resources:
    requests:
      storage: 100Gi
  selector:
    matchLabels:
      app: sample-pv

Podにマウントする

最後にPodからアクセスできるようにします。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  ...
  template:
    ...
    spec:
      containers:
        - name: sample
          ...
          volumeMounts:
            - name: sample-volume
              mountPath: /sample
      volumes:
        - name: sample-volume
          persistentVolumeClaim:
            claimName: sample-pvc
      securityContext:
        fsGroup: 2000
        runAsNonRoot: true
        runAsUser: 1000

ポイントはsecurityContextの部分です。
この記述がないと、permissionでエラーになってPodが起動しなくなってしまいます。

以上がGKEでPersitentVolumeを使用する方法です。
PersitentVolumeのような環境に依存する部分は、具体的なコードがあまりないので誰かの参考になれば嬉しいです。

KubernetesでBlue-Green Deploymentしてみる

今回はサービスを本番で運用していくときに欲しくなるBlue-Green DeploymentをKubernetesでやってみます。

TL;DR

  • Serviceのselectorを更新するやり方だと10分程度BlueとGreenがまざる
  • Istioを使用すれば瞬時に100%のトラフィックを切り分けられるのでBlue-Green Deploymentができる

環境

  • GKE 1.9.7-gke.0

2種類のAPI

BlueとGreenを見分けるために、自身の色を返すAPIを用意します。

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "blue-api")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":80", nil)
}

そして、このAPIが稼働するDeploymentがこれです。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: blue-api
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: color-api
        version: blue
    spec:
      containers:
      - name: blue-api
        image: asia.gcr.io/hoge/color-api:blue
        ports:
        - containerPort: 80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: green-api
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: color-api
        version: green
    spec:
      containers:
      - name: green-api
        image: asia.gcr.io/hoge/color-api:green
        ports:
        - containerPort: 80

ServiceでB/Gしてみる

KubernetesはServiceでアクセスするPodを見つけているので、そこの設定を書き換えればB/Gできそうです。

apiVersion: v1
kind: Service
metadata:
  name: service-color-api
spec:
  selector:
    app: color-api
    version: green
#     version: blue
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: http

versionをblueとgreenで変えてみた結果、このようになりました。

f:id:akaimo3:20180522222711p:plain

ちょっとわかりづらいですね。
点線の時間で切り替えを行い、Blue(上)からGreen(下)にトラフィックが切り替わるようにServiceを書き換えました。
Serviceの設定を更新したらすべてのトラフィックがGreenに行ってほしいところですが、10分ほど両方のPodにアクセスが行ってしまっています。

これではAPIのバージョニングができていないと不整合がおきてしまい正しく動かない恐れがあります。

なにか設定が漏れているのかもしれません。
原因を知っていれば教えてほしいです...

余談ですが、切り替えた直後に前の色のPodを削除することで、瞬時に全てのトラフィックを新しい色に流すことができます。

瞬時に100%のトラフィックを切り替えたいので別のアプローチとしてサービスメッシュのIstioを導入してみます。

IstioでB/Gする

まずはIstioの導入をします。
公式ドキュメントのステップ2まででIstioの導入は完了です。

Istioで経路の制御をするので、ServiceはBlueにもGreenにも通信ができるようにしておきます。

apiVersion: v1
kind: Service
metadata:
  name: service-color-api
  labels:
    app: color-api
spec:
  selector:
    app: color-api
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: http

Istioを使用する場合はIstio用のIngressを通さないと経路制御などができないので、少し設定に変更を入れます。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: color-ingress
  annotations:
    kubernetes.io/ingress.class: "istio"
spec:
  backend:
    serviceName: service-color-api
    servicePort: 80

最後に本命の経路制御の設定をいれます。

apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: color-api
spec:
  destination:
    name: service-color-api
  precedence: 1
  route:
  - labels:
      version: green

今回もselectorを書き換えることで制御できます。
定期的なアクセスがある状態で切り替えるとこのようになりました。

f:id:akaimo3:20180523104428p:plain

取得できるメトリクスがCPU利用率しかなかったので、あまりいいグラフにはなっていませんが、切り替えた直後からすべてのトラフィックが新しいほうに流れています。
Prometheusを導入後に今度はアクセス数をグラフ化して追記したいとおもいます。

まとめ

サービスメッシュを入れるとトラフィックの正確な制御ができます。
しかし、制御のためのプロキシも追加されてしまうため、余計なリソースを使うことにもなってしまいます。

デメリットも存在しますが、Istioを入れてしまえばB/Gだけでなくカナリアリリースもできるようになるので、
リリースを正確に行うためにIstioを導入するのもありかもしれません。

Kubernetesに負荷をかけオートスケールを観察する

本番環境でKubernetesを使うためには、Kubernetesがどのように動作するか把握していないと安心して運用することができません。
まずはサービスを運用する上で重要になる、高負荷時の動きを確認してみたいと思います。

KubernetesにはHPAとCluster Autoscalerの二種類がありますが、両方の動きを見ていきます。

詳細な解説は他に譲りますが、
HPAは稼働中のPodを増加させることで高負荷に耐え、
Cluster AutoscalerはPodを増加させることができないときにKubernetesが扱えるリソースを追加するものです。

つまり、負荷が高まるとHPAがおこりPodが追加され、それでも負荷が高くHPAが続きKubernetesが管理しているリソースに空きがなくなったときにCluster AutoscalerによってVMが追加され、そこにPodが追加されます。

環境

  • GKE 1.9.7-gke.0

実行するアプリケーション

できるだけ手軽に実験したいので、goでリクエストを受け付けて、少し負荷がかかる処理をした後にレスポンスを返すアプリケーションをKubernetesで実行してみます。

Kubernetesの構成は
Ingress -> Service -> Deployment
となっています。

実行するgoのAPIのソースはこのようにしました。

package main

import (
    "fmt"
    "net/http"
    "strconv"
)

func handler(w http.ResponseWriter, r *http.Request) {
    n := 0
    for i := 0; i < 1*1000*1000*1000; i++ {
        n += i
    }
    fmt.Fprintf(w, strconv.Itoa(n)+"\n")
    fmt.Fprintf(w, "my-api")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":80", nil)
}

とても単純なものです。
これをDocker Imageにし、DockerのrunコマンドでAPIサーバーが起動するようにDockerfileを書きます。

FROM golang:1.9.2-alpine3.6

ADD ./server.go ./
RUN go build -o server
CMD ["/go/server"]

Imageをアップロードしますが、せっかくGKEを使用するので同じGCP上にあるGoogle Container Registryにアップロードします。

$ docker build -t asia.gcr.io/{your-project-id}/my-api:v1 .
$ gcloud docker -- push asia.gcr.io/{your-project-id}/my-api:v1

Kubernetesの設定ファイル

Kubernetesについては省略します。
今回は以下の設定ファイルを使用します。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "test-ip"
spec:
  backend:
    serviceName: service-my-api
    servicePort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: service-my-api
spec:
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: http
  selector:
    app: my-api
    version: v1
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-api
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: my-api
        version: v1
    spec:
      containers:
      - name: myapi
        image: gcr.io/{your-project-id}/myapi:v1
        resources:
          requests:
            cpu: "500m"
          limits:
            cpu: "1500m"
        ports:
        - containerPort: 80
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: my-api
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-api
  minReplicas: 2
  maxReplicas: 6
  targetCPUUtilizationPercentage: 70

オートスケールに関係のあるところだけ解説します。
今回負荷をかけるAPIがあるPodに対してはCPUを1.5個まで使用を許可しています。
このPodが初期値として2台稼働しており、閾値を70としています。

つまり、CPUの使用率が70%を超えたときにPodの追加が行われることになります。
また、公式ドキュメントのautoscaling-algorithmによると、10%の許容値があったり瞬間的に超えただけだとオートスケールしなかったりするようです。

負荷をかける

負荷をかけるツールはいろいろありますが、手軽に使えるheyを使用します。

github.com

このツールで負荷をかけてみた結果、このようになりました。

PodのCPU使用率 f:id:akaimo3:20180515231231p:plain

ノードのCPU使用率 f:id:akaimo3:20180515231244p:plain

ピーク時はPodは6個に、ノードは3台にまで増えていますね。
HPAは追加にかかる時間が数秒なので、EC2とかのオートスケールと比べるとだいぶ早いですね。
これは便利そうです。