2018年の振り返り

2018年のことをエンジニアリングを中心に振り返える。

Work

仕事では大きな変化があった。社内転職をすることができ、アプリなどのフロントを中心としてたまにサーバーサイドの開発もするポジションから、基盤担当へと変わった。

アプリ開発の勢いが全盛期と比べると落ち込んだことや、将来のエンジニアとしてのスキルセットなどを考えると、より技術的に深いレイヤーの開発ができるこのポジションに付けたのは非常に大きい。

フロントなどの時代の移り変わりが速いレイヤーで、フレームワークをただ仕事で使うような状態では、やる気と時間がたっぷりある若者にアッサリと追いつかれてしまうだろう。もちろん上の方のレイヤーであっても、フレームワークやライブラリなどを開発するぐらいの技術力があれば追いつかれることはない。しかし、このレイヤーのエンジニアの大半はそうではない。経験と技術力の双方が必要になる場所で戦っていれば、勢いのある若者でも簡単には追いつけないはずだ。それは個人としての価値につながる。

仕事で取り組んだこととして主要なものはAWSからGCPへのクラウド移行だ。
具体的には、

  • Elastic BeanstalkからGKE+Istioへの移行
  • RDSからCloud SQLへの移行

これらからはネットワークや自動化のためのツール開発などで大きな学びが得られた。

来年はSREのような活動をしていく予定だ。

Book

今年読んだ技術書の中からオススメを紹介する。

Private

2018年は年明けから風邪が続いたり腰を痛めたりと体調が優れないことが多かった。厄年だからだろうか。腰痛対策として始めたストレッチにより体が柔らかくなってつま先に手が届くようになったり、だんだんと風邪を引かなくなってきたりと、終盤には改善が見られた。

2019

体調に気をつけながら技術力を伸ばしていきたい。
ルイさんのCコンパイラ作成入門とかやってみようかな。

複数のPodのログをまとめて表示する

Kubernetesを開発環境として使用しプロダクションと同じような環境で開発すると、複数のアプリケーションが可動することになると思います。
そうするとログも複数の場所に出力されることになります。

複数個のターミナルを立ち上げ1つずつログを表示してもいいですが、場所を取りますし確認する箇所が増えて面倒なので、まとめて表示する方法を紹介します。

今回は以下の2つの構成に合わせたやり方を紹介します。

  • ログを標準出力に書き出している場合
  • ログをファイルに書き出している場合

ログを標準出力に書き出している場合

これは stern というアプリケーションを使えば簡単にできます。

github.com

使い方は簡単でPodの名前を引数として渡すだけです。
正規表現でいい感じに検索してくれて、オプションを入れればコンテナで絞り込むこともできます。

Dockerの標準に沿ったアプリケーションならこれを使えば解決です。

ログをファイルに書き出している場合

様々な理由によりDockerの標準に合わせることができず、ファイルにログを書き出している場合もあると思います。

この場合は kubectl exectail することになりますが、複数のPodのログをまとめるためには少し工夫をしなければいけません。

名前付きパイプを使う

名前付きパイプを使って複数の kubectl exec の出力を1つにまとめます。

名前付きパイプについての説明はこの記事がわかりやすいです。

qiita.com

名前付きパイプに kubectl exec を出力し、名前付きパイプを cat することでログをまとめて表示します。

シェルスクリプトで一連の処理を書きます。

#! /usr/bin/env bash

FIFO="/tmp/shout"

mkfifo ${FIFO}
trap 'rm ${FIFO}; exit 1' 1 2 3 15

PODA=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-a")].metadata.name}')
PODB=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-b")].metadata.name}')
PODC=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-c")].metadata.name}')

kubectl exec -i ${PODA} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \
kubectl exec -i ${PODB} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \
kubectl exec -i ${PODC} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \
cat ${FIFO}

stern と比べると見やすさや使いやすさで数段劣りますが、同じようにまとめて出力することができます。

GKEでプリエンプティブインスタンスを使いこなす

GCPにあるプリエンプティブ インスタンスをGKEでうまいこと使えないか、試行錯誤した結果をまとめます。

プリエンプティブ インスタンスとは

一言で言ってしまえば、AWSにあるスポットインスタンスGCP版です。

公式ドキュメントにはこのように書いてあります。

プリエンプティブ VM は、最長持続時間が 24 時間で、可用性が保証されない Google Compute Engine VM インスタンスです。プリエンプティブ VM は標準的な Compute Engine VM よりも低価格で、同じマシンタイプとオプションを使用できます。

ドキュメントに書かれているように低価格で使用できるのが最大のメリットです。
だいたい1/3ぐらいの価格で使用することができます。

f:id:akaimo3:20180722164618p:plain

しかし、様々な制限があります。
意識しておかなければならないこととして、次のようなことがあります。

  • いつ終了するかわからない
  • 最大でも24時間でシャットダウンされる
  • 常に使用できるとは限らない

これらの注意点と上手に付き合いながらGKEのNodeとして使用していきます。

使いこなす

シャットダウンの対策

まず、シャットダウンによりNode数が減ってしまう問題はGKEを使っている上では問題ありません。
KubernetesがNode数の減少を検知して、即座に元と同じ数になるようにNodeを起動し直してくれます。

しかし、一時的とはいえNodeが減るので、そこで可動しているPodは止まってしまします。
すぐに復帰するので、冗長化されているPodであればそこまで問題でもありません。せいぜい、一時的(数秒から数十秒)に別のPodに負荷が集中するぐらいです。

アプリケーションの構成などの理由により冗長化できないPodや一時的でも減っては困るPodに対しては、プリエンプティブ インスタンスに配置されないように設定をします。

プリエンプティブ インスタンスの回避

Kubernetesのtaintsとtolerationsという機能を使用してプリエンプティブ インスタンスを回避します。

まず、通常のインスタンスで構成されるノードプールとは別に、プリエンプティブ インスタンスだけで構成されたノードプールを作成します。 プリエンプティブのノードプールを作成するときに、ノードtaintを設定します。

f:id:akaimo3:20180722171308p:plain

そして、プリエンプティブ インスタンス配置されても問題ないPodにtolerationを設定します。

spec:
  template:
    spec:
      containers:
      ...
      tolerations:
        - key: gke-preemptible
          operator: Equal
          value: "true"
          effect: NoSchedule

これでこの設定がされているPodのみがプリエンプティブ インスタンスのノードプールに配置されるようになります。

taintsとtolerations

ちょっと仕組みがわかりにくいので、taintsとtolerationsの概念を説明します。

Nodeにtaints(よごれ)をつけ、そのtaintsをtolerations(寛容、黙許)できないPodをNoSchedule(スケジュールしない)と設定しています。
そしてPodにtolerationするtaintの内容を記述することで、taintのついたNodeに配置されるようになります。

24時間でシャットダウンされる

上で書いたプリエンプティブ インスタンスの回避の設定をしていれば24時間でシャットダウンされる制限も問題ないように感じるかもしれません。
しかし、24時間たつ前にシャットダウンされることがなかった場合、ノードプール内の複数のインスタンスが同時に24時間を経過し、同じタイミングでインスタンスがシャットダウンする可能性があります。

これだと、いくら冗長化していても運しだいですべてのPodが消えてしまう可能性があり、リスクが高くなります。

この問題の解決方法は簡単で、ノードプールを追加後、起動時間がバラけるように一部のNodeをシャットダウンしてしまえば良いです。
そうすることで複数のNodeが同時に24時間を迎えてシャットダウンされることが防げます。

常に使用できるかわからない

プリエンプティブ インスタンスがシャットダウンされたあと、GCPの状況によってはプリエンプティブ インスタンスが起動できないことがあります。

この場合、通常のインスタンスで構成されるノードプールにクラスターのオートスケールを設定しておくことで、そっちにインスタンスが追加されPodが配置されます。
tolerationsの設定がされていれも問題なく通常のインスタンスに配置されます。
なぜならtolerationsはtaintsを受け入れる設定なので、taintsが無いNodeにも配置されます。

まとめ

これを意識することでGKEを低価格で運用することができます。
ぜひ試してみてください。

GKEからCloud SQLに接続する

GKEからCloud SQLに接続するやり方としてCloud SQL Proxyを使う方法が推奨されています。

公式ドキュメントで解説されているやり方は、Cloud SQLにアクセスしたいコンテナが入っているPodにサイドカーとしてProxyを入れる方法です。
公式の例だと、wordpressのコンテナとProxyのコンテナを同じPodにしています。

GKE内にCloud SQLに接続するコンテナが一種類であれば公式のやり方で問題ありませんが、複数のサービスがCloud SQLにアクセスするとなると、全てのコンテナにサイドカーとして入れることになってしまい、あまりうれしくありません。

そこでProxyを単体のPodとして作成し、Serivceを経由してアクセスする方法をとります。

Deploymentの作成

基本的には公式ドキュメントと同じように進め、サイドカーとなっている部分だけを残し、適切なポートを開放してあげれば完了です。

注意点として、他のPodからのアクセスを受け入れる場合は、起動スクリプトでIPを指定しなければなりません。
ポート番号だけだと、ローカルホストからのアクセスしか受け付けてくれません。

具体的にはこのようにします。

command: ["/cloud_sql_proxy",
          "-instances=sample-165109:asia-northeast1:sample=tcp:0.0.0.0:3306",
          "-credential_file=/secrets/cloudsql/credentials.json"]

あとは適切にServiceを作成すれば完成です。

デメリット

サイドカーではなくPod単体として起動させるということは、クラスタ内のDBにアクセスするPodが全てこのPodを経由することになります。
そのため、このPodが起動していないと、全てのPodはDBにアクセスできなくなってしまいますので、きちんと冗長化しておきましょう。

定義ファイル

最後に定義ファイルの全体を貼っておきます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sql-proxy
  labels:
    app: sql-proxy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sql-proxy
  template:
    metadata:
      labels:
        app: sql-proxy
    spec:
      containers:
        - name: cloudsql-proxy
          image: gcr.io/cloudsql-docker/gce-proxy:1.11
          ports:
            - name: mysql
              containerPort: 3306
          command: ["/cloud_sql_proxy",
                    "-instances=sample-165109:asia-northeast1:sample=tcp:0.0.0.0:3306",
                    "-credential_file=/secrets/cloudsql/credentials.json"]
          volumeMounts:
            - name: cloudsql-instance-credentials
              mountPath: /secrets/cloudsql
              readOnly: true
      volumes:
        - name: cloudsql-instance-credentials
          secret:
            secretName: cloudsql-instance-credentials
---
apiVersion: v1
kind: Service
metadata:
  name: sql-proxy
spec:
  selector:
    app: sql-proxy
  ports:
    - name: mysql
      protocol: TCP
      port: 3306
      targetPort: mysql

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日を切ると更新されます。