LoginSignup
28

More than 3 years have passed since last update.

kubernetesクラスタでRailsアプリを公開するチュートリアル

Posted at

はじめに

kubernetes(k8s) クラスタを構築するためのインフラがあり、kubectl コマンドで操作が出来る状態となっていることが前提です。

Rancher を使って構築する方法(勉強用ですが)について安いクラウド環境で RancherOS / Kubernetes を使って勉強用クラスタを作るも参照してみて下さい。

本記事について

色々な k8s の概念についての説明は本家ドキュメント、他記事や、本に書かれていることを参考にしてみて下さい。

この記事では概念の整理はせず、チュートリアル形式でアプリケーションを k8s で動作させることを目的として、なるべく 1 歩ずつ進めつつ概念理解が必要になったタイミングで説明していきます。

チュートリアルが終わった段階で k8s 上で特定のアプリケーションが公開できるようにするための関連知識を一通り理解できるようになることを目指します。

但し、チュートリアルではなるべく新しい概念や外部連携が少ない方法を選択するため、本番利用に適した設計とはなっていません。

アプリケーションはコンテナ化されていれば何でもよいですが、Rails アプリケーションを動作させることを目指してみます。

k8s 概要

k8s ではコンテナをどのように動作させるべきかを YAML 形式で定義します。
これをマニフェストと言います。

ここでの動作とは、コンテナの名前、コンテナイメージ、コンテナボリューム等の Docker コンテナでおなじみの設定に加えて、k8s システム内で何個起動するべきか、ラベル(負荷分散をするときのグループ名などとして使う)は何かなどを指します。

マニフェストという名前のとおり動作を宣言するものであり、起動してからどのようなコマンドを実行するといったシーケンスではありません。

コンテナを 1 つ起動する

k8s でコンテナを起動する最小単位は Pod です。
まずは Pod のマニフェストを記述して Rails アプリケーションを起動させることにします。

Pod は Workload リソースの 1 つです。
Workload リソースとは k8s におけるコンテナの動作を定義するリソースです。
Pod には 1 つ以上のコンテナが定義され、全てのコンテナは同一 IP アドレスを共有します。

Docker image をビルドする (RAILS_ENV=development)

まずは k8s コンテナとして起動する Rails アプリケーションの Docker image をビルドすることにします。

Rails アプリケーションは GitHub > rails-sample_app を使います。

RAILS_ENV=Production では S3 や SENDGRID 等の外部サービスを利用する設定を行う必要があるため、まずは RAILS_ENV=development で動作させて DB は sqlite を使うこととします。

FROM ruby:2.5.3

# install tools
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        postgresql-client \
        apt-transport-https \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# install yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update \
    && apt-get -y install yarn \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# install node 10.x
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN apt-get install -y nodejs \
    && apt-get clean

WORKDIR /usr/src/app
COPY Gemfile* ./
RUN bundle install --with development
COPY . .

EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

使い方は Rails アプリケーションの TOP (Gemfile, Gemfile.lock が存在するディレクトリ) と同じディレクトリに Dockerfile を設置して docker build を実行します。

Railsアプリケーション用Dockerイメージをビルドする
$ docker build -t ruby-app .
# 作成が完了したことを確認する
$ docker image ls | grep ruby-app
ruby-app                                       latest              93a74f4d19d9        7 seconds ago       1.19GB

Docker image が作成出来たら、k8s 上で起動する前に Docker run で正常に起動するか確認してみます。
尚、DB のマイグレーションを行うために entrypoint は上書きしています。

作成したRailsアプリケーションを起動する
$ docker run --rm -p 3000:3000 --entrypoint /bin/bash ruby-app \
    -c 'rails db:migrate && rails server -b 0.0.0.0'
# RailsアプリケーションのTOPにアクセスする
$ curl -I http://localhost:3000
# HTTP 200 OK が返ってくれば OK

Docker image をイメージリポジトリ (Docker Hub) に保存する

作成した Docker image を k8s から参照できる repository へ保存します。

今回は Docker hub へ保存することにします。

Dockerイメージを保存する
$ docker login # docker.ioにログインする
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: YOUR_ACCOUNT
Password: *****
WARNING! Your password will be stored unencrypted in /home/vagrant/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store


# Docker hub のアカウント(YOUR_ACCOUNT)とリポジトリ名(REPOSITORY_NAME)とタグ名(TAG_NAME)は適宜設定してください。
$ docker image tag ruby-app YOUR_ACCOUNT/REPOSITORY_NAME:TAG_NAME
  • Docker hub のアカウントが無ければ作成してから docker login すること
  • Repository を作成していなければ作成してから docker push すること

Rails アプリケーションを動作させる Pod リソースを作成する

Pod のマニフェストを作成していきます。

Pod に記述する内容は docker run で指定した内容とほぼ同じです。

sample-pod-ruby.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod-ruby
spec:
  containers:
    - name: rails-app
      image: ryu310/rails-sample_app:20190418
      command:
        - /bin/bash
      args:
        - -c
        - rails db:migrate && rails server -b 0.0.0.0

ファイルが作成出来たら k8s に Pod を作成します。

k8sへPodを適用する(applyを使うと、リソースが未作成であれば作成され、リソースが作成済であれば変更が反映される)
$ kubectl apply -f smaple-pod-ruby.yaml

適用されたらしばらくして Pod が Running の状態となります。
起動している Pod の一覧を表示するために kubectl get pods コマンドが使えますが、ログファイルにおける tail -f のように、Pod の状態に変化があったタイミングで変更内容が随時表示される watch(-w) オプションをつけて確認するとよいでしょう。

Podの一覧を表示する
$ kubectl get pods -w
NAME         READY   STATUS              RESTARTS   AGE
sample-pod-ruby   0/1     ContainerCreating   0          7s
sample-pod-ruby   1/1     Running             0          55s

Pod が持つ IP アドレス等の詳細情報を確認するためには kubectl describe を実行します。

Podの詳細を表示する
$ kubectl describe pod sample-pod-ruby
Name:               sample-pod-ruby
Namespace:          default
Priority:           0
PriorityClassName:  <none>
Node:               rancher-worker01/A.B.C.D
Start Time:         Tue, 02 Apr 2019 02:18:47 +0900
Labels:             <none>
Annotations:        cni.projectcalico.org/podIP: 10.42.1.57/32
                    kubectl.kubernetes.io/last-applied-configuration:
                      {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"sample-pod-ruby","namespace":"default"},"spec":{"containers":[{"args":["-c...
Status:             Running
IP:                 10.42.1.57
Containers:
  rails-app:
    Container ID:  docker://e84e4973a98c832f4d40be498c942c7d30390e568321e960c02a0639edf2c836
    Image:         YOUR_ACCOUNT/rails-sample_app:20190417
    Image ID:      docker-pullable://YOUR_ACCOUNT/rails-sample_app@sha256:ec734efc933de45469efd8094e854152a420d3c8c08d9aa77292b806baa46c7b
    Port:          <none>
    Host Port:     <none>
    Command:
      /bin/bash
    Args:
      -c
      rails db:migrate && rails server -b 0.0.0.0
    State:          Running
      Started:      Tue, 02 Apr 2019 02:18:47 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-zvct6 (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  default-token-zvct6:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-zvct6
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From                       Message
  ----    ------     ----  ----                       -------
  Normal  Scheduled  12m   default-scheduler          Successfully assigned default/sample-pod-ruby to rancher-worker01
  Normal  Pulled     12m   kubelet, rancher-worker01  Container image "YOUR_ACCOUNT/rails-sample_app:20190417" already present on machine
  Normal  Created    12m   kubelet, rancher-worker01  Created container
  Normal  Started    12m   kubelet, rancher-worker01  Started container

また、Pod で動作する Container のログを表示するには kubectl logs コマンドを実行する。

Pod内Containerのログを表示する
# ログを表示して終了する
$ kubectl logs sample-pod-ruby

# 継続してログを表示する(tail -f と同様の動作)
$ kubectl logs -f sample-pod-ruby

# 特定のコンテナのログを表示する
$ kubectl logs sample-pod-ruby -c rails-app

以上で Rails アプリケーションを Pod として起動する方法は終わりです。

尚、アプリケーションを停止する時は Pod を削除することになります。

Pod削除
$ kubectl delete pod sample-pod-ruby

Rails アプリケーションが起動したことを確認する

さて、起動した Rails アプリケーションに HTTP でアクセスしてみることにします。

しかし Pod が起動しただけでは Rails アプリケーションの 3000 番に対して k8s の外部からアクセスすることが出来ません。

そこで kubectl コマンドを実行しているホストの任意のポートを port forward し、Pod の 3000 番ポートへ接続して確認してみることにします。

ローカルホストの3000番をPodの3000番ポートにフォワードする
$ kubectl port-forward pod/sample-pod-ruby 3000:3000
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000
# プロンプトは返ってこない

上記を実行すると、Ctrl+C 等でコマンドを終了するまでポートフォワードが有効になります。

後はブラウザで http://localhost:3000/ にアクセスすれば下記のように Pod 上で動作する Rails アプリケーションを表示することが出来ます。

image.png

また、一時的に curl を実行するためだけの Pod を起動して動作確認することも出来ます。

curl の引数に渡す http://A.B.C.D:3000 は Pod の詳細情報の中に書かれた IP の値を入力してください。

Railsアプリケーションへcurlコマンドを実行するPodを一時的に作成する(実行が終わったら自動で削除(--rm)する)
$ kubectl run --generator=run-pod/v1 -it --rm curl-test --image=centos:6 -- curl -I http://10.42.1.57:3000
If you don't see a command prompt, try pressing enter.
Error attaching, falling back to logs: unable to upgrade connection: unable to read error from server response
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: text/html; charset=utf-8
ETag: W/"542b9c88b05fb8aee0dbdb502f0e69ba"
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: _rails_sample_app_session=L0hRTWVkd1Z4bjd3MThQK09BQmZqZ0ZZTG9ZWHVEbXZ5aHBNSEV2bHVEUVJIUlluU3Bxc2NSVTc4b0lMRWZ6QmNYODBtd1pKWmxEQ3dtWFJVMUg4SjBxbmdXV0ZVbkw4bndVTnUrc3RhUkZPRUxKUWRMUm51aTJteVIzazdReEtwSHB4YlhiSWlJR1BZaGowVXdOVllnPT0tLVQ0M2VYTE9vaWtYQVdhQ1ZIYmtMM2c9PQ%3D%3D--c330af50875366cad20ef6295375f8305e47edb1; path=/; HttpOnly
X-Request-Id: fe8218f3-0d5a-40a7-a2be-75e31db91b47
X-Runtime: 0.125827

pod "curl-test" deleted

上記のように HTTP 200 OK が応答されたら成功です。

DB(SQLite) ファイルを永続化する

先に紹介した方法では、Pod が停止(削除) されると DB ファイル(/tmp/development.sqlite3) が削除される問題があります。

実際、アプリケーションを起動してから Sign up し(メールは届かない)、sqlite3 コマンド等を使って DB 上でユーザが作成されたことを確認した後に Pod の再作成をしてみて下さい。ユーザが初期化されることが確認できると思います。

# 事前に Sign up で適当なユーザの作成を試みてください。

# Pod 内 container の bash を起動
$ kubectl exec -it sample-pod-ruby bash

# Pod 内 container で rails console を実行
root@sample-pod-ruby:/usr/src/app# bin/rails c
Running via Spring preloader in process 1250
Loading development environment (Rails 5.2.3)
irb(main):001:0> User.count
   (0.2ms)  SELECT COUNT(*) FROM "users"
=> 1  # ユーザが存在することを確認
irb(main):001:0> exit
root@sample-pod-ruby:/usr/src/app# exit

# Pod を再起動する
$ kubectl delete pods sample-pod-ruby
$ kubectl apply -f sample-pod-ruby.yaml

$ kubectl exec -it sample-pod-ruby bash
root@sample-pod-ruby:/usr/src/app# bin/rails c  
Running via Spring preloader in process 616
Loading development environment (Rails 5.2.3)
irb(main):001:0> User.count
   (0.1ms)  SELECT COUNT(*) FROM "users"
=> 0 # ユーザが存在しないことを確認

(前提) Dynamic Provisioning 環境を用意する

Pod が使用するファイルを永続化したい場合は PersistentVolumeClaim リソースを作成することになります。

PersistentVolumeClaim は Claim の名がつく通り永続化領域を要求する設定です。
あくまで要求であり、k8s 上ではこの要求に応じた PersistentVolume リソースが作成されます。
(自動で PersistentVolume リソースが作成される環境ではない場合は手動で作成する必要があります)

GKE を使っている場合、PersistentVolumeClaim リソースを作成すると自動で PersistentVolume として GCE の Persistent Disk が作成されます。
オンプレで k8s 環境を用意している場合は、快適な kubernetes オンプレミス環境を構築する(7. NFSサーバ&NFSクライアントセットアップ) 等を参考にして自動で PersistentVolume リソースが作成されるよう設定を行ってください。
本記事では PersistentVolumeClaim リソースを作成することにより、自動で PersistentVolume リソースが作成される Dynamic Provisioning 環境であることを前提とします。

PersistentVolumeClaim, PersistentVolume を作成する

まずは SQlite3 の DB ファイル保存先となるボリュームを作成します。
永続化されるボリュームは PersistentVolume と呼びます。

先に記載したとおり、PersistentVolumeClaim リソースを作成することにより自動で PersistentVolume リソースが作成されます。

本記事では NFS サーバを用意し、k8s クラスタ上に nfs-client-provisioner Pod をインストールした Dynamic Provisioning 環境を前提としています。

NFSサーバを指すStorageClassリソース
$ kubectl get storageclass
NAME                   PROVISIONER                                                 AGE
nfs-client (default)   cluster.local/unrealistic-cardinal-nfs-client-provisioner   37m
sample-pvc-rails.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-pvc-rails
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests: 
      storage: 1Gi
  storageClassName: nfs-client # Dynamic Provisioning 環境に応じて設定してください
PersistentVolumeClaimを作成する
# PersistentVolumeClaimを作成する
$ kubectl apply -f sample-pvc-rails.yaml
persistentvolumeclaim/sample-pvc-rails created

# 作成されたPersistentVolumeClaimを確認する
$ kubectl get persistentvolumeclaim
NAME               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
sample-pvc-rails   Bound    pvc-42326d92-6348-11e9-8ff7-92ffaff62273   1Gi        RWO            nfs-client     5s

# 作成されたPersistentVoluemを確認する
$ kubectl get persistentvolume
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                      STORAGECLASS   REASON   AGE
pvc-42326d92-6348-11e9-8ff7-92ffaff62273   1Gi        RWO            Delete           Bound    default/sample-pvc-rails   nfs-client              10s

上記のように、PersistentVolumeClaim にはリソースの名前(metadata.name) をはじめ、利用ポリシー(spec.accessModes)、保存容量と保存先となるストレージ(spec.storageClassName)を指定します。

  • spec.accessModes: ReadWriteOnce
    • 同時に 1 つの Pod (正確には 1 つの Node)からのアクセスのみ許可する
  • spec.resources.requests.storage: 1Gi
    • 1GB の容量を要求する
  • spec.storageClassName: nfs-client
    • ストレージは nfs-client を使う

以上で k8s クラスタから利用できる NFS サーバ上のボリュームが 1GB だけ作成されました。

この PersistentVolumeClaim や PersistentVolume は Pod を削除しても消えません。
(ボリュームが不要になった場合は kubectl delete コマンドにより PersistentVolumeClaim と PersistentVolume を削除します)
(本記事における nfs-client ストレージクラスでは、PersistentVolumeClaim リソースが削除されると対応する PersistentVolume も削除されます)

Rails アプリケーションを動作させる Pod リソースを作成する (PersistentVolumeClaim を利用する)

sample-pod-ruby-pvc.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod-ruby-pvc
spec:
  containers:
    - name: rails-app
      image: ryu310/rails-sample_app:20190418
      command:
        - /bin/bash
      args:
        - -c
        - rails db:migrate && rails server -b 0.0.0.0
      volumeMounts:
        - mountPath: "/tmp"
          name: db
  volumes:
    - name: db
      persistentVolumeClaim:
        claimName: sample-pvc-rails

先に作成した Pod リソースで記述した YAML から追加された項目は spec.containers[].volumeMounts と spec.volumes です。

spec.containers[].volumeMounts では Container 内の /tmp (sqlite3ファイルが保存される先) は PersistentVolume からマウントするよう設定しています。
そこでマウントする先となる PersistentVolume が先に作成した PersistentVolumeClaim 名 sample-pvc-rails で作成された pvc-42326d92-6348-11e9-8ff7-92ffaff62273 となります。

Podリソースを作成する(PersistentVolumeClaimを利用する)
$ kubectl apply -f sample-pod-ruby-pvc.yaml
pod/sample-pod-ruby-pvc created

Rails アプリケーションが利用するデータが永続化されたことを確認する

# 事前に Sign up で適当なユーザの作成を試みてください。

# Pod 内 container の bash を起動
$ kubectl exec -it sample-pod-ruby-pvc bash

# Pod 内 container で rails console を実行
root@sample-pod-ruby-pvc:/usr/src/app# bin/rails c
Running via Spring preloader in process 1250
Loading development environment (Rails 5.2.3)
irb(main):001:0> User.count
   (0.2ms)  SELECT COUNT(*) FROM "users"
=> 1  # ユーザが存在することを確認
irb(main):001:0> exit
root@sample-pod-ruby-pvc:/usr/src/app# exit

# Pod を再起動する
$ kubectl delete pods sample-pod-ruby-pvc
$ kubectl apply -f sample-pod-ruby-pvc.yaml

$ kubectl exec -it sample-pod-ruby-pvc bash
root@sample-pod-ruby-pvc:/usr/src/app# bin/rails c  
Running via Spring preloader in process 616
Loading development environment (Rails 5.2.3)
irb(main):001:0> User.count
   (0.1ms)  SELECT COUNT(*) FROM "users"
=> 1 # ユーザが存在する確認

もし NFS サーバを確認できるようであれば、ファイルが保存されていることを確認してみるのもよいでしょう。

NFSサーバに保存されたファイルを確認する
$ ls -al /srv/nfsroot/
total 12
drwxr-xr-x 3 root root 4096 Apr 20 17:42 .
drwxr-xr-x 3 root root 4096 Apr 15 23:35 ..
drwxrwxrwx 2 root root 4096 Apr 20 18:03 default-sample-pvc-rails-pvc-42326d92-6348-11e9-8ff7-92ffaff62273

$ ls -al /srv/nfsroot/default-sample-pvc-rails-pvc-42326d92-6348-11e9-8ff7-92ffaff62273/
total 72
drwxrwxrwx 2 root root  4096 Apr 20 18:03 .
drwxr-xr-x 3 root root  4096 Apr 20 17:42 ..
-rw-r--r-- 1 root root 61440 Apr 20 18:03 development.sqlite3

default-sample-pvc-rails-pvc-42326d92-6348-11e9-8ff7-92ffaff62273 のように、Namespace default と PersistentVolumeClaim 名 sample-pvc-rails と PersistentVolume 名 pvc-42326d92-6348-11e9-8ff7-92ffaff62273 をハイフンで繋げたディレクトリが作成されているようでした。

PostgreSQL を動作させる Pod リソースを作成する

これまでは、構成を簡易にするために SQLite を使い、1 つの Pod だけで Rails アプリケーションを動作させていましたが、DB をアプリケーションと別 Pod として動作させるようにしてみます。

DB の Pod を作成する方法も Rails アプリケーションの Pod と同様です。
また、当然ですが PersistentVolume を使って DB のデータは永続化します。

DB は冗長構成にするのが望ましいですが、まずは 1 つの DB だけを起動するように設定していきます。

先に PersistentVolumeClaim リソースを作成します。

sample-pvc-psql.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-pvc-psql
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: nfs-client
PostgreSQLのデータを保存するPersistentVolumeClaimリソースを作成する
$ kubectl apply -f sample-pvc-psql.yaml
persistentvolumeclaim/sample-pvc-psql created

$ kubectl get pvc
NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
sample-pvc-psql   Bound    pvc-0c9c2c3d-636c-11e9-8ff7-92ffaff62273   5Gi        RWO            nfs-client     16s
$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                     STORAGECLASS   REASON   AGE
pvc-0c9c2c3d-636c-11e9-8ff7-92ffaff62273   5Gi        RWO            Delete           Bound    default/sample-pvc-psql   nfs-client              7s

次に PostgreSQL を動作させる Pod リソースを作成します。
まずは永続ボリュームをもつ PostgreSQL を起動するだけの yaml を作成することにし、後に環境変数を与えて、アプリケーションで利用するためのユーザを作成することにします。

sample-pod-psql.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod-psql
spec:
  containers:
    - name: psql
      image: postgres:11
      volumeMounts:
        - mountPath: "/var/lib/postgresql/data"
          name: pgdata
  volumes:
    - name: pgdata
      persistentVolumeClaim:
        claimName: sample-pvc-psql

YAML に記載した内容はこれまでに紹介した内容のため理解できると思います。

PostgreSQLを動作させるPodリソースを作成する
$ kubectl apply -f sample-pod-psql.yaml
pod/sample-pod-psql created

$ kubectl get pods
NAME                                                           READY   STATUS    RESTARTS   AGE
sample-pod-psql                                                1/1     Running   0          60s

# PostgreSQL のデータを psql コマンドを使って確認する
$ kubectl exec -it sample-pod-psql bash
root@sample-pod-psql:/# df -h /var/lib/postgresql/data/
Filesystem                                                                                   Size  Used Avail Use% Mounted on
XXX.YY.ZZZ.AA:/srv/nfsroot/default-sample-pvc-psql-pvc-ad6f7f4d-636d-11e9-8ff7-92ffaff62273   78G  1.2G   77G   2% /var/lib/postgresql/data

root@sample-pod-psql:/# su - postgres
postgres@sample-pod-psql:~$ psql
psql (11.2 (Debian 11.2-1.pgdg90+1))
Type "help" for help.

postgres=# \du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

postgres=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges   
-----------+----------+----------+------------+------------+-----------------------
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
(3 rows)

postgres=#

さて、psql コマンドで確認したとおり、PostgreSQL の内容は初期設定のままなので postgres ユーザと postgres データベースが存在する状態です。

ここに Rails アプリケーションで利用するユーザを設定していくことにしましょう。
新しいユーザは作成せずに postgres ユーザを利用することにし、パスワードだけ設定しておくことにします。

  • ユーザ名: postgres (スーパーユーザ)
  • パスワード: rails-sample-password (適当に変更してください)

上記のとおり postgres ユーザにパスワードを設定することにしましょう。
また事前に Database を作成しておきます。

postgresユーザにパスワードを設定し
$ kubectl exec -it sample-pod-psql bash
root@sample-pod-psql:/# su - portgres
No passwd entry for user 'portgres'
root@sample-pod-psql:/# su - postgres
postgres@sample-pod-psql:~$ psql
psql (11.2 (Debian 11.2-1.pgdg90+1))
Type "help" for help.

postgres=# alter role postgres with password 'rails-sample-password';
ALTER ROLE
postgres=# create database rails_sample_app;
CREATE DATABASE
postgres=# \l
                                    List of databases
       Name       |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges   
------------------+----------+----------+------------+------------+-----------------------
 postgres         | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 rails_sample_app | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 template0        | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
                  |          |          |            |            | postgres=CTc/postgres
 template1        | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
                  |          |          |            |            | postgres=CTc/postgres
(4 rows)

Docker image をビルドする (RAILS_ENV=production)

Dockerfile
FROM ruby:2.5.3

# install tools
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        postgresql-client \
        apt-transport-https \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# install yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update \
    && apt-get -y install yarn \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# install node 10.x
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN apt-get install -y nodejs \
    && apt-get clean

WORKDIR /usr/src/app

ENV RAILS_ENV=production
COPY Gemfile* ./
RUN bundle install --with production --without development,test,deployment
COPY . .

EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

使い方は Rails アプリケーションの TOP (Gemfile, Gemfile.lock が存在するディレクトリ) と同じディレクトリに Dockerfile を設置して docker build を実行します。
※ 値が config/environments/production.rb は config.force_ssl = false でなければ修正してください

Railsアプリケーション用Dockerイメージをビルドする
$ docker build -t ruby-app .
# 作成が完了したことを確認する
$ docker image ls | grep ruby-app
ruby-app                                       latest              93a74f4d19d9        7 seconds ago       1.19GB

Docker image が作成出来たら、k8s 上で起動する前に Docker run で正常に起動するか確認してみます。

事前に PostgreSQL を起動しておき、link で接続できるようにしつつビルドしたイメージを起動します。
尚、本番環境で利用するための各環境変数を設定し、アセットのプリコンパイルと DB のマイグレーションを行うために entrypoint は上書きしています。

※ AWS S3 の Bucket を作成した上で、該当のバケットにアクセスできるよう S3_XXXX の値を設定してください
※ SendGrid を有効にした上で、該当のサービスを利用できるよう SENDGRID_XXXX の値を設定してください

作成したRailsアプリケーションの起動を確認する
$ docker run --name postgres -d \
  -e POSTGRES_PASSWORD=rails-sample-password \
  postgres:11
$ docker run --rm -p 3000:3000 \
  -e DATABASE_URL=postgres://postgres:rails-sample-password@postgres/rails_sample_app \
  -e S3_ACCESS_KEY=${INPUT_YOUR_VALUE} \
  -e S3_BUCKET=${INPUT_YOUR_VALUE} \
  -e S3_SECRET_KEY=${INPUT_YOUR_VALUE} \
  -e SENDGRID_PASSWORD=${INPUT_YOUR_VALUE} \
  -e SENDGRID_USERNAME=${INPUT_YOUR_VALUE} \
  -e SECRET_KEY_BASE=${INPUT_YOUR_VALUE} \
  -e RAILS_SERVE_STATIC_FILES=1 \
  --link postgres \
  --entrypoint /bin/bash \
  ruby-app \
  -c 'rails assets:precompile && rails db:migrate && rails server -b 0.0.0.0'
# RailsアプリケーションのTOPにアクセスする
$ curl -I http://localhost:3000
# HTTP 200 OK が返ってくれば OK

先と同様に Docker image をイメージリポジトリの Docker Hub にプッシュしてください。

Rails アプリケーションを動作させる Pod リソースを作成する

PostgreSQL と接続する Rails アプリケーションを動作させる Pod を作成します。

RAILS_ENV=production で動作させるために必要な環境変数はマニフェスト内の spec.containers[].env で指定できます。
(環境変数として設定する値は ConfigMap リソースとして管理する方が望ましく、アクセスキーやパスワードは Secret リソースとして管理する方が直指定するより望ましいです。今は簡易のため環境変数で直指定しますが後ほど修正します)

マニフェストに記述する内容は Docker run で実行した内容とほぼ同じです。

sample-pod-ruby-psql.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod-ruby-psql
spec:
  containers:
    - name: rails-app
      image: ryu310/rails-sample_app:20190420
      command:
        - /bin/bash
      args:
        - -c
        - rails assets:precompile && rails db:migrate && rails server -b 0.0.0.0
      env:
        - name: DATABASE_URL
          value: postgres://postgres:rails-sample-password@10.42.1.144/rails_sample_app 
        - name: S3_ACCESS_KEY
          value: ${INPUT_YOUR_VALUE} 
        - name: S3_BUCKET
          value: ${INPUT_YOUR_VALUE} 
        - name: S3_SECRET_KEY
          value: ${INPUT_YOUR_VALUE} 
        - name: SENDGRID_PASSWORD
          value: ${INPUT_YOUR_VALUE} 
        - name: SENDGRID_USERNAME
          value: ${INPUT_YOUR_VALUE} 
        - name: SECRET_KEY_BASE
          value: ${INPUT_YOUR_VALUE} 
        - name: RAILS_SERVE_STATIC_FILES
          value: 1

DATABASE_URL に書かれている IP アドレスは sample-pod-psql Pod の IP アドレスです。
Pod は Pod network に所属するため、IP アドレスを指定すれば Pod 同士で通信ができます。

下記コマンドで確認できます。

Pod名sample-pod-psqlのIPアドレスのみを表示する
$ kubectl get pod sample-pod-psql -o jsonpath="{.status.podIP}"
10.42.1.144

Rails アプリケーションが起動したことを確認する

さて、先と同様に Port forward を行い、起動した Rails アプリケーションに HTTP でアクセスしてみることにします。
画面が表示され、 RAILS_ENV=development で表示するように設定されたデバッグ情報が表示されていなければ成功です。

image.png

Rails アプリケーションから DB の Pod を発見する

先の Rails アプリケーションの Pod には PostgreSQL が動作する Pod の IP アドレスが直指定されていました。
これでは sample-pod-psql の IP アドレスが変わった場合に接続できなくなる問題があります。

クラスタ内のサービスディスカバリを行う Service リソースを使って IP アドレスが変わった場合にも接続できるように修正します。

sample-service-psql.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-service-psql
spec:
  selector:
    app: sample-psql
  ports:
    - protocol: TCP
      port: 5432

記述内容のポイントは spec.selector に書かれた app: sample-psql です。
この Service は Pod のラベルに app: sample-psql が含まれる Pod を発見し Service の EndPoint として認識し、このサービスに対する接続を EndPoint に受け渡します。

# Service を作成する
$ kubectl apply -f sample-service-psql.yaml 
service/sample-service-psql created

# Service を確認する
$ kubectl get service
NAME                  TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
kubernetes            ClusterIP   10.43.0.1     <none>        443/TCP    24d
sample-service-psql   ClusterIP   10.43.7.196   <none>        5432/TCP   3s

# Service の詳細を確認する
$ kubectl describe service sample-service-psql
Name:              sample-service-psql
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-service-psql","namespace":"default"},"spec":{"ports":[{"po...
Selector:          app=sample-psql
Type:              ClusterIP
IP:                10.43.233.109
Port:              <unset>  5432/TCP
TargetPort:        5432/TCP
Endpoints:         <none>
Session Affinity:  None
Events:            <none>

まだ Endpoint が作成されていません。
これは selector の条件に合致する Pod が存在しないためです。

先に作成した PostgreSQL を動作させる Pod にラベルとして app: sample-psql を指定し、Rails アプリケーションを動作させる Pod の DB 接続先を service に変えてみます。

sample-pod-psql.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod-psql
  labels:
    app: sample-psql
spec:
  containers:
    - name: psql
      image: postgres:11
      volumeMounts:
        - mountPath: "/var/lib/postgresql/data"
          name: pgdata
  volumes:
    - name: pgdata
      persistentVolumeClaim:
        claimName: sample-pvc-psql
sample-pod-ruby-psql.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod-ruby-psql
spec:
  containers:
    - name: rails-app
      image: ryu310/rails-sample_app:20190420
      command:
        - /bin/bash
      args:
        - -c
        - rails assets:precompile && rails db:migrate && rails server -b 0.0.0.0
      env:
        - name: DATABASE_URL
          value: postgres://postgres:rails-sample-password@sample-service-psql/rails_sample_app
        - name: S3_ACCESS_KEY
          value: ${INPUT_YOUR_VALUE} 
        - name: S3_BUCKET
          value: ${INPUT_YOUR_VALUE} 
        - name: S3_SECRET_KEY
          value: ${INPUT_YOUR_VALUE} 
        - name: SENDGRID_PASSWORD
          value: ${INPUT_YOUR_VALUE} 
        - name: SENDGRID_USERNAME
          value: ${INPUT_YOUR_VALUE} 
        - name: SECRET_KEY_BASE
          value: ${INPUT_YOUR_VALUE} 
        - name: RAILS_SERVE_STATIC_FILES
          value: 1

以上で sample-pod-psql の IP アドレスは直指定せずに済むようになりました。
sample-service-psql は k8s クラスタ内 DNS サーバにより名前解決が出来るため、IP アドレスが変わった場合にも対応することが出来ます。

# Endpoint の IP アドレスを確認する
$ kubectl describe service sample-service-psql 
Name:              sample-service-psql
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-service-psql","namespace":"default"},"spec":{"ports":[{"po...
Selector:          app=sample-psql
Type:              ClusterIP
IP:                10.43.100.113
Port:              <unset>  5432/TCP
TargetPort:        5432/TCP
Endpoints:         10.42.1.147:5432
Session Affinity:  None
Events:            <none>

# sample-pod-psql Pod を再作成する(IPアドレスを変える)
$ kubectl delete pod sample-pod-psql
pod "sample-pod-psql" deleted
$ 
$ kubectl apply -f sample-pod-psql.yaml 
pod/sample-pod-psql created

# Endpoint の IP アドレスを確認する
$ kubectl describe service sample-service-psql 
Name:              sample-service-psql
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-service-psql","namespace":"default"},"spec":{"ports":[{"po...
Selector:          app=sample-psql
Type:              ClusterIP
IP:                10.43.100.113
Port:              <unset>  5432/TCP
TargetPort:        5432/TCP
Endpoints:         10.42.1.152:5432
Session Affinity:  None
Events:            <none>

環境変数を Secret として保存する

環境変数が複数の Pod で使用する場合に値を指定する箇所は 1 つにまとまっていた方が、変更する時に楽です。そのような用途で ConfigMap が使われます。

また、ユーザ名やパスワード等の秘匿したい情報は Secret を使うことが推奨されています。

今回記述する中で RAILS_SERVE_STATIC_FILES の設定以外は Secret に登録してよいでしょう。
RAILS_SERVE_STATIC_FILES は Rails アプリケーションの動作に関する設定なので ConfigMap に纏めずに env で指定したままにしておくことにします。

Secret ファイルには KEYVALUE のセットを記述しますが、VALUE は BASE64 でエンコードする必要があります。
kubectl コマンドから Secret ファイルを作成する場合にはファイルや引数を与えれば BASE64 でエンコードできる --from-file--from-literal オプションがあります。

Secretファイルを作成する
# Secretファイルを作成する
$ kubectl create secret generic --save-config sample-secret \
  --from-literal=DATABASE_URL=postgres://postgres:rails-sample-password@sample-service-psql/rails_sample_app \
  --from-literal=S3_ACCESS_KEY=${INPUT_YOUR_VALUE} \
  --from-literal=S3_BUCKET=${INPUT_YOUR_VALUE} \
  --from-literal=S3_SECRET_KEY=${INPUT_YOUR_VALUE} \
  --from-literal=SENDGRID_PASSWORD=${INPUT_YOUR_VALUE} \
  --from-literal=SENDGRID_USERNAME=${INPUT_YOUR_VALUE} \
  --from-literal=SECRET_KEY_BASE=${INPUT_YOUR_VALUE}
secret/sample-secret created

# 作成した Secret を確認する
$ kubectl describe secret sample-secret
Name:         sample-secret
Namespace:    default
Labels:       <none>
Annotations:  
Type:         Opaque

Data
====
SENDGRID_PASSWORD:  12 bytes
SENDGRID_USERNAME:  23 bytes
DATABASE_URL:       78 bytes
S3_ACCESS_KEY:      20 bytes
S3_BUCKET:          10 bytes
S3_SECRET_KEY:      40 bytes
SECRET_KEY_BASE:    128 bytes

尚、BASE64 でエンコードされた文字列はデコード(復元)できますので、外部に流出しないよう気を付けて下さい。

sample-pod-ruby-psql.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod-ruby-psql
spec:
  containers:
    - name: rails-app
      image: ryu310/rails-sample_app:20190420
      command:
        - /bin/bash
      args:
        - -c
        - rails assets:precompile && rails db:migrate && rails server -b 0.0.0.0
      env:
        - name: RAILS_SERVE_STATIC_FILES
          value: '1'
      envFrom:
        - secretRef:
            name: sample-secret

このように Secret を全て参照する場合に spec.containers[].envFrom.secretRef でリソース名を指定することが出来ます。

尚、Secret の中の特定の値だけを利用する場合は spec.containers[].env[].valueFrom.secretKeyRef で key を指定することが出来ます。

修正した Pod を再作成すれば正常に起動することからも Secret が読み込まれていることが分かると思います。
Container で env コマンドを実行して Secret で指定した値が環境変数として設定されていることを確認するのもよいでしょう。

Rails アプリケーションを冗長化する

これまでのアプリケーションは 1 つの Container で動作しており耐障害性はありません。
k8s では複数の Pod をまとめて管理するリソースとして ReplicaSet があり、ReplicaSet にリリースの概念を追加した Deployment があります。

アプリケーション起動時の初期化処理を Job で実行する

rails db:migration は並列で実行することが出来ません。
Pod を複数用意した場合にこれらが実行されると Pod が Running にならずに起動失敗してしまいます。
(ReplicaSet や Deployment を使う場合は Pod が再起動されるので大きな問題にはなりません)

Pod 起動時に指定している rails db:migrate を行うリソースを作成していくことにします。

Rails アプリケーションを動作させる Pod を起動する前に Job によりマイグレーション処理を行うことになります。

sample-job-migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: sample-job-migrate
spec:
  completions: 1
  parallelism: 1
  template:
    spec:
      containers:
        - name: rails-app
          image: ryu310/rails-sample_app:20190420
          command:
            - /bin/bash
          args:
            - -c
            - rails db:migrate
          envFrom:
            - secretRef:
                name: sample-secret
      restartPolicy: Never

Job のマニフェストには spec.template 配下に Pod のマニフェストの spec を書きます。

Pod に書かれていた spec.containers[].args から rails db:migrate を実行する処理を切り出しています。

spec.completions は成功回数を指定します。マイグレーション処理は1回成功すればよいので1を指定しています。(デフォルトは1なので敢えて指定する必要はありません)

spec.parallelism は並列処理数を指定します。マイグレーション処理は並列処理が出来ないため1を指定しています。(デフォルトは1なので敢えて指定する必要はありません)

spec.template.restartPolicy は Job の処理が失敗した時に、Pod を再利用(再起動) するかどうか指定します。Never では再起動せずに新規 Pod を立ち上げます。

マイグレーション処理を行うJobリソースを作成する
# Job リソースを作成する
$ kubectl apply -f sample-job-migrate.yaml 
job.batch/sample-job-migrate created

# Job リソースを確認する(処理が完了していない場合)
$ kubectl get jobs
NAME                 COMPLETIONS   DURATION   AGE
sample-job-migrate   0/1           5s         5s

# Job リソースを確認する(処理が完了している場合)
$ kubectl get jobs
NAME                 COMPLETIONS   DURATION   AGE
sample-job-migrate   1/1           6s         10s

これでマイグレーション処理は Job で実施する方針となったので Rails アプリケーションを実行する Pod の args からはマイグレーション処理が不要になりました。

sample-pod-ruby-psql.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod-ruby-psql
spec:
  containers:
    - name: rails-app
      image: ryu310/rails-sample_app:20190420
      command:
        - /bin/bash
      args:
        - -c
        - rails assets:precompile && rails server -b 0.0.0.0
      env:
        - name: RAILS_SERVE_STATIC_FILES
          value: '1'
      envFrom:
        - secretRef:
            name: sample-secret

ReplicaSet により Rails アプリケーションを起動する Pod を冗長化する

Pod を複数起動するためのリソースとして ReplicaSet があります。

ReplicaSet はマニフェストとして Pod のマニフェストを spec.template として指定し、その Pod が期待した数だけ起動している状態を維持するリソースです。

sample-replicaset-ruby.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: sample-replicaset-ruby
spec:
  replicas: 2
  selector:
    matchLabels:
      app: rails-app
  template:
    metadata:
      labels:
        app: rails-app
    spec:
      containers:
        - name: rails-app
          image: ryu310/rails-sample_app:20190420
          command:
            - /bin/bash
          args:
            - -c
            - rails assets:precompile && rails server -b 0.0.0.0
          env:
            - name: RAILS_SERVE_STATIC_FILES
              value: '1'
          envFrom:
            - secretRef:
                name: sample-secret

ReplicaSet マニフェストの spec.template 配下には起動する Pod の spec を書きます。

起動する Pod の数は spec.replicas で指定します。クラスタ内でこの数だけ Pod が起動している状態を維持しようとされます。

Pod の数は spec.selector の条件に一致するものが対象になります。
spec.selector.matchLabelsapp: rails-app と書いた場合は、Pod のラベルに app=rails-app が書かれた Pod であれば条件に一致することになります。
ReplicaSet のマニフェストで記載した Pod が対象となるよう、 spec.template.metadata.labelsapp: rails-app を指定しています。

ReplicaSet を作成するには、これまでと同様 kubectl apply コマンドを使います。

ReplicaSetリソースを作成する
# ReplicaSet を作成する
$ kubectl apply -f sample-replicaset-ruby.yaml 
replicaset.apps/sample-replicaset-ruby created

# ReplicaSet リソースを確認する
$ kubectl get replicaset
NAME                                                     DESIRED   CURRENT   READY   AGE
sample-replicaset-ruby                                   2         2         2       24s

# Pod リソースを確認する
$ kubectl get pods
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-job-migrate-sfdfn                                       0/1     Completed   0          25m
sample-pod-psql                                                1/1     Running     0          12h
sample-replicaset-ruby-mjzv6                                   1/1     Running     0          5s
sample-replicaset-ruby-rzhns                                   1/1     Running     0          5s

kubectl get replicaset の結果から、ReplicaSet が起動する必要のある Pod 数が DESIRED として表示され、現状で起動している Pod 数が CURRENT として表示されていることが分かります。

kubectl get pods の結果を見ると ReplicaSet の名前の末尾にランダムな文字列が追加された Pod が 2 つ起動していることが分かります。これらが ReplicaSet により起動された Pod です。

ここで Pod を削除してみると、自動で新しい Pod が作成されることが分かります。

Podを削除すると新しいPodが作成されることを確認する
$ kubectl get pods
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-job-migrate-sfdfn                                       0/1     Completed   0          55m
sample-pod-psql                                                1/1     Running     0          12h
sample-replicaset-ruby-78g9b                                   1/1     Running     0          2m15s
sample-replicaset-ruby-mjlsf                                   1/1     Running     0          2m15s

$ kubectl delete pod sample-replicaset-ruby-mjlsf
pod "sample-replicaset-ruby-mjlsf" deleted

$ kubectl get pods
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-job-migrate-sfdfn                                       0/1     Completed   0          58m
sample-pod-psql                                                1/1     Running     0          12h
sample-replicaset-ruby-78g9b                                   1/1     Running     0          5m47s
sample-replicaset-ruby-7kwqw                                   1/1     Running     0          18s

Deployment により冗長化された Rails アプリケーションを起動する Pod をリリース管理する

ReplicaSet により Pod の冗長化が出来ました。
しかし ReplicaSet は Pod の起動数が期待どおりとなるよう維持してくれますが、Rails アプリケーションを更新してリリースする時など、Rails アプリケーションのコンテナイメージが新しくしたい場合に、全ての Pod に新しいコンテナイメージが適用されるように Pod の再起動をしたりしません。

試しに、ReplicaSet で管理する Rails アプリケーション Pod のコンテナイメージを更新してみます。
コンテナイメージは ryu310/rails-sample_app:20190420 から ryu310/rails-sample_app:20190421 に変更します。

# ReplicaSet が管理する Rails アプリケーション Pod のコンテナイメージを更新する
$ kubectl set image replicaset sample-replicaset-ruby rails-app=ryu310/rails-sample_app:20190421
replicaset.extensions/sample-replicaset-ruby image updated

# ReplicaSet リソースや、Pod の AGE に変化はないこと、ReplicaSet のコンテナイメージが変わっていることを確認する
$ kubectl get replicaset
NAME                                                     DESIRED   CURRENT   READY   AGE
sample-replicaset-ruby                                   2         2         2       36m
$ kubectl get pods
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-job-migrate-sfdfn                                       0/1     Completed   0          89m
sample-pod-psql                                                1/1     Running     0          13h
sample-replicaset-ruby-78g9b                                   1/1     Running     0          36m
sample-replicaset-ruby-7kwqw                                   1/1     Running     0          31m
$ kubectl describe replicaset sample-replicaset-ruby
Name:         sample-replicaset-ruby
Namespace:    default
Selector:     app=rails-app
Labels:       app=rails-app
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"apps/v1","kind":"ReplicaSet","metadata":{"annotations":{},"name":"sample-replicaset-ruby","namespace":"default"},"spec":{"r...
Replicas:     2 current / 2 desired
Pods Status:  2 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:  app=rails-app
  Containers:
   rails-app:
    Image:      ryu310/rails-sample_app:20190421  ★コンテナイメージが更新されている
  : <snip>
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
  Normal  SuccessfulCreate  38m   replicaset-controller  Created pod: sample-replicaset-ruby-78g9b
  Normal  SuccessfulCreate  38m   replicaset-controller  Created pod: sample-replicaset-ruby-mjlsf
  Normal  SuccessfulCreate  32m   replicaset-controller  Created pod: sample-replicaset-ruby-7kwqw

# Pod の image が ReplicaSet で指定したものと違うことを確認する
$ kubectl describe pod sample-replicaset-ruby-78g9b
Name:               sample-replicaset-ruby-78g9b
Namespace:          default
Priority:           0
PriorityClassName:  <none>
Node:               rancher-worker01/XXX.YY.ZZZ.AA
Start Time:         Sun, 21 Apr 2019 16:18:11 +0900
Labels:             app=rails-app
Annotations:        cni.projectcalico.org/podIP: 10.42.1.173/32
Status:             Running
IP:                 10.42.1.173
Controlled By:      ReplicaSet/sample-replicaset-ruby
Containers:
  rails-app:
    Container ID:  docker://5d7003e42764b0696086152038151022293e867bcc5d789cfc4d9a26ae29330b
    Image:         ryu310/rails-sample_app:20190420  ★コンテナイメージが ReplicaSet で指定したものと違う
    Image ID:      docker-pullable://ryu310/rails-sample_app@sha256:0f0b6495c084cadf675658c1d3c7b07947cb12e26fa2d90098a4882e420b13be
  : <snip>

# Pod を削除して強制再起動する
$ kubectl delete pod sample-replicaset-ruby-78g9b
pod "sample-replicaset-ruby-78g9b" deleted
$ kubectl get pods
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-job-migrate-sfdfn                                       0/1     Completed   0          92m
sample-pod-psql                                                1/1     Running     0          13h
sample-replicaset-ruby-7kwqw                                   1/1     Running     0          34m
sample-replicaset-ruby-txtsg                                   1/1     Running     0          11s
unrealistic-cardinal-nfs-client-provisioner-69859fb545-lnn4w   1/1     Running     0          23h

# Pod のコンテナイメージが更新されたことを確認する
$ kubectl describe pod sample-replicaset-ruby-txtsg
Name:               sample-replicaset-ruby-txtsg
Namespace:          default
Priority:           0
PriorityClassName:  <none>
Node:               rancher-worker01/XXX.YY.ZZZ.AA
Start Time:         Sun, 21 Apr 2019 16:57:43 +0900
Labels:             app=rails-app
Annotations:        cni.projectcalico.org/podIP: 10.42.1.175/32
Status:             Running
IP:                 10.42.1.175
Controlled By:      ReplicaSet/sample-replicaset-ruby
Containers:
  rails-app:
    Container ID:  docker://e5131278a3263e380ba6c163b2f6295d547a91c7bb14bc788eabfa3a5419cb51
    Image:         ryu310/rails-sample_app:20190421
    Image ID:      docker-pullable://ryu310/rails-sample_app@sha256:99e2b13498a9c3d798ff46b183e5272e2d7d3dfef87c7233211ecdc7c3e800d9
  : <snip>

これは ReplicaSet では Pod のリリースを管理するための仕組みがないためです。
コンテナイメージを最新化したい場合や、新しいイメージが正常に動作する時だけ Pod を更新したい場合、古いバージョンにロールバックしたい場合など、Pod のバージョン管理を行いたい場合は Deployment リソースを使います。

sample-deployment-ruby.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment-ruby
spec:
  replicas: 2
  selector:
    matchLabels:
      app: rails-app
  template:
    metadata:
      labels:
        app: rails-app
    spec:
      containers:
        - name: rails-app
          image: ryu310/rails-sample_app:20190420
          command:
            - /bin/bash
          args:
            - -c
            - rails assets:precompile && rails server -b 0.0.0.0
          env:
            - name: RAILS_SERVE_STATIC_FILES
              value: '1'
          envFrom:
            - secretRef:
                name: sample-secret

Deployment マニフェストに記載する内容は ReplicaSet と同様です。
kind, metadata 以外は同じ内容を記述してみました。

Deployment を作成してコンテナイメージを更新してみることにします。

# Deployment リソースを作成する
$ kubectl apply -f sample-deployment-ruby.yaml 
deployment.apps/sample-deployment-ruby created

# Deployment のイメージを更新する
$ kubectl set image deployment sample-deployment-ruby rails-app=ryu310/rails-sample_app:20190421
deployment.extensions/sample-deployment-ruby image updated

# Deployment の AGE には変化が無いが、Pod の AGE に変化があることを確認する
$ kubectl get deployment
NAME                                          READY   UP-TO-DATE   AVAILABLE   AGE
sample-deployment-ruby                        2/2     2            2           20m
$ kubectl get pod
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-deployment-ruby-6dd8b54c8-bf26t                         1/1     Running     0          28s
sample-deployment-ruby-6dd8b54c8-btcqn                         1/1     Running     0          27s
sample-job-migrate-sfdfn                                       0/1     Completed   0          140m
sample-pod-psql                                                1/1     Running     0          13h
unrealistic-cardinal-nfs-client-provisioner-69859fb545-lnn4w   1/1     Running     0          24h

# Pod のコンテナイメージが更新されていることを確認する
$ kubectl get pod sample-deployment-ruby-6dd8b54c8-bf26t -o 'jsonpath={.spec.containers[0].image}'
ryu310/rails-sample_app:20190421

Deployment を使った場合、set image を使ってコンテナイメージを更新すると、既に起動している Rails アプリケーション用 Pod のコンテナイメージも更新されていることが分かります。

Deployment ではマニフェストに更新があった場合に、更新された内容を反映させる方法を spec.storategy で指定できます。デフォルトでは spec.storagety.type: RollingUpdate となります。

$ kubectl get deployment sample-deployment-ruby -o yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "2"
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"sample-deployment-ruby","namespace":"default"},"spec":{"replicas":2,"selector":{"matchLabels":{"app":"rails-app"}},"template":{"metadata":{"labels":{"app":"rails-app"}},"spec":{"containers":[{"args":["-c","rails assets:precompile \u0026\u0026 rails server -b 0.0.0.0"],"command":["/bin/bash"],"env":[{"name":"RAILS_SERVE_STATIC_FILES","value":"1"}],"envFrom":[{"secretRef":{"name":"sample-secret"}}],"image":"ryu310/rails-sample_app:20190420","name":"rails-app"}]}}}}
  creationTimestamp: "2019-04-21T08:24:30Z"
  generation: 2 # 世代番号
  labels:
    app: rails-app
  name: sample-deployment-ruby
  namespace: default
  resourceVersion: "3117677"
  selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/sample-deployment-ruby
  uid: e2635f32-640e-11e9-8ff7-92ffaff62273
spec:
  progressDeadlineSeconds: 600
  replicas: 2
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: rails-app
  strategy:
    rollingUpdate:
      maxSurge: 25%       # アップデート中に許容する超過Pod数(%の場合はreplicas数からの比率)
      maxUnavailable: 25% # アップデート中に許容する不足Pod数(%の場合はreplicas数からの比率)
    type: RollingUpdate   # RollingUpdate方式でアップデートする
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: rails-app
    spec:
      containers:
      - args:
        - -c
        - rails assets:precompile && rails server -b 0.0.0.0
        command:
        - /bin/bash
        env:
        - name: RAILS_SERVE_STATIC_FILES
          value: "1"
        envFrom:
        - secretRef:
            name: sample-secret
        image: ryu310/rails-sample_app:20190421
        imagePullPolicy: IfNotPresent
        name: rails-app
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status:
  availableReplicas: 2
  conditions:
  - lastTransitionTime: "2019-04-21T08:24:32Z"
    lastUpdateTime: "2019-04-21T08:24:32Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  - lastTransitionTime: "2019-04-21T08:24:30Z"
    lastUpdateTime: "2019-04-21T08:45:04Z"
    message: ReplicaSet "sample-deployment-ruby-6dd8b54c8" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
  observedGeneration: 2
  readyReplicas: 2
  replicas: 2
  updatedReplicas: 2

Deployment リソースは ReplicaSet を世代管理しています。
metadata.generation が現状の世代を表す番号です。
(Deploymentを新規作成した時点の generation が 1 であり、コンテナイメージを更新したため generation が 2 になっています)

Deployment はコンテナイメージの更新等、spec.template 配下の情報が更新された場合に ReplicaSet が更新されたと判断して更新処理を行います。

RollingUpdate 方式の場合、新しい ReplicaSet を用意してその中に maxSurge で許容される数だけ Pod を作成して正常に Running 状態となったら順序古い ReplicaSet の Pod を停止して最終的に新しい世代の ReplicaSet とその内容に応じた Pod となるように更新されます。

そのため、仮に kubectl set image コマンドで存在しないコンテナイメージが指定された場合は新しい ReplicaSet が用意できずに更新処理が行われません。つまり最低限起動する Pod である場合だけ更新処理を行うよう管理してくれます。

存在しないコンテナイメージを指定した場合は RollingUpdate は失敗して Pod 再起動が発生しないことを確認してみましょう。

# 存在しないコンテナイメージを設定して Deployment を更新する
$ kubectl set image deployment sample-deployment-ruby rails-app=ryu310/rails-sample_app:fault-tag
deployment.extensions/sample-deployment-ruby image updated

# 更新処理のステータスを確認する(処理が完了したり中断されるまでプロンプトが返らない)
$ kubectl rollout status deployment sample-deployment-ruby
Waiting for deployment "sample-deployment-ruby" rollout to finish: 1 out of 2 new replicas have been updated...

# 既に起動していた Pod の AGE が更新されていないことを確認する
$ kubectl get pods
NAME                                                           READY   STATUS             RESTARTS   AGE
sample-deployment-ruby-59c766c7c4-ts6cr                        0/1     ImagePullBackOff   0          12m
sample-deployment-ruby-6dd8b54c8-bf26t                         1/1     Running            0          39m
sample-deployment-ruby-6dd8b54c8-btcqn                         1/1     Running            0          39m
sample-job-migrate-sfdfn                                       0/1     Completed          0          178m
sample-pod-psql                                                1/1     Running            0          14h

参考までに、Deployment の Reivision は kubectl rollout history で確認できます。
Deployment の更新が失敗した場合は kubectl rollout undo で元に戻せます。

$ kubectl rollout history deployment sample-deployment-ruby
deployment.extensions/sample-deployment-ruby 
REVISION  CHANGE-CAUSE
1         <none> # コマンドを実行する際に --record true を指定するとコマンドが表示されます。
2         <none>
3         <none>

$ kubectl rollout undo deployment sample-deployment-ruby
deployment.extensions/sample-deployment-ruby rolled back

$ kubectl rollout history deployment sample-deployment-ruby
deployment.extensions/sample-deployment-ruby 
REVISION  CHANGE-CAUSE
1         <none>
3         <none>
4         <none> # REVISION 2 に元戻しされたが REVISION は 4 に採番され直されている

ReplicaSet を使って PostgreSQL を動作させる Pod を冗長化する

PostgreSQL を動作させる Pod を冗長化させていきます。
Rails アプリケーションの Pod を冗長化させた時と同様にまずは ReplicaSet を使って冗長化させます。

sample-replicaset-psql.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: sample-replicaset-psql
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sample-psql
  template:
    metadata:
      labels:
        app: sample-psql  # Service の selector で指定した値と一致させる
    spec:
      containers:
        - name: psql
          image: postgres:11
          volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: pgdata
      volumes:
        - name: pgdata
          persistentVolumeClaim:
            claimName: sample-pvc-psql
# ReplicaSet を作成する
$ kubectl apply -f sample-replicaset-psql.yaml 
replicaset.apps/sample-replicaset-psql created

# Pod が複数作成されたことを確認する
$ kubectl get pods
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-deployment-ruby-6dd8b54c8-bf26t                         1/1     Running     0          79m
sample-deployment-ruby-6dd8b54c8-btcqn                         1/1     Running     0          79m
sample-job-migrate-sfdfn                                       0/1     Completed   0          3h39m
sample-replicaset-psql-2qz7q                                   1/1     Running     0          18s
sample-replicaset-psql-6dlrx                                   1/1     Running     0          18s

# Service の Endpoints に 2 つの Pod が認識されていることを確認する
$ kubectl describe service sample-service-psql
Name:              sample-service-psql
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-service-psql","namespace":"default"},"spec":{"ports":[{"po...
Selector:          app=sample-psql
Type:              ClusterIP
IP:                10.43.100.113
Port:              <unset>  5432/TCP
TargetPort:        5432/TCP
Endpoints:         10.42.1.190:5432,10.42.1.191:5432
Session Affinity:  None
Events:            <none>

StatefulSet を使って冗長化した PostgreSQL Pod を管理する

ReplicaSet を使った PostgreSQL Pod は同じ PersistentVolumeClaim を利用します。
つまり PostgreSQL インスタンスが 2 つでデータは共用しているといういびつな状態です。

冗長化させたい場合は PostgreSQL のレプリケーションを作るのがよいでしょう。

k8s で永続化したデータを取り扱うようなステートフルリソースを管理するために StatefulSet リソースが使えます。

StatefulSet は複数の Pod を管理し、Pod が使う PersistentVolumeClaim も template として管理します。

PostgreSQL のレプリケーションを作る際、bitnami/posgresql を使うのが簡単な用でしたので、利用することにします。
README.md に書かれているとおり、 POSTGRESQL_REPLICATION_MODE=master 等の環境変数を設定することでレプリケーションの設定が出来ます。

README.md に書かれている参考の設定を使って master と slave それぞれの StatefulSet リソースを作成していきます。
まずは事前に Secret を事前に作成しておきます。
(DB のユーザ名は postgres, パスワードは rails-sample-password です※再掲)

# Secret を作成する
$ kubectl create secret generic sample-secret-psql \
  --from-literal=POSTGRESQL_REPLICATION_USER=repl_user \
  --from-literal=POSTGRESQL_REPLICATION_PASSWORD=repl_password \
  --from-literal=POSTGRESQL_USERNAME=postgres \
  --from-literal=POSTGRESQL_PASSWORD=rails-sample-password

# Secret が作成されたことを確認する(sample-secret-psqlが追加されている)
$ kubectl get secret
NAME                                                      TYPE                                  DATA   AGE
default-token-zvct6                                       kubernetes.io/service-account-token   3      24d
sample-secret                                             Opaque                                7      19h
sample-secret-psql                                        Opaque                                4      34m

PosqgreSQL の master 用の StatefulSet を作成します。

  • 起動時に database rails_sample_app を作成する
  • PosqgreSQL レプリケーションの master として起動する
  • postgres ユーザアカウントと、レプリケーション用ユーザのアカウントは Secret から参照する

※ 環境変数は https://github.com/bitnami/bitnami-docker-postgresql#step-1-create-the-replication-master を参考にします。

sample-statefulset-psql-master.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sample-statefulset-psql-master
spec:
  serviceName: sample-statefulset-psql-master
  replicas: 1
  selector:
    matchLabels:
      app: sample-psql
      role: master
  template:
    metadata:
      labels:
        app: sample-psql
        role: master
    spec:
      containers:
        - name: psql
          image: bitnami/postgresql:11
          volumeMounts:
            - name: pgdata
              mountPath: /bitnami
          env:
            - name: POSTGRESQL_REPLICATION_MODE
              value: master
            - name: POSTGRESQL_DATABASE
              value: rails_sample_app
          envFrom:
            - secretRef:
                name: sample-secret-psql
  volumeClaimTemplates:
    - metadata:
        name: pgdata
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 5Gi
        storageClassName: nfs-client

次に PostgreSQL の slave 用の StatefulSet を作成します。

  • master の IP アドレスは先に作成した Service sample-service-psql を使う
  • PosqgreSQL レプリケーションの slave として起動する
  • レプリケーション用ユーザのアカウントは Secret から参照する

※ 環境変数は https://github.com/bitnami/bitnami-docker-postgresql#step-2-create-the-replication-slave を参照する

sample-statefulset-psql-slave.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sample-statefulset-psql-slave
spec:
  serviceName: sample-statefulset-psql-slave
  replicas: 1
  selector:
    matchLabels:
      app: sample-psql
      role: slave
  template:
    metadata:
      labels:
        app: sample-psql
        role: slave
    spec:
      containers:
        - name: psql
          image: bitnami/postgresql:11
          volumeMounts:
            - name: pgdata
              mountPath: /bitnami
          env:
            - name: POSTGRESQL_REPLICATION_MODE
              value: slave
            - name: POSTGRESQL_MASTER_HOST
              value: sample-service-psql
            - name: POSTGRESQL_REPLICATION_USER
              valueFrom:
                secretKeyRef:
                  name: sample-secret-psql
                  key: POSTGRESQL_REPLICATION_USER
            - name: POSTGRESQL_REPLICATION_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: sample-secret-psql
                  key: POSTGRESQL_REPLICATION_PASSWORD
  volumeClaimTemplates:
    - metadata:
        name: pgdata
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 5Gi
        storageClassName: nfs-client

ここで、PostgreSQL slave から master を参照する設定として POSTGRESQL_MASTER_HOST に sample-service-psql を指定していますが、これは Service sample-service-psql を使うことを意図しています。

しかし Service sample-service-psql はラベルに app: sample-psql を持つ Pod を Selector として指定しているため、今のままでは PostgreSQL の master/slave 両方を指すことになってしまいます。

そこで PostgreSQL master を指すよう selector を修正することにします。

sample-service-psql.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-service-psql
spec:
  selector:
    app: sample-psql
    role: master  # role: master を追加した
  ports:
    - protocol: TCP
      port: 5432

※ Service の selector を更新する場合は kubectl apply コマンドで更新後のファイルを指定することで更新できます。

以上で StatefulSet を作成する準備が整ったので、最後に StatefulSet を作成します。

# PostgreSQL の master となる Pod を StatefulSet で作成する
$ kubectl apply -f sample-statefulset-psql-master.yaml 
statefulset.apps/sample-statefulset-psql-master created

# PostgreSQL の slave となる Pod を StatefulSet で作成する
$ kubectl apply -f sample-statefulset-psql-slave.yaml 
statefulset.apps/sample-statefulset-psql-slave created

# 作成した StatefulSet を確認する
$ kubectl get statefulset
NAME                             READY   AGE
sample-statefulset-psql-master   1/1     27m
sample-statefulset-psql-slave    1/1     25m

# 作成した Pod を確認する
# ※ StatefulSet が作成する Pod は ${StatefulSet名}-[0-9] のように、0から順に採番されます
$ kubectl get pod
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-deployment-ruby-6dd8b54c8-kn2d4                         1/1     Running     0          41m
sample-deployment-ruby-6dd8b54c8-l82pj                         1/1     Running     0          41m
sample-job-migrate-2rlbl                                       0/1     Completed   0          24m
sample-statefulset-psql-master-0                               1/1     Running     0          27m
sample-statefulset-psql-slave-0                                1/1     Running     0          25m

# 作成した PersistentVolumeClaim を確認する
$ kubectl get pvc
NAME                                      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pgdata-sample-statefulset-psql-master-0   Bound    pvc-a4397a0f-6446-11e9-8ff7-92ffaff62273   5Gi        RWO            nfs-client     27m
pgdata-sample-statefulset-psql-slave-0    Bound    pvc-f0c756ec-6446-11e9-8ff7-92ffaff62273   5Gi        RWO            nfs-client     25m
sample-pvc-psql                           Bound    pvc-5098bd06-638b-11e9-8ff7-92ffaff62273   5Gi        RWO            nfs-client     22h

# 作成した PersistnetVolume を確認する
$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                             STORAGECLASS   REASON   AGE
pvc-5098bd06-638b-11e9-8ff7-92ffaff62273   5Gi        RWO            Delete           Bound    default/sample-pvc-psql                           nfs-client              22h
pvc-a4397a0f-6446-11e9-8ff7-92ffaff62273   5Gi        RWO            Delete           Bound    default/pgdata-sample-statefulset-psql-master-0   nfs-client              27m
pvc-f0c756ec-6446-11e9-8ff7-92ffaff62273   5Gi        RWO            Delete           Bound    default/pgdata-sample-statefulset-psql-slave-0    nfs-client              25m

※ StatefulSet を作成する前に Pod sample-pod-psql 用に作成した PersistentVolumeClaim sample-pvc-psql は使わなくなりました。

最後にやることが残っています。
StatefulSet を作成して PersistentVolumeClaim を管理することにしたため、PostgreSQL 起動時にはマイグレーションが行われておりません。

そこで Job を再度使用してマイグレーションを行います。
Job は既に成功した Pod を 1 つ持ち目的を果たしているため、一度削除してから再度作成します。
これでマイグレーション処理が行われることになります。

# マイグレーション用 Job を削除する
$ kubectl delete job sample-job-migrate
job.batch "sample-job-migrate" deleted

# マイグレーション用 Job を作成する
$ kubectl apply -f sample-job-migrate.yaml
job.batch/sample-job-migrate created

# Job が作成した Pod が Completed となったことを確認する
$ kubectl get pod
NAME                                                           READY   STATUS      RESTARTS   AGE
sample-deployment-ruby-6dd8b54c8-kn2d4                         1/1     Running     0          41m
sample-deployment-ruby-6dd8b54c8-l82pj                         1/1     Running     0          41m
sample-job-migrate-2rlbl                                       0/1     Completed   0          24m
sample-statefulset-psql-master-0                               1/1     Running     0          27m
sample-statefulset-psql-slave-0                                1/1     Running     0          25m

Rails アプリケーションを外部に公開する

これまで Rails アプリケーションも PostgreSQL もクラスタ内部からのアクセスしかできませんでした。本記事の最後として Rails アプリケーションを外部に公開することにします。

GCP や AWS 等のサービスを使っている場合は LoadBalancer リソースを使うのが望ましいと思います。
LoadBalancer リソースを作成すると、クラスの外部に LoadBalancer インスタンスが作成されます。(GCP では負荷分散インスタンスが作成されます)

本記事では Rancher を使ったオンプレミス環境を想定しているため LoadBalancer リソースは使用せず、Nginx Ingress Controller による Ingress リソースを使うことにします。

Nginx Ingress Controller による Ingress リソースはクラスタ内に Pod を作成し、その Pod が L7 Load Balancer となります。
Ingress リソースは backend として type: NodePort の Service を指定する必要があります。
そのため、事前に type: NodePort の Service を作成しておく必要があります。

type: NodePort の Service リソースを作成すると、クラスタ内全ノードの外部インタフェースに 0.0.0.0:XXXX で LISTEN されます。従って NodePort を作成すると外部に公開されることになります。
但し、あくまでそのポートを使って通信が出来るだけのため、証明書を使った HTTPS 通信やパスベースルーティングなどは行われません。

sample-service-nodeport-ruby.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-service-ruby
spec:
  type: NodePort
  selector:
    app: rails-app
  ports:
    - name: rails-app
      protocol: "TCP"
      port: 80
      targetPort: 3000

type: NodePort の Service リソースで記述する内容は Rails アプリケーションから PostgresSQL に接続するために作成した Service と同様です。

# Service を作成する
$ kubectl apply -f sample-service-nodeport-ruby.yaml 
service/sample-service-ruby created

# Service が作成した NodePort のポート番号を確認する
$ kubectl get service
NAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes            ClusterIP   10.43.0.1       <none>        443/TCP        25d
sample-service-psql   ClusterIP   10.43.100.113   <none>        5432/TCP       23h
sample-service-ruby   NodePort    10.43.154.15    <none>        80:30411/TCP   5s
$ kubectl describe service sample-service-ruby
Name:                     sample-service-ruby
Namespace:                default
Labels:                   <none>
Annotations:              field.cattle.io/publicEndpoints:
                            [{"addresses":["XXX.YY.ZZZ.AA"],"port":32194,"protocol":"TCP","serviceName":"default:sample-service-ruby","allNodes":true}]
                          kubectl.kubernetes.io/last-applied-configuration:
                            {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-service-ruby","namespace":"default"},"spec":{"ports":[{"na...
Selector:                 app=rails-app
Type:                     NodePort
IP:                       10.43.154.15
Port:                     rails-app  3000/TCP
TargetPort:               3000/TCP
NodePort:                 rails-app  32194/TCP
Endpoints:                10.42.1.197:3000,10.42.1.198:3000
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

NodePort 32194/TCP とあるように、クラスタ内全ノードの外部インタフェースの TCP 32194 番が公開されていることが分かります。
クラスタノードの 1 つを選び、外部IPアドレスとポート番号(例では32194)を使ってブラウザでアクセスしてみて下さい。

http://<クラスタノードのIPアドレス>:32194

Rails アプリケーションが表示されれば成功です。

次に Ingress リソースを作成します。

sample-ingress-ruby.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sample-ingress-ruby
spec:
  rules:
    - http:
        paths:
          - backend:
              serviceName: sample-service-ruby
              servicePort: 3000
# Ingress リソースを作成する
$ kubectl apply -f sample-ingress-ruby.yaml 
ingress.extensions/sample-ingress-ruby created

# Ingress リソースが作成されたことを確認する
$ kubectl get ingress
NAME                  HOSTS   ADDRESS         PORTS   AGE
sample-ingress-ruby   *       XXX.YY.ZZZ.AA   80      4m21s  # XXX.YY.ZZZ.AA はアドレスが表示されるまで時間がかかる場合があります
$ kubectl describe ingress sample-ingress-ruby
Name:             sample-ingress-ruby
Namespace:        default
Address:          
Default backend:  default-http-backend:80 (<none>)
Rules:
  Host  Path  Backends
  ----  ----  --------
  *     
           sample-service-ruby:3000 (10.42.1.197:3000,10.42.1.198:3000)
Annotations:
  kubectl.kubernetes.io/last-applied-configuration:  {"apiVersion":"extensions/v1beta1","kind":"Ingress","metadata":{"annotations":{},"name":"sample-ingress-ruby","namespace":"default"},"spec":{"rules":[{"http":{"paths":[{"backend":{"serviceName":"sample-service-ruby","servicePort":3000}}]}}]}}

Events:
  Type    Reason  Age   From                      Message
  ----    ------  ----  ----                      -------
  Normal  CREATE  15s   nginx-ingress-controller  Ingress default/sample-ingress-ruby

Ingress はどのホスト名でどのパスに来ても Rails アプリケーション用 Service sample-service-ruby に Proxy します。

クラスタの IP アドレスを使ってブラウザで Rails アプリケーションが表示できることを確認してみて下さい。

http://<クラスタノードのIPアドレス>

終わりに

Rails アプリケーションを k8s で公開することを目的とし、クラスタ外部からブラウザでアプリケーションにアクセスできるようになるところまで紹介しました。

勉強のため冗長構成を作成する最低限の方法を記述しておりますが、PostgreSQL を冗長化させるためには Helm Chart を使ってインストールする方が望ましいでしょう。

HTTPS 通信が出来るようにしたり、証明書を発行する等より実践的な方法については他記事を参照するようにしてください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28