Kubernetes on AWS: LoadBalancer型 Service との決別

LoadBalancer型 Service (type: LoadBalancer) は、Pod群にアクセスするための ELB を自動的に作ってくれて便利なのだが、ELB に関する全ての設定をサポートしているわけではなく、Service を作り直す度に、k8s の外側でカスタマイズした内容もやり直さなければならないのはつらい。というわけで、type: LoadBalancer を利用するのは止めて、ELB は Terraform で管理し、そこから NodePort型 Service に接続する方法を試してみた。

Kubernetes がサポートする ELB 設定

「ELB に関する全ての設定をサポートしているわけではなく」と書いたが、今現在どれぐらいサポートされているのだろうか? 改めて調べてみた。

Service 定義の metadata.annotations に、以下の値を書くことで ELB の設定を行うことが出来る (v1.5現在)。

  • Backend protocol
    • TCP – default
    • HTTP – service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
    • HTTPS – service.beta.kubernetes.io/aws-load-balancer-backend-protocol: https
  • SSL Certificate
    • service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "[cert-arn]"
  • SSL Port
    • service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
    • The SSL certificate will only be used for 443 and not 80.
  • Internal ELB
    • service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
  • Security group
    • service.beta.kubernetes.io/load-balancer-source-ranges: [a comma separated list of CIDRs]
  • Idle timeout
    • service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: [seconds]
  • Access logs
    • Enabled – service.beta.kubernetes.io/aws-load-balancer-access-log-enabled: [true|false]
    • Emit interval – service.beta.kubernetes.io/aws-load-balancer-access-log-emit-interval: [minutes]
    • s3://bucket/prefix
      • S3 bucket – service.beta.kubernetes.io/aws-load-balancer-access-log-s3-bucket-name: [bucket-name]
      • S3 prefix – service.beta.kubernetes.io/aws-load-balancer-access-log-s3-bucket-prefix: [prefix]
  • Cross-Zone Load Balancing
    • service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: [true|false]
  • Connection draining
    • service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: [true|false]
    • service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: [seconds]
  • Proxy protocal
    • service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'
  • Route53

こうやってまとめてみると、v1.5 においては、ELB の設定項目はほとんど網羅されてるような印象を受ける。Route53 についても Third party の拡張を使えばなんとかなるようなので、ほとんどのケースではわざわざ LoadBalancer を別管理にする必要はないのかもしれない。

それでもあえて、LoadBalancer を k8s の外側で管理する理由があるとすれば、

  • k8s に問題が起きた時に、通常の EC2方式に戻せるようにしておきたい。
  • Application Load Balancer (ALB) を使いたい。
  • ELB に分かりやすい名前を付けたい。
    • k8s側から作ると a5902e609eed711e69a1986001d7b1fb みたいなランダムな名前になる。
    • Tag kubernetes.io/service-name みれば、どの Service のものかは分かるのだけど。
  • Cloudwatch Alarm を Terraform で管理したい。
    • ELB に Cloudwatch Alarm を設定する場合は、ELB も Terraform で管理しておいた方がやりやすい。
  • k8s の LoadBalancer 管理に一抹の不安がある。

ぐらいだろうか。

移行手順

AWS 上の Kubernetes クラスタが kops で構築されていることを前提に、ELB を Terraform で 管理する運用に移行してみる。

  • クラスタの名前を仮に k8s.example.com とする。

1. Terraform ELB から k8sノードへアクセス出来るように追加のセキュリティグループを作る

k8sノード用とELB用の二つのセキュリティグループを作る。

# For nodes
resource "aws_security_group" "nodes" {
  vpc_id = "${module.environment.vpc_id}"
  name = "additional.nodes.k8s.example.com"
  description = "Additional security group for nodes"
  tags {
    Name = "additional.nodes.k8s.example.com"
  }
  # ELB から NodePort経由のアクセスを受け付ける
  ingress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    security_groups = ["${aws_security_group.service_elb.id}"]
  }
  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# For ELB
resource "aws_security_group" "service_elb" {
  vpc_id = "${module.environment.vpc_id}"
  name = "k8s-service-elb"
  description = "Security group for k8s service ELBs"
  tags {
    Name = "k8s-service-elb"
  }
  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port = 443
    to_port = 443
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

kops 1.5.x 以降では、マスターあるいはノードに対して、デフォルトのセキュリティグループの他に、独自のセキュリティグループを追加することができる。

上で作ったセキュリティグループ(additional.nodes.k8s.example.com)が、ノード起動時に適用されるようにクラスタの設定を更新する。

$ kops edit ig nodes
...
spec:
  additionalSecurityGroups:
  - sg-xxxxxxxx  # additional.nodes.k8s.example.com
...

$ kops update cluster k8s.example.com --yes

Sticky session を必要としない場合

2. Service を NodePort 型で立ち上げる

通常 NodePort の番号は自動で決定されるが、Terraform ELB から接続できるように固定の番号を設定しておく。NodePort に設定出来る番号の範囲は 30000-32767

apiVersion: v1
kind: Service
metadata:
  name: example-app
spec:
  selector:
    app: example-app
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 30000

3. Terraform で ELB を立ち上げる

  • instance_porthealth_checktarget に NodePort を指定する。
  • instances は設定しない(後ほど Auto Scaling group と紐付けるため)。
  • k8sノードへアクセス出来るように、先ほど作ったセキュリティグループ(k8s-service-elb)を設定する。
resource "aws_elb" "example_app" {
  name = "k8s-example-app"
  cross_zone_load_balancing = true
  subnets = ["${split(",", module.environment.subnets)}"]
  security_groups = ["sg-xxxxxxxx"]   # k8s-service-elb
  listener {
    instance_port = 30000
    instance_protocol = "http"
    lb_port = 80
    lb_protocol = "http"
  }
  listener {
    instance_port = 30000
    instance_protocol = "http"
    lb_port = 443
    lb_protocol = "https"
    ssl_certificate_id = "${module.environment.ssl_certificate_id}"
  }
  health_check {
    healthy_threshold = 3
    unhealthy_threshold = 2
    timeout = 5
    target = "HTTP:30000/healthcheck"
    interval = 30
  }
}

4. ELB にドメイン名を設定する

サービスのドメイン名が、たった今作った ELB を参照するように Route53 を設定する。

resource "aws_route53_record" "example_app" {
  zone_id = "${module.environment.zone_id}"
  name = "app.example.com"
  type = "CNAME"
  ttl = "300"
  records = ["<elb-dns-name>"]
}

5. ELB をノードの Auto Scaling Group に紐付ける

3 で立ち上げた ELB を、k8sノードの Auto Scaling Group (nodes.k8s.example.com) の Load Balancers に追加する。これによりk8sノードがELBに追加される。

6. 全ての k8sノードが ELB に追加されて、InService になることを確認する

サービスのURLにアクセスして、サービスが問題なく稼動していることを確認する。

Sticky session を必要とする場合

アプリケーションが sticky session を必要とする場合は、予め ingress controller をクラスタにインストールしておき、そこ経由でサービスにアクセスさせる。なので、このケースではアプリケーション用の ELB は立てない。

参考: Kubernetes on AWS で sticky session を実現する | ゆびてく

今回の移行では、ingress controller の入り口となる Service を NodePort で作っておいて、そこにアクセスする ELB を Terraform で作った。

2. Service を ClusterIP 型で立ち上げる

apiVersion: v1
kind: Service
metadata:
  name: example-app
spec:
  selector:
    app: example-app
  type: ClusterIP
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

3. Ingress controller にドメイン名を追加する

サービスのドメイン名が ingress controllerの ELB を参照するように Route53 を設定する。

resource "aws_route53_record" "example_app" {
  zone_id = "${module.environment.zone_id}"
  name = "app.example.com"
  type = "CNAME"
  ttl = "300"
  records = ["<ingress-controller-elb-dns-name>"]
}

4. Ingress を追加する

Ingress controller から対象のサービスにアクセス出来るように ingress rule を追加する。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sticky-session-ingress
spec:
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-app
          servicePort: 80

5. サービスが問題なく稼動していることを確認する

サービスのURLにアクセスして、サービスが問題なく稼動していることを確認する。

Kubernetes Secrets の紹介 – データベースのパスワードやその他秘密情報をどこに保存するか?

なんてこったい(棒)

GitHub の Public リポジトリには、太っ腹な開発者によって大量の Credentials(外部サービスに接続するための秘密キーなど)が公開されており、賢い人たちが日夜クローラーを走らせてそれらを回収し、5万件もの Uber ドライバーの個人情報を頂戴するために利用したり高価な AWS のインスタンスを沢山立ち上げて、もの凄い勢いでビットコインを発掘したりしているらしい。

ウチのリポジトリはプライベートだから問題ないよねって思われる方もおられるかもしれないが、ほんの5分間違って公開しただけで流出したケースもあるらしいので、そもそもコードリポジトリに秘密情報を入れること自体が太っ腹行為の可能性を高めていることを理解しておきたい。

というわけで、Kubernetes でサービスを運用する場合、そういった秘密情報をどこに保存すれば良いかという要求に応えるのが Secrets という仕組みである。

kube-secrets

秘密情報を Secrets というデータベースで集中管理し、それぞれの情報はそれらを必要とする Pod/Container のみに送られる。Docker Image や Container を作るプロセスから秘密情報を切り離せるので、その過程で情報を漏洩させるリスクは少なくなる。Container に送られた秘密情報は tmpfs 上に置かれるので、ノード上のディスクに書き込まれることもない。

Kubernetes 自体がまだ若いプロジェクトなので、この Secrets にも注意しなければならない点がいくつかある。

  • Secrets のデータは etcd の中に平文で保存されているので、etcd には管理者ユーザーだけがアクセス出来るようにセットアップする必要がある。
    • etcdは kubernetes のあらゆるデータを保管しているデータストア。
  • 現在のところ、Secrets に対するユーザーごとのアクセスコントロールは出来ない(将来的にはサポート予定)。

以下は、社内向けに書いた Secrets の簡単なチュートリアル。


Secret のデータ構造

Secret の中身は単純な Key-Value ペアのリスト:

secret-structure

kubectl コマンドで登録されている Secret のリストを見る:

$ kubectl get secrets
NAME                  TYPE                                  DATA      AGE
default-token-pb7ls   kubernetes.io/service-account-token   3         21m
mysecret              Opaque                                2         38s

その中から一つの Secret を選んで中身を見てみる:

$ kubectl describe secret mysecret
Name:       mysecret
Namespace:  sandbox
Labels:     <none>
Annotations:    <none>

Type:   Opaque

Data
====
password:   12 bytes
username:   5 bytes

mysecret の内容を図に書くと以下のような感じ:

mysecret

Secret を登録する

Secret は、以下の二種類のファイルのいずれかを経由して登録できる。

  1. 中身が Value になっているファイル(便宜的に「Secret Value ファイル」と呼ぶ)
  2. YAML あるいは JSON 形式の Kubernetes Manifest ファイル

以下のような Secret を、

[Secret: test-secret] => [Key: password] => [Value: this-is-a-password]

それぞれのファイル形式で登録してみよう。

1. Secret Value ファイル経由

1) ファイルを作る

$ echo -n "this-is-a-password" > ./password

2) --from-file オプションを使って登録

$ kubectl create secret generic test-secret --from-file=./password
secret "test-secret" created

3) 中身を見てみる

$ kubectl describe secrets/test-secret
Name:       test-secret
Namespace:  sandbox
Labels:     <none>
Annotations:    <none>

Type:   Opaque

Data
====
password:   18 bytes

--from-file に指定したファイルの名前が Key になっていることが分かる。

2. Kubernetes Manifest ファイル経由

1) Value を base64 でエンコードする

$ echo -n "this-is-a-password" | base64
dGhpcy1pcy1hLXBhc3N3b3Jk

2) ファイルを作る

以下の内容を secret.yaml に保存:

apiVersion: v1
kind: Secret
metadata:
  name: test-secret
type: Opaque
data:
  password: dGhpcy1pcy1hLXBhc3N3b3Jk

3) -f オプションを使って登録

$ kubectl create -f ./secret.yaml

4) 中身を見てみる

$ kubectl describe secrets/test-secret
Name:       test-secret
Namespace:  sandbox
Labels:     <none>
Annotations:    <none>

Type:   Opaque

Data
====
password:   18 bytes

Secret Value ファイル経由のときと全く同じ Secret が出来ていることが分かる。

Secret の中身を取得する

$ kubectl get secret test-secret -o yaml
apiVersion: v1
data:
  password: dGhpcy1pcy1hLXBhc3N3b3Jk
kind: Secret
metadata:
  creationTimestamp: 2017-03-01T08:49:49Z
  name: test-secret
  namespace: sandbox
  resourceVersion: "12535581"
  selfLink: /api/v1/namespaces/sandbox/secrets/test-secret
  uid: 0747637f-fe5c-11e6-8f7a-0674330dcd09
type: Opaque

Value をデコードする:

$ echo "dGhpcy1pcy1hLXBhc3N3b3Jk" | base64 --decode
this-is-a-password

Secret を使う

Container から Secret を使うには、

  1. 環境変数
  2. Volume としてマウント

の二種類の経路がある。

1. 環境変数

Container の Manifest から以下のような感じで参照:

containers:
- name: http-debug-server
  image: cotoami/http-debug-server:latest
  ports:
  - containerPort: 3000
  env:
    - name: PASSWORD
      valueFrom:
        secretKeyRef:
          name: test-secret
          key: password

Container にログインして、環境変数を見てみる:

$ kubectl get pods
NAME                                 READY     STATUS    RESTARTS   AGE
http-debug-server-4073343574-ro8s8   1/1       Running   0          2m

$ kubectl exec -it http-debug-server-4073343574-ro8s8 /bin/sh

# echo $PASSWORD
this-is-a-password

2. Volume としてマウント

Container 内のディレクトリに、Key をファイル名、Value をファイルの中身としてマウントできる。

以下のように、volumes を定義しておいて、それを volumeMounts でマウントする:

containers:
- name: http-debug-server
  image: cotoami/http-debug-server:latest
  ports:
  - containerPort: 3000
  volumeMounts:
  - mountPath: /tmp
    name: test-secret
    readOnly: true
volumes:
- name: test-secret
  secret:
    secretName: test-secret

Container にログインして、マウントされたファイルを見てみる:

$ kubectl get pods
NAME                                 READY     STATUS    RESTARTS   AGE
http-debug-server-2197190929-q6lke   1/1       Running   0          1m

$ kubectl exec -it http-debug-server-2197190929-q6lke /bin/sh

# cat /tmp/password
this-is-a-password

マウントされた Secret を後から更新した場合は、Kubernetes が自動的に検出してリフレッシュしてくれる。

参考

Kubernetes の登場でインフラ担当の仕事はますます曖昧になっていくような気がする

アプリケーション開発者は Dockerfile を作るところまで、インフラ担当はサービス構成とデプロイメントパイプラインを整備する、という分担を考えていたけれど…

マニフェストファイルを作ってサービスの構成をデザインしたり、ビルドスクリプトを書いて自動デプロイを実現するのも、もうアプリケーション開発者のレベルで出来る。ここを無理に分業すると、むしろ k8s が提供するサイクルの短さを享受出来なくなる可能性がある。サービスが出来上がってからインフラを移行するというのは k8s の考え方に合わないので、開発の初期から k8s 上で育てるのが標準的なモデルになるが、そのモデルに則って開発して行けば、開発者は無理なくそのプラットフォームを使いこなせるようになる。

つまり、DevOps の問題が、Kubernetes によるマイクロサービス化によって、ますます深刻になる可能性がある。アプリケーション更新のサイクルとインフラ更新のサイクルが限りなく近づいたとき、そこにどこまでのコミュニケーションコストを許容出来るだろうか。

チームとして分かれていることのコミュニケーションコストは決して過小評価することは出来ない。

では、インフラチームは Kubernetes クラスタの管理やモニタリングを担当するようにしたらどうか?

筋的には悪くない気もするが、GKE のような環境が充実してきた時に、アプリケーション開発者が自身で出来る領域はさらに広がって行くのではないかと思う。

さらには、Kubernetes のような環境に適応出来る・出来ない、という形でも大きな分断が起きて行きそうな予感もある。

Kubernetes に限らず、インフラの自動化を進めるためには、アプリケーションパッケージのポータビリティが重要になってくる。しかし、そのようなことを意識して開発しているプロジェクトは案外少ないのではないだろうか。例えば、Twelve-Factor App みたいな指針を全く知らないというのも珍しくないのではないか。そうなると、インフラ担当はアーキテクチャやソフトウェアデザインを指導する立場になるが、これは結構広範な指導を必要とするし、そのような規律を快く思わないエンジニアも多い。

一方で、Twelve-Factor App みたいな指針に慣れているエンジニアは、上に書いたように、自分でアーキテクチャや自動化の基盤を作って行けるので、自律してサービスを開発出来る。

アプリ開発もインフラも、一つのチームに閉じることに越したことはないけれど、多くの組織ではそんな贅沢は許されないだろう。組織全体でインフラを刷新していくためには、どうしても独立したインフラチームが必要になる。でも、マイクロサービス開発のサイクルの短さに合わせて、チーム間のコミュニケーションコストを下げて行くのは至難の業のように思える。

DevOpsの起源とOpsを巡る対立 | ゆびてく

Docker(コンテナ型仮想化)と Kubernetes についての簡単な紹介

社内向けに書いた文書です。

コンテナ型仮想化とは何か?

OS上に、コンテナと呼ばれる、隔離されたアプリケーションの実行環境を作り、一台のホスト上であたかも複数のホストが動いているかのような環境を実現するのが、コンテナ型仮想化技術です。

コンテナ型仮想化と従来の VM 型仮想化を比較したものが以下の図です(Xen は AWS EC2 などの仮想化を実現する実装で、Docker はコンテナ型仮想化の代表的な実装)。

container

これらの仮想化技術に共通するのは、

  1. アプリケーションの実行環境(ホスト)を仮想化すること
  2. それらのスナップショットをイメージとして保存することで、アプリケーションを環境ごとパッケージング出来るようにすること

という二つの目的です。

VM 型仮想化ではハードウェアのレベルで仮想化が実現されているので、ホスト上で動く一つ一つの VM の中でそれぞれ別々の OS を動作させることができます。しかしその一方で、実行環境(VM)のサイズが大きくなり、アプリケーションの起動にかかる時間も長くなります。

コンテナ型仮想化では OS 上の実行環境を仮想化するので、OS 自体は限定されますが、その分実行環境(コンテナ)のサイズはコンパクトになり、起動にかかる時間も短くなります。

基本的に VM 型仮想化からコンテナ型仮想化への移行は、一つ一つのアプリケーションの粒度が小さくなる、いわゆるマイクロサービス化を促進します。インフラ上の更新の単位はより小さくなり、変更のサイクルは短くなります。

パッケージング技術の変遷

packaging

Kubernetes とは何か?

1. Docker 実行環境をクラスタ化する

通常、Docker の実行環境は一台のホストに閉じています。

docker-network

同一ホスト内で動くコンテナ同士は、プライベートネットワーク経由でやり取りができますが、ホストの外側とやり取りする場合は NAT (IP Masquerade) を経由する必要があります。

このように、標準の Docker 実行環境では、ホスト間の連携が煩雑になるため、コンテナの数が増えて要求されるリソースが大きくなった時に、容易にスケールアウトすることが出来ません。

この問題を解決するのが Kubernetes です。

Kubernetes によって、複数台のホストから構成される実行環境を あたかも一台の実行環境のように 扱うことができるようになります。

kube1

コンテナを起動する際は、イメージと台数を指定するだけでよく、クラスタのどこにどのように配置するかは Kubernetes 側で面倒を見てくれます(スケジューリング)。

そして、クラスタのリソース(CPU、メモリ、記憶領域など)が足りなくなった場合は、単純にノードを増やすだけで、既存のサービスに影響を与えることなく、いくらでも拡張することができます。

2. Self-healing – 耐障害性

kube2

上図はデプロイプロセスの詳細です。

  1. オペレーターが、どのようなコンテナを何台起動するかといった情報を Spec として Kubernetes 側に渡すと、
  2. Scheduler が、空きリソースを見ながらそれらをどのように配置するかを決定し、
  3. 各ノードに常駐している Kubelet というプログラムがその決定に従ってコンテナを起動します

オペレーターが直接コンテナを起動するのではなく、必要とする状態を Spec として渡すと、Kubernetes 側がクラスタの状態を Spec に合わせようする、というこの挙動が重要です。

仮に、運用中のコンテナに不具合があってサービスがダウンしたとします。Kubernetes はこの状態変化を察知し、Spec の状態に合わせようとして、そのコンテナを自動的に再起動します(Self-healing)。

多少の不具合であれば問題なく運用出来てしまう反面、問題の発覚するタイミングが遅れてしまう可能性もあるのでモニタリングが重要になります。

コンテナではなく、ノードとなるホストマシンに障害があった場合はどうなるか? Kubernetes 環境のセットアップによりますが、AWS の場合は Auto Scaling グループでクラスタが組まれているので、自動的にインスタンスが作り直され、その中に元の状態が復元されます。

3. Pod – 管理上の基本単位

Kubernetes 上で動作するプログラムの最小単位はコンテナですが、管理上の基本単位は Pod というものになります。

pod

Pod は、Volume という記憶領域を共有するコンテナの集まりで、Volume の他には一つのIPアドレスを共有しています。つまり、Pod は Kubernetes 上でホストに相当する単位です。

VM イメージによるパッケージングでは、一つのホストがデプロイの単位になっていました。あるいは、Java EE の WAR パッケージだと、必要最低限のプログラムだけをデプロイの単位に出来て軽量ですが、上図で言うと、デプロイの対象は webapp の部分だけになります。Docker/Kubernetes 環境でマイクロサービス化を推進すると、ホストを構成する様々な部品(webapp, nginx, log collectorなど)全てを、独立してデプロイ出来るようになります。

kubectl というコマンドがセットアップされていれば、以下のようにして Pod の一覧を見ることができます。

$ kubectl get pods
NAME                                 READY     STATUS    RESTARTS   AGE
cotoami-2660026290-81y2g             1/1       Running   0          9d
cotoami-2660026290-xzua4             1/1       Running   0          9d
default-http-backend-t4jid           1/1       Running   0          6d
grafana-807516790-8pszk              1/1       Running   0          63d
http-debug-server-2355350955-38jht   1/1       Running   0          6d
http-debug-server-2355350955-c64y1   1/1       Running   0          6d
http-debug-server-2355350955-g3rjq   1/1       Running   0          6d
nginx-ingress-controller-ka0od       1/1       Running   0          6d
node-exporter-556yr                  1/1       Running   0          63d
node-exporter-euprj                  1/1       Running   0          63d
node-exporter-hzdqk                  1/1       Running   0          63d
prometheus-1314804115-9v0yk          1/1       Running   0          54d
redis-master-517881005-ceams         1/1       Running   0          70d

コンテナのデバッグ

Pod 内のコンテナにログインしたい場合は、リストにある Pod の名前をパラメータにして以下のコマンドを実行します。

$ kubectl exec -it http-debug-server-2355350955-38jht /bin/sh
/app # ls
Dockerfile    README.md     circle.yml    index.js      node_modules  package.json

Pod に複数のコンテナが含まれる場合は、-c オプションでコンテナの名前を指定します。

$ kubectl exec -it POD -c CONTAINER /bin/sh

あるいは、以下のコマンドでコンテナが出力するログを見ることができます。

$ kubectl logs redis-master-517881005-ceams
1:M 16 Feb 03:27:35.080 * 1 changes in 3600 seconds. Saving...
1:M 16 Feb 03:27:35.081 * Background saving started by pid 226
226:C 16 Feb 03:27:35.084 * DB saved on disk
226:C 16 Feb 03:27:35.084 * RDB: 0 MB of memory used by copy-on-write
1:M 16 Feb 03:27:35.181 * Background saving terminated with success

# 複数のコンテナがある場合
$ kubectl logs POD CONTAINER 

4. Deployment – Pod の配備と冗長化

Pod の配備と冗長化を担当するのが Deployment という仕組みです。

deployment

ある Pod について、Spec で定義されたレプリカの数を維持する責任を負うのが Replica Set、Replica Set の配備・更新ポリシーを定義するのが Deployment です。

以下は、Deployment 定義の例です。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: http-debug-server
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: http-debug-server
    spec:
      containers:
      - name: http-debug-server
        image: cotoami/http-debug-server:latest
        ports:
        - containerPort: 3000

この内容を、例えば http-debug-server.yaml というファイルに保存し、以下のコマンドを実行すると Deployment として定義された Replica Set がクラスタ内に出来上がります。

$ kubectl create -f http-debug-server.yaml

$ kubectl get deployments
NAME                DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
http-debug-server   3         3         3            3           6s

Rolling Update

Deployment には標準で Rolling Update(無停止更新)の機能が備わっています。

例えば、以下のようなコマンドで Docker イメージのバージョンを更新すると、Replica Set の中のコンテナ全てを一斉に更新するのではなく、稼働中の Pod を何台か維持したまま、一つずつ順番に更新を行います。

$ kubectl set image deployment/DEPLOYMENT CONTAINER=IMAGE_NAME:TAG

5. Service – Pod へのアクセス

Pod への安定的なアクセス手段を提供するのが Service です。

Deployment で配備した Pod にアクセスしようと思っても、実際に Pod がどのノードに配備されているかは分かりません。仮に分かったとしても、Pod は頻繁に作り直されるので、いつまでも同じ Pod にアクセスできる保証はありません。Replica Set に対するロードバランシング機能も必要です。

ClusterIP

そこで Service は、Pod の集合(一般的には Replica Set)に対して安定的にアクセスできる仮想の IP アドレスを割り当てます。これを cluster IP と呼び、Kubernetes クラスタ内だけで通用するアドレスです。cluster IP にアクセスすると、Service の対象となる Pod 群の中のいずれかの Pod にアクセスできます。

NodePort

さらに Service は、クラスタの外部から Pod にアクセスするための経路も開いてくれます。具体的には、各ノードの特定のポートを経由して Pod にアクセス出来るようになります。

service

上図のように、Service の実体は、cluster IP のルーティングとロードバランシング機能を実現する kube-proxy というプログラムです。

クラスタの外部から Service が用意してくれたポートを経由してノードにアクセスすると、kube-proxy 経由で適切な Pod に接続することができます。

Kubernetes on AWS で sticky session を実現する

  • 対象の環境
    • Kubernetes 1.4.x(筆者の環境は 1.4.6)
    • AWS

AWS 上で稼動する Kubernetes (k8s) で LoadBalancer 型の Service を作ると、自動的に入り口となる ELB (Elastic Load Balancer) を立ち上げてくれてとても便利なのだが、AWS では当たり前に出来ていた sticky session が、この構成だとどうも実現できないらしい。

k8s で新規に開発しようという場合なら、sticky session が必要な実装はなるべく避けるのかもしれないが、稼働中の Web アプリを k8s に移行したいとなった場合に sticky session が必要になるケースもあるのではないだろうか。

というわけで、k8s on AWS における sticky session の実現方法について調べてみた。

諸悪の根源 kube-proxy

まず、単に ELB の sticky session を有効にしたらどうなるだろうか?

[ELB] = sticky => [Node] => [kube-proxy] => [Pod]

ELB から kube-proxy までが、Service の内部構造である。まず ELB が選択するのは k8s クラスタの各ノードである。ELB が sticky に設定されている場合、ノードの選択が sticky になるだけで、その先の Pod の選択は kube-proxy に委ねられている。

もう少し詳しく見てみると、

k8s-sticky-session

こんな感じで、ELB が sticky になってても意味はなく、Pod の選択を行う kube-proxy が sticky にならないといけない。

Service の sessionAffinity 機能

実は k8s の Service (kube-proxy) には sessionAffinity という機能がある。これを有効にすれば IP アドレスベースの stickiness を実現できるらしいのだが、なんと type が LoadBalancer の場合はこの機能を利用出来ないという。

ClientIP value for sessionAffinity is not supported when service type is LoadBalancer.

IPアドレスが隠れちゃう問題

では、LoadBalancer 型を利用するのはやめて、NodePort 型の Service にして、sessionAffinity を有効にし、自前で ELB を立てたらどうなるか?

その場合も、ELB がクライアントの IP アドレスを隠してしまうので sessionAffinity は正しく動作しない。現状の kube-proxy は、ELB がサポートしている X-Forwarded-For ヘッダや、proxy protocol を解釈しないので、[ELB] → [kube-proxy] の構成だとどう頑張っても sticky session を実現できないことになる。LoadBalancer 型の Service が sessionAffinity をサポートしない所以だ。

Proxy protocol のサポートについては多くのリクエストがあるようなので、将来的にはサポートされる可能性が高い。もしサポートされれば IP アドレスベースの stickiness については標準構成で利用出来るようになる。

解決策 1) ELB と kube-proxy の間に nginx を立てる

kube-proxy は X-Real-IP ヘッダを解釈するらしい。これを利用して、ELB と kube-proxy の間に nginx を立てて、そこで X-Real-IP ヘッダを追加するという手段が紹介されていた(ちなみに、ELB は X-Real-IP を追加してくれない)。

以下のような構成になる。

[ELB] = proxy protocol => [nginx] = X-Real-IP => [kube-proxy]

解決策 2) Nginx Ingress Controller

解決策 1) も悪くないように思えるが、IP アドレスベースの stickiness だと Web アプリとしてはあんまり嬉しくないので、なんとか諸悪の根源である kube-proxy をバイパスする方法がないだろうかということで見つけたのが、最近サポートされ始めたばかりの Ingress を使う方法だ。

Ingress とは、Service に対する柔軟なルーティングやロードバランシング機能を提供する k8s では比較的新しいリソースで、機能的には LoadBalancer 型の Service を切り出して、独立したリソースにしたような感じである。

Ingress を利用するためには、まず Ingress Controller というものを k8s 内に立ち上げておく必要がある。Ingress Controller の実体は proxy server であり、その proxy server に適用するルールのセットを Ingress リソースと呼ぶ。

ingress-controller

Ingress Controller には色々な実装が提供されていて、その中でも k8s の公式実装と思われる Nginx Ingress Controller が sticky session をサポートするために kube-proxy をバイパスするらしい。まさに今回のニーズにぴったしな実装だと言える。

The NGINX ingress controller does not uses Services to route traffic to the pods. Instead it uses the Endpoints API in order to bypass kube-proxy to allow NGINX features like session affinity and custom load balancing algorithms. It also removes some overhead, such as conntrack entries for iptables DNAT.

Nginx Ingress Controller のセットアップ

実際に Nginx Ingress Controller をセットアップする手順は以下の通り。必要なマニフェストファイル一式は、cotoami-infraのリポジトリ に置いてある。

1) まず、ルーティング先が見つからなかった場合にリクエストを送るための、デフォルトのバックエンドサービスを立てる。このサービスは単に 404 を返すだけである。

$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/default-backend.yaml

2) Ingress Controller を作る前に、ConfigMap による設定を先に作っておく。

$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/nginx-load-balancer-conf.yaml

この ConfigMap の中に、sticky session を有効にする設定 enable-sticky-sessions: "true" が含まれている。

3) 設定が登録出来たら、主役の Ingress Controller を作る。

$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/ingress-controller.yaml

上のマニフェストファイル ingress-controller.yaml では、Ingress Controller に外部からアクセス出来るように、LoadBalancer 型の Service をくっつけてある。これで、ELB 経由で Ingress Controller にアクセス出来るようになる。

(※) Controller本体は、公式の例に倣って ReplicationController で作成しているが、Daemonset で作ることも出来る。

以下のように Service の一覧を見ると、nginx-ingress-controller へアクセスするための Service が登録されているのが分かる。

$ kubectl get svc
...
nginx-ingress-controller   100.66.116.210   a5902e609eed6...   80/TCP     20s
...

a5902e609eed6... と省略表示されているのが ELB の DNS name である。試しにこのアドレスにアクセスしてみると、

default backend - 404

という感じで、先ほど立ち上げておいたデフォルトサービスの応答が得られるはずだ。

4) Ingress をテストするための、テスト用 Web アプリを立ち上げる。

Ingress 経由でアクセスする Web アプリを立ち上げる。ここでは、HTTPやサーバーに関する情報を表示するだけの http-debug-server を立ち上げてみる。

$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/http-debug-server.yaml

マニフェストファイルにあるように、Nginx Ingress Controller 経由でアクセスする場合、Service の type は ClusterIP にしておく。

5) Ingress リソースを登録する

最後にルーティングルールを登録する。

$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/ingress.yaml

このルールでは、ホスト debug.example.com に対するアクセスを、http-debug-server Service にルーティングするように設定している。以下のように、curl コマンドでアクセスすると、デバッグ用の情報がレスポンスとして返されるはずだ。

$ curl <ELB-DNS-name> -i -H 'Host: debug.example.com'

sticky session をテストするためには cookie が必要になるので、以下のようにしてレスポンスヘッダーを表示させる。

$ curl <ELB-DNS-name> -I -H 'Host: debug.example.com'

以下のような感じで、ヘッダーに cookie の情報が含まれているので、

...
Set-Cookie: route=acfc2f2f4d692d7a90b9797428615e29d3936e95; Path=/; HttpOnly
...

この cookie をリクエストに含めて送信する。

$ curl -b route=acfc2f2f4d692d7a90b9797428615e29d3936e95 <ELB-DNS-name> -H 'Host: debug.example.com' -s | python -mjson.tool

レスポンスの JSON データが出力されると思うが、hostname のところに注目して欲しい。何度リクエストを送っても、同じホスト名が返ってくるはずだ。試しに-b オプションを外して送信すると、送信するたびにホスト名が変わる。

大分ややこしい感じになったが、以上が Ingress で sticky session を実現するための手順である。

解決策 3) service-loadbalancer

その他、k8s が提供している service-loadbalancer を使うことで sticky session を実現する事例が紹介されていた。

Elm沼とKubernetes沼で交互に溺れているうちに1月が終わっていた

ElmCotoami のユーザーインタフェースを作るために選択したプログラミング言語で、そのデザインは JavaScript 界で一世を風靡している Redux の原型になったと言われている。その原型を体験してみたいという単純な好奇心だけで Elm を選択し、分かりやすいチュートリアルに感心したのも束の間、実際にアプリケーションを作り始めると、入り口ではあんなに優しかった Elm の顔がみるみる般若のようになり、気がついたら底なしの泥沼に足を取られていた。

Elm のような、いわゆる純粋な関数型と呼ばれる環境では、言語からコンピューターを直接操作することが出来ない。出来るのはデータの変換だけだ。この変換がいわゆる「関数」で、関数型でプログラマーに許されているのは関数を書くことだけである。しかし、コンピューターを操作出来ないと何も出来ないのでどうするのかと言うと、コンピューターへの命令をデータとして表現して、それを返す関数を作って環境に渡すみたいなことをする。

関数型にはこのような制限があるので、従来の命令型言語でプログラムを書く場合に比べて、かなり回りくどい書き方をしなければならない。しかしその代償として、プログラムは必ず「データの変換」という形に落とし込まれるので、実行時に意図しない動作をすることが格段に少なくなり、デバッグやテストも容易になるというわけだ。

関数型の回りくどさに加えて、Elm には厳密な型システムがある。実は今、開発スピードを減退させている最大の原因はこの型システムである。代数的データ型とか、今まで遭遇したことのないコンセプトに純粋な感動を覚える一方、曖昧さを許さない型システムのおかげで、型を合わせるためにどうすれば良いかというのを考えてるだけで膨大な時間が過ぎて行く。

動的型が、書く時にいかに楽をするか(少ない記述でいかに多くを実現するか)を主眼に置いているとすれば、静的型は、書いた後にいかに楽をするか(起こりうることを明示的に示しておく)ことに主眼を置く。しかし、動的型でも、曖昧に書いて楽をしつつも、危ないと思うところは慎重に書いたり、テストでフォローしたりするわけなので、単純に上のような図式が当てはまるとも思えず、どちらが良いと判断するのはなかなか難しい。動的型の言語であれば、場所によって手綱を締めたり緩めたり出来るのが、静的型だと一律で同じような書き方をしなければならない、というのがなかなか辛いところである。

まだ言語自体に慣れてないということもあるし、これから開発が進んで、規模が大きくなり、複数人で連携するようになったら関数型や型システムの恩恵を実感出来るのかもしれないなと思いつつ、最初の敷居はやっぱり高かったということだけここに記録しておく。

一方の、Kubernetes 沼の方はと言えば、こちらは Cotoami で試行錯誤した成果を仕事の方にフィードバックしたいということで、プロダクションへの導入を目指して色々と準備しているため、Elm よりもより深い深い泥沼となっている。Elm の場合はとりあえずユーザーインタフェースが動けばなんとかなるのに対して、インフラの場合はその辺のごまかしが効かないので、あらゆる側面から検証を行わなければならず、人材の不足も手伝ってかなり余裕のない状況に陥ってる今日この頃(もし、同じように Kubernetes で試行錯誤している方がいたら、情報交換したいなあと思うのですが… @marubinotto までご連絡お待ちしております(切実))。

Kubernetes は未来のインフラだとの意を強くする一方(やっと10年ぐらい前のGoogleに追いついただけとも言える)、アジャイルでない組織に導入してもメリットはあまりないだろうなとも思う。Kubernetes は単なるインフラというよりも、サービスがどのように開発・運用されるべきかという思想が強烈に埋め込まれた環境だと言った方が良いかもしれない。

Kubernetes によって、いわゆるデリバリーサイクルは極限まで短縮され、システムの柔軟性もかつてないレベルで実現出来るようになる。しかしその一方、運用やモニタリングにかかるコストは従来より高いように感じられるし、Kubernetes 自体も高速で進化していくので、そこにキャッチアップ出来る人材も確保する必要がある。例えば、運用において、MTTR (Mean-Time-To-Recovery) よりも、MTBF (Mean-Time-Between-Failures) の方を重視するような組織だと、なかなかこの機動性をメリットだと感じるのは難しいだろう。

既に開発が落ち着いていて安定運用されているサービスを Kubernetes に移行するメリットは、おそらくほとんどない。基本的には開発の初期から導入するのが望ましい。Cotoami はリスクのないオープンソース開発なので、その辺の事情で迷う必要がなく、一番始めのハリボテアプリの段階から Kubernetes で本番 ( https://cotoa.me/ ) の運用を始めて、自動デプロイなどの仕組みも整備出来た。

去年まで、ウチのチームでは、AMI (Amazon Machine Image) のように VM 上で動くホストがパッケージングの単位になっていた。パッケージを作るのも、パッケージをデプロイするのもうんざりするほど時間がかかる。それが Kubernetes/Docker によって、ホストの中で動く一つ一つのプロセスがパッケージングの単位となり、それらの小さな部品を組み合わせてホスト(Kubernetes では Pod と呼ばれている)を作り、それらをまた組み合わせて大きなWebサービスを作るという形に変わり、一度の更新も最小限で高速、しかも無停止ということで、新しい時代の到来を感じずにはいられない。

Kubernetes で実現する Phoenix/Elm アプリのホットデプロイ自動化完全詳解(2016年12月版)

今年の初頭に「Phoenixアプリのホットデプロイ完全自動化」の記事を書いてから一年が過ぎようとしている。この自動化は Elixir/Erlang の Hot swapping 機能を利用していて、git push から10分以内でデプロイが完了するという、当時としてはそこそこ満足のいく達成だったのだが、こんな不具合や、exrm (Elixir Release Manager) 作者の「hot upgrades はあんまりオススメ出来ない発言」などを見るにつけ、これを本番で使うのはちょっと辛いかもしれないと思うようになった。

今回、Cotoami プロジェクト を始めるに当たって、前々から気になっていた Google の Kubernetes(クバネテス)を試してみようと思い立った。そして実際に自動化の仕組みを構築してみて、その簡単さと仕組みの先進さに驚いた。言語に依存しないマイクロサービスのパッケージングと、それらを組み合わせて簡単にスケーラブルなWebサービスを構築できる仮想環境。これで本格的にコンテナの時代が来るんだなという新しい時代の訪れを感じずにはいられない。

というわけで、以下では Kubernetes を使った自動化の詳細について紹介したいと思う。この仕組みの全貌は Cotoami プロジェクトの一部として公開しているので、興味のある方は以下の GitHub プロジェクトを覗いて頂ければと思う。



 

Kubernetes とは何か?

Kubernetes が提供する仕組みは Container Orchestration と呼ばれている。Container Orchestration とは、Docker のようなコンテナ(アプリケーションを実行環境ごとパッケージングする仕組み)で実現されている小さなサービス(マイクロサービス)を組み合わせて、より大きなサービスを作るための仕組みである。

今では、Webサービスを複数のサービス(プロセス)の連携として実現することが当たり前になって来ている。次第に細かくなりつつあるこれらのサービスを扱う時の最大の障害が従来型の重い仮想化だ。例えば、Amazon Machine Images (AMI) のような従来型の仮想化技術を使ってサービスを更新する場合、イメージをビルドするのに20分から30分程度、更にそれを環境にデプロイするのに10分以上かかってしまう。自動化も容易ではない。サービスの数が多くなるほどに時間的なペナルティが積み重なってしまい、マイクロサービスのメリットを享受するのは難しくなる。なので、実際はマシンイメージをデプロイの単位にすることはせずに、言語やフレームワーク固有のパッケージに頼ったデプロイを行っている現場が多いのではないだろうか。

これらの問題を一挙に解決しようとするのが、Docker のような軽い仮想化と、それらをまるでソフトウェアモジュールのように組み合わせることを可能にする Container Orchestration 技術である。

 

Kubernetes を最短で試す

複数サービスの連携を、ローカルマシンで簡単に試せるというのも Kubernetes のようなツールの魅力だ。Kubernetes には Minikube というスグレモノのツールが用意されていて、ローカルマシン上に、お手軽に Kubernetes 環境を立ち上げることが出来る。

以下では、Mac OS X での手順を紹介する。

1. VirtualBox をインストールする

筆者の環境:

$ vboxmanage --version
5.1.8r111374

2. Minikube をインストールする

$ curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.12.2/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

$ minikube version
minikube version: v0.12.2

3. Kubernetes を操作するためのコマンドツール kubectl をインストールする

$ curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.3.0/bin/darwin/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/

4. Minikube を起動する

$ minikube start
Starting local Kubernetes cluster...
Kubectl is now configured to use the cluster.

以下のような情報を見れれば、準備は完了。

$ kubectl cluster-info
Kubernetes master is running at https://192.168.99.101:8443
KubeDNS is running at https://192.168.99.101:8443/api/v1/proxy/namespaces/kube-system/services/kube-dns
kubernetes-dashboard is running at https://192.168.99.101:8443/api/v1/proxy/namespaces/kube-system/services/kubernetes-dashboard

$ kubectl get nodes
NAME       STATUS    AGE
minikube   Ready     5d

5. サンプルプロジェクトをデプロイしてみる

Kubernetes には色んなサンプルプロジェクトが用意されているが、ここでは Guestbook という簡単なアプリを試してみる。

以下のファイル(guestbook-all-in-one.yaml)を適当な場所に保存して、

https://github.com/kubernetes/kubernetes/blob/master/examples/guestbook/all-in-one/guestbook-all-in-one.yaml

以下のコマンドを実行してデプロイする。

$ kubectl create -f guestbook-all-in-one.yaml 
service "redis-master" created
deployment "redis-master" created
service "redis-slave" created
deployment "redis-slave" created
service "frontend" created
deployment "frontend" created

これによって、以下の3つの Deployments と、

$ kubectl get deployments
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
frontend       3         3         3            3           5m
redis-master   1         1         1            1           5m
redis-slave    2         2         2            2           5m

それぞれの Deployments に対応する3つの Serviceskubernetesはシステムのサービスなので除く)が出来上がっていることが分かる。

$ kubectl get services
NAME           CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
frontend       10.0.0.118   <none>        80/TCP     7m
kubernetes     10.0.0.1     <none>        443/TCP    6d
redis-master   10.0.0.215   <none>        6379/TCP   7m
redis-slave    10.0.0.202   <none>        6379/TCP   7m

簡単に説明すると、Deployment は一つのマイクロサービスのクラスタに対応し、Service はそのクラスタへのアクセス手段を提供する。

たったこれだけの手順で、冗長化された Redis をバックエンドにした、アプリケーションの環境が出来上がってしまった。構成の全ては guestbook-all-in-one.yaml というテキストファイルに定義されている。

早速ブラウザでアクセスして試してみたいところだが、デフォルトの設定だとサービスが Kubernetes の外部には公開されていないので、frontendサービスの設定をちょっと書き換えて(guestbook-all-in-one.yaml に以下のような感じで type: NodePort の行を追加する)、

apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    app: guestbook
    tier: frontend
spec:
  ports:
  - port: 80
  type: NodePort
  selector:
    app: guestbook
    tier: frontend

以下のコマンドを実行して設定ファイルの更新を環境に適用する。

$ kubectl apply -f guestbook-all-in-one.yaml 

更新が完了したら、以下のコマンドでアプリケーションのURLを知ることが出来る。

$ minikube service frontend --url
http://192.168.99.101:31749

以下のようなページが表示されただろうか?

guestbook

6. お片づけ

先ほどのサンプルプロジェクトで作ったリソースは、以下のコマンドで全部削除出来る。

$ kubectl delete -f guestbook-all-in-one.yaml

Minikube の停止は以下。

$ minikube stop

 

Phoenix/Elm アプリの Docker イメージを作る

さて、Cotoami の話に戻ろう。Cotoami では、以下のような構成で自動化を実現しようとしている。

cotoami-auto-deploy

CircleCI 上のビルドで Docker イメージをビルドして Docker Hub にリリース。その後、AWS上に構築した Kubernetes に更新の命令を出して、新しいイメージでアプリケーションの Rolling Update(無停止デプロイ)を行う。

この仕組みを構築するためには、まず Phoenix/Elm アプリケーションを Docker でパッケージングするための Dockerfile を用意する必要がある。しかし、ここで気をつけなければならないのは、パッケージングそのものよりも、CircleCI 上でどうやって Phoenix/Elm アプリケーションをビルドするかという問題である。

Elixirアプリケーションは、クロスコンパイル・ビルドが出来るという説明もあるが、実行環境とビルド環境は合わせておいた方が良いというアドバイスもよく見かけるので、Cotoami ではよりトラブルが少なそうな、環境を合わせるアプローチを取ることにした。

今回の例では、実行環境も Docker 上になるので、まずビルド用の Docker イメージを用意しておき、それを使ってアプリケーションのコンパイルとテストを行い、その後、そのイメージをベースにしてアプリケーションをパッケージングするという、docker build の二段構え方式でビルドを実施する。

まずは、以下の Dockerfile で Phoenix/Elm アプリのビルド環境を作る。

一度作ったイメージは、CircleCI のキャッシュディレクトリに入れておき、後々のビルドで使い回せるようにしておく。この辺の設定は全て circle.yml に書く。

アプリケーションのコンパイルとテストが終わったら、ビルド用のイメージをベースにして、アプリケーションのパッケージングを行う。そのための Dockerfile が以下である。

これらの組み合わせで、git push する度に、Docker Hub にアプリケーションのイメージがリリースされるようになる(Docker Hub に docker push するために、CircleCI に 認証用の環境変数を設定しておくこと: DOCKER_EMAIL, DOCKER_USER, DOCKER_PASS)。

参考: Continuous Integration and Delivery with Docker – CircleCI

 

AWS上に Kubernetes 環境を作る

アプリケーションの Docker イメージが用意出来たら、それを動かすための Kubernetes 環境を作る。今回は AWS 上に Kubernetes 環境を構築することにした。

Kubernetes から kops という、これまた便利なツールが提供されていて、これを使うと簡単に環境を構築出来る。

1. kops のインストール

Mac OS の場合:

$ wget https://github.com/kubernetes/kops/releases/download/v1.4.1/kops-darwin-amd64
$ chmod +x kops-darwin-amd64
$ mv kops-darwin-amd64 /usr/local/bin/kops

2. Kubernetes 用のドメイン名を用意する

ここが比較的厄介なステップなのだが、kops による Kubernetes 環境はドメイン名を名前空間として利用する仕組みになっている。具体的には、Route 53 内に Kubernetes 環境用の Hosted zone を作る必要がある。

例えば、立ち上げようとしているWebサービスのドメインが example.com だとすれば、k8s.example.com のような専用の Hosted zone を用意する(k8s は Kubernetes の略称)。

Cotoami の場合、AWS のリソースは出来るだけ Terraform を利用して管理することにしているので、Terraform で Hosted zone を設定する際の例を以下に置いておく。

resource "aws_route53_zone" "main" {
  name = "example.com"
}

resource "aws_route53_zone" "k8s" {
  name = "k8s.example.com"
}

resource "aws_route53_record" "main_k8s_ns" {
  zone_id = "${aws_route53_zone.main.zone_id}"
  name = "k8s.example.com"
  type = "NS"
  ttl = "30"
  records = [
    "${aws_route53_zone.k8s.name_servers.0}",
    "${aws_route53_zone.k8s.name_servers.1}",
    "${aws_route53_zone.k8s.name_servers.2}",
    "${aws_route53_zone.k8s.name_servers.3}"
  ]
}

主ドメインとなる example.com の Hosted zone について、サブドメイン k8s の問い合わせを委譲するような NS レコードを登録しておくのが味噌。

以下のコマンドを叩いて、DNSの設定がうまく行っているかを確認する。

$ dig NS k8s.example.com

上で設定した4つの NS レコードが見えれば OK。

3. kops の設定を保存するための S3 bucket を作る

kops は、Amazon S3 上に保存された構成情報に基づいて環境の構築・更新などを行う。というわけで、予めそのための S3 bucket を作っておき、その場所を環境変数 KOPS_STATE_STORE に設定する。

$ aws s3 mb s3://kops-state.example.com
$ export KOPS_STATE_STORE=s3://kops-state.example.com

これで、準備は完了。いよいよ Kubernetes の環境を立ち上げる。

4. Kubernetes の設定を生成する

新しい環境の名前を staging.k8s.example.com として、以下のコマンドで新規環境の設定を生成する。生成された設定は先ほどの S3 bucket に保存される。

$ kops create cluster --ssh-public-key=/path/to/your-ssh-key.pub --zones=ap-northeast-1a,ap-northeast-1c staging.k8s.example.com

Kubernetes ノードにログインするための ssh キーや、ノードを展開する Availability Zone などを指定する。細かいオプションについては、以下を参照のこと。

デフォルトでは、以下のような構成の環境が立ち上がるようになっている。

  • master (m3.medium)
  • node (t2.medium * 2)

5. Kubernetes 環境を立ち上げる

Kubernetes 環境を AWS 上に立ち上げる。単純に以下のコマンドを実行すれば良いのだが、

$ kops update cluster staging.k8s.example.com --yes

Terraform の設定ファイルを生成するオプションもあるので、Cotoami ではその方法を取ることにした。

$ kops update cluster staging.k8s.example.com --target=terraform

$ cd out/terraform
$ terraform plan
$ terraform apply

生成されたデフォルトの構成から、セキュリティグループなどをより安全な設定にカスタマイズすることもあると思われるが、これらのファイルは自動生成によって更新される可能性があることに注意する必要がある。ファイルを直接編集すると、新しく生成したファイルに同じ変更を施すのを忘れてしまう可能性が高い。なので、AWS のコンソール上で直接カスタマイズした方が良いかもしれない(新しい設定ファイルとの齟齬は terraform plan の時に気づける)。

どのようなファイルが生成されるか興味のある方は、Cotoami のリポジトリを覗いてみて欲しい。

環境を立ち上げる過程で、kop によって kubectl の設定も自動的に追加されている。以下のコマンドを実行すれば、AWS上の環境に接続していることが確認できるはずだ。

$ kubectl cluster-info
Kubernetes master is running at https://api.staging.k8s.example.com
KubeDNS is running at https://api.staging.k8s.example.com/api/v1/proxy/namespaces/kube-system/services/kube-dns

 

Kubernetes 上にアプリケーションをデプロイする

Kubernetes の準備は整ったので、後はアプリケーションをデプロイするだけである。Minikube のところでサンプルアプリをデプロイしたのと同じように、サービスの構成情報を YAML ファイルに定義しておき、kubectl create コマンドでデプロイを行う。

Cotoami の構成ファイルは以下に置いてある。

$ kubectl create -f deployment.yaml
$ kubectl create -f service.yaml

設定ファイルの仕様については Kubernetes のサイトを参照して頂くとして、内容自体は単純だということはお分かり頂けると思う。deployment.yaml では、アプリケーションの Docker イメージ名やクラスタを構成するレプリカの数、ポート番号などが指定されている。service.yaml では、そのサービスを外部にどのように公開するかという設定がされており、面白いのは type: LoadBalancer と書いておくと、AWS の ELB が自動的に作成されてアプリケーションのエンドポイントになるところだろうか。

 

デプロイ自動化をビルド設定に組み込む

最初のデプロイが無事に成功すれば、無停止更新の仕組みは Kubernetes 上に用意されている。後はそれを利用するだけである。

CircleCI から Kubernetes にアクセスするためには、以下のような準備が必要になる。

  1. kubectl のインストール
  2. kubectl の設定
    • ensure-kubectl.sh では、環境変数 S3_KUBE_CONF に設定された Amazon S3 のパスから kubectl の設定ファイルをビルド環境にコピーする。
    • Kubernetes on AWS を構築する過程でローカルに出来上がった設定ファイル ~/.kube/config を S3 にコピーして、その場所を CircleCI の環境変数 S3_KUBE_CONF に設定する。
      • この設定ファイルには、Kubernetes にアクセスするための credential など、重要な情報が含まれているので、取り扱いには注意すること!
    • CircleCI 側から S3 にアクセスするためのユーザーを IAM で作成して最低限の権限を与え、その credential を CircleCI の AWS Permissions に設定する。

これらの設定が完了すれば、ビルド中に kubectl コマンドを呼び出せるようになる。Cotoami の場合は、circle.ymldeployment セクションに、以下の二行を追加するだけで自動デプロイが行われるようになった。

https://github.com/cotoami/cotoami/blob/auto-deployment/circle.yml

- ~/.kube/kubectl config use-context tokyo.k8s.cotoa.me
- ~/.kube/kubectl set image deployment/cotoami cotoami=cotoami/cotoami:$CIRCLE_SHA1

長くなってしまったが、以上が自動化の全貌である。