こんにちは!プロジェクト推進室でエンジニアをしているTei1988です。
先日開催したSpeeeKaigi #3で「I ❤️ Kubernetes」を発表しました。
Kubernetesは、コンテナオーケストレーションツールです。 コンテナ化されたアプリケーションの管理やデプロイ、スケールなどを一手に引き受けます。
Google Container EngineからGoogle Kubernetes Engineに名称変更されたり、EKS(Amazon Elastic Container Service for Kubernetes)も始まったりと、スタンダードなツールになりつつあるので、Kubernetesを学んでおいても損にはならないかと思います。
DockerもKubernetesもほぼ初めてだったのですが、触っていくうちにいつのまにか❤️ になっていました。
ここでは、今回やってみたことを紹介します。
0. そもそも
最近、自部署近辺でKubernetesというワードがちらほら出てきました。これまで、Dockerにもあまり触れられていなかったので、勉強のためにRailsでアプリケーションを作り、Kubernetes上で動かしてみることにしました。
1. Kubernetesを構築してみる
KubernetesはOSSなので、自分で構築することができます。どのような仕組みで動くのか興味があったので、IDCFさんの500円サーバを3台使って自分で構築することにしました。
構築するにあたって、下記の記事を参考にしました。
kubernetesによるDockerコンテナ管理入門 | さくらのナレッジ
丁寧に導入方法が書かれていて、とても助かりました。
また、文中のマスター-ノード間や、ノード-ノード間の動きの図がわかりやすく、動きの理解もしやすかったです。
ただ、オプションの命名規則が_
から-
に変更されていたようで、一通り設定を終えていざPodをデプロイしようという段階でデプロイに失敗してしまう現象に悩まされました。 マスター-ノードの認証をしている部分で、service-account-private-key-file
のオプションが効いておらず、認証のための秘密鍵が設定されていないために認証に失敗してしまうことが原因でした。
kubectl cluster-info
や、kubectl get nodes
でクラスタやノードの状態を調べても正常な場合と差が見当たらず、原因がなかなか特定できませんでした。
とりあえず、kubectl create -f ...
などはできて、nginxのwelcomeページは表示できるようになりました。
なんとなく仕組みを理解できたものの、 Railsアプリケーションに全然着手できていなかったので、自分で構築することを諦めて、GCPで提供されているKubernetes(GKE)を使うことにしました。
ちなみに、Kubernetes自体を試す場合には、GitHub - kubernetes/minikube: Run Kubernetes locallyというものがあり、こちらを使えばすぐに環境が作れます。
2. Railsアプリケーションをつくってみる
ウェブサイトとその管理画面、バッチ、非同期処理を一つのRailsアプリケーションとしました。
アプリケーションの詳細は特に変わったことをしているわけではないので省きますが、ウェブサイトとその管理画面を起動時の環境変数によって切り替えられるようにしました。
全体のシステム構成図は以下を想定しました。
Railsアプリケーションの前にnginxを入れて、非同期処理にSidekiqを利用する、よくある構成だと思います。
3. Redisを導入してみる
Sidekiqで使うために、Redisを導入することにしました。
AWSにはElastiCacheがあるのですが、GCPには今のところありません。そのため、 Kubernetes上でRedisを動かすことにしました。 本格的にサービスに導入する場合は、冗長構成などを検討すると思いますが、今回はマスター1台としました。
下記のように、 readinessProbe
と livenessProbe
で、Podが落ちていても再起動がかかるようにしました。
... readinessProbe: tcpSocket: port: 6379 initialDelaySeconds: 5 periodSeconds: 1 livenessProbe: tcpSocket: port: 6379 initialDelaySeconds: 10 periodSeconds: 30 ...
4. CloudSQLのプロキシを設定してみる
データベースは、GCPのCloudSQLを利用することにしました。Kubernetesからの接続にはいくつか方法があります。
今回は以下を参考にプロキシを挟むことにしました。
Connecting from Google Kubernetes Engine | Cloud SQL for MySQL | Google Cloud Platform
サイドカーコンテナを使うことが書かれているのですが、アプリ本体や、管理ツール、バッチ、Sidekiqでそれぞれサイドカーコンテナを設定するのはなんとなく避けたいと思ってしまったので、プロキシ単体でPodを作ってみました。
引数で、tcp:3306
をtcp:0.0.0.0:3306
に変えることでローカル接続以外も受け付けるようになります。
... - -instances=<project_name>:<zone>:<instance_name>=tcp:0.0.0.0:3306 ...
あとは、 3306
を expose
する設定をデプロイしてあげれば、Kubernetesの内部であればどのPodからでも接続が可能になります。
5. Railsアプリケーションをコンテナ化してみる
Railsアプリケーションをコンテナ化する際に、動作に必要の無いパッケージを極力含めたくなかったので、ruby:alpine
からイメージを作ることにしました。
とはいえ、Gemの中にはNative Extensionを利用するものもあり、それらをインストールするためには開発用パッケージが必要になります。 最初の頃はDockerfileのRUNを一つにまとめて頑張っていたのですが、毎回パッケージインストールやらコンパイルやらが走ってしまい、イメージのビルドに時間がかかってしまっていました。 そんな時に、ECSとGoとDocker multistage build - Speee DEVELOPER BLOGでも紹介されているmultistage buildを知り、二段構えにすることにしました。
流れとしては以下のような形です。
- Railsアプリケーションで使うGemのコンパイルをするための
app:bundle-build
をビルドします。 - 実際に動かすための
app:latest
をビルドします。この時、app:bundle-build
からインストールしたGemのディレクトリをまるっとCOPYしてきます。
これにより、不要な開発ツールを実行用のイメージに含めなくてよくなると同時に、Gemの更新が無い場合はapp:bundle-build
はキャッシュされたものが使われるので、毎回nokogiri
のコンパイルを待つ必要が無くなりました。
6. Railsアプリケーションを起動してみる
#5で作ったイメージには、ウェブサイトとその管理画面、バッチ(Rakeタスク)、非同期処理(Sidkeq)が含まれています。
そこで、Kubernetesのデプロイの設定にそれぞれの起動時のコマンド変えて、一つのイメージから起動させることにしました。
ウェブサイトは以下のようにしました。(部分抜粋)
... containers: - name: web image: gcr.io/<project_name>/app:latest imagePullPolicy: Always command: - bundle args: - exec - pumactl - -F - config/puma.rb - start ports: - containerPort: 3000 protocol: TCP env: - name: RAILS_ENV value: production - name: RAILS_LOG_TO_STDOUT value: "1" - name: RAILS_SERVE_STATIC_FILES value: "1" ...
管理画面は以下のようにしました。(部分抜粋)
... - name: admin image: gcr.io/<project_name>/app:latest imagePullPolicy: Always command: - bundle args: - exec - pumactl - -F - config/puma.rb - start ports: - containerPort: 3000 protocol: TCP env: - name: RAILS_ENV value: production - name: ADMIN value: "1" - name: RAILS_LOG_TO_STDOUT value: "1" - name: RAILS_SERVE_STATIC_FILES value: "1" ...
管理画面とウェブサイトの差分は、起動時に環境変数ADMINがセットされているかどうか、です。
Sidekiqは以下のようにしました。(部分抜粋)
containers: - name: sidekiq image: gcr.io/<project_name>/app:latest imagePullPolicy: Always command: - bundle args: - exec - sidekiq - -C - config/sidekiq.yml env: - name: RAILS_ENV value: production - name: RAILS_LOG_TO_STDOUT value: "1"
ウェブサイトや管理画面との違いは、puma
ではなく、sidekiq
を立ち上げていることです。
kubernetes
だけが解消できるという訳ではありませんが、環境変数を設定に書くことで、実はxxの環境変数が動作に影響していて、それはooの奥底で定義されていた!の様な、辛い経験とはおさらばです。
そのままでは他のPodから繋がらないのでapp
の3000
番ポートをexpose
するような設定を作って、デプロイします。
admin
はそのままお外からも繋げるようにしました。
とはいえ、アクセス元は制限したいので、以下のような設定を追加しました。
... spec: ports: - name: admin port: 80 targetPort: 3000 selector: app: admin type: LoadBalancer loadBalancerIP: <事前に取得した静的IP> loadBalancerSourceRanges: - <許可するアクセス元のIP/CIDR> ...
直接GCPのコンソールから設定を追加することもできるのですが、Kubernetes側で再度デプロイすると、当然ながら手動で入れた設定も上書きしてしまいます。 いつのまにか全世界に公開していた、ということになるかもしれないので、設定に含めるのが良いと思います。
7. nginxに設定を追加してみる
システム構成図の nginx
の部分です。
Kubernetesでは、Serviceを作ると、そのServiceにつけた名前で名前解決ができるようになります。 *1
そこで、以下のような設定にしてみました。 この設定で、ちゃんとRailsアプリへアクセスが行きます。
... upstream backend { server web:3000; } ... server { ... location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Host $host; ... }
スケールしてRailsアプリを複数デプロイしてある場合でも、Service
で複数のPodにマッチするように設定を書いている場合はこのままでアクセスが分散されます。すごい。
セッションなどを使う場合は、少し考えないとダメそうですね。
8. 定期的にジョブを実行する
Kubernetesには、cron的なものがあります。ズバリ、CronJobです。 1.8以降でAPIがbetaになったのですが、1.8以前はalpha版で、GCPでは使えませんでした。
自分が試した時はまだalpha版だったため、busyboxのcrondをフォアグラウンドで動かすことにしました。 バッチとbusyboxをイメージに入れて、crontabを書くだけなので、手軽だと思います。
kind: ConfigMap apiVersion: v1 metadata: name: crontab data: root: |- 10,40 0-18 * * * echo "hoge" --- kind: Deployment apiVersion: extensions/v1beta1 metadata: name: cron spec: replicas: 1 selector: matchLabels: app: cron template: metadata: labels: app: cron spec: containers: - name: cron image: gcr.io/<project_name>/app:latest imagePullPolicy: Always command: - busybox args: - crond - -d - '6' - -f volumeMounts: - name: crontab mountPath: /var/spool/cron/crontabs readOnly: true volumes: - name: crontab configMap: name: crontab
9. Sidekiqのスケール
Kubernetesを使っていて、とても感動したことがこれです。
非同期処理をたくさん行う場合、Skidekiqのノードの数をたくさん増やしたくなると思います。
Kubernetesでは、
kubectl scale --replicas 3 deployment sidekiq
で、Podを3つに増やせます *2。
とても簡単でした。
10.さいご
SpeeeKaigiをきっかけに、KubernetesやDockerに触れられて、本当によかったと思いました。
自分でKubernetesを建ててみたことは無駄ではなく、下記「Be Docker Day」ですでに使い始めていた他のメンバーと同じぐらいKubernetesやDockerの話を深くすることもできました。
tech.speee.jp
ここ最近では、Java + AkkaなシステムをDocker化してKubernetesで動かしました。 今後も、DockerとKubernetesの良さをもっと広めていきたいなぁ、と思います。