Kubernetesの学習のためにMastodonを構築したら勉強になった

そろそろKubernetes(以後k8s)触ってみないといかんな欲が高まってきました。
が、k8sを使ってなにを構築したものかと思ってたんですが、
Mastodonを使いたい案件(プライベートで)があったので、k8sを使ってMastodonを構築していこうと思います!

自分のメモ書きみたいな内容なので注意。

Kubernetesについて

まずk8sについて基礎的なところから学んでいきます。

k8sは一言でいうと「コンテナオーケストレーションツール」です。
しかしイメージしづらいので詳しく調べてみます。

k8sが生まれた背景

同一ホスト内に複数コンテナがあるとします。
(図を書くのが面倒なのでPlantUMLで適当に書いた図)

この同一ホスト内でのコンテナ同士はプライベートネットワークで通信ができますが、外部への通信はNATを通してしかできません。

このホストのスケールアウトを考えたとき、コンテナの連携やルーティングが複雑になるのは想像に難くないでしょう。

この問題を解決するのがKubernetesです。
ユーザに上記のような問題を意識させないように、k8sはコンテナのクラスタ化を行います。

ユーザがコンテナを起動させたいときは

  • コンテナイメージ
  • コンテナの台数

を指定するだけで、k8sがいい感じにクラスタ化してくれます。

仕組み

そもそもどうやってk8sにコンテナがデプロイされるんでしょう?

  1. ユーザはどのようなコンテナを何台起動するかという情報(Spec)をk8sに渡す
  2. k8sのMasterは渡されたSpecをもとに、クラスタ内の空きリソースを確認してどのNodeにどのように配置するか決定する
  3. 各NodeはMasterが決定した内容をもとにコンテナを起動する

Master1台が複数台あるNodeに対して指示を出しSpec通りコンテナを起動していく という流れになります。
またk8sはSpecに指示された台数などを維持します。
たとえコンテナやNodeに障害があり落ちても、k8sはこれを検知し、再起動やNodeの起動などを行います。

Pod

Podはk8sにおいての最小単位であり、コンテナの集まりです。
上記の「同一ホスト内に複数コンテナ」の図がそのまんまPodにあたります。
Podは以下の要素で構成されています。

  • コンテナ
    • 複数個ある場合もある。Nginx、Webアプリ、Redisなど
  • Volume
    • Pod内コンテナが共用する記憶領域
  • Cluster IP
    • Pod内コンテナが共有するIPアドレス

Deployment

じゃあPodの配置ってどうやんの?って思いますが、それがDeploymentです。
Podの起動数やコンテナイメージなどを指定します。

Service

DeploymentでPodを作っただけだと、外部からアクセス出来ないですし、
複数同じPod(Replica Set)がある場合、どれにアクセスしていいかわかりません。
そのPodへのアクセス手段を用意するのがServiceです。

Replica Setへのロードバランス機能もServiceが提供します。

実際k8sでpodを作ってみる

ここまで

  • Pod
  • Deployment
  • Service

というものが出てきました。
それを理解するために実際にk8sになにかデプロイしてみたいと思います。

Googleがチュートリアルを提供していますのでこれを例にやっていきます。 https://cloud.google.com/kubernetes-engine/docs/tutorials/hello-app?hl=ja

また今回は定義ファイルを中心に見ていきたいので、クラスタ生成などは上記チュートリアルを見てください。

# サンプルダウンロード
$ git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples
$ cd kubernetes-engine-samples/hello-app
$ vim helloweb-deployment.yaml
apiVersion: apps/v1beta1
kind: Deployment  # Deploymentの定義ファイルであると宣言
metadata:
  name: helloweb # このDeploymetの名前付け
  labels:
    app: hello # app=helloというラベルを付与
spec: # k8sに渡すSpec
  replicas: 3 # 以下テンプレートの内容を3つ作る
  template:
    metadata:
      labels:  # それぞれのPodにこのラベルを付ける
        app: hello
        tier: web
    spec:
      containers: # コンテナの定義
      - name: hello-app
        image: gcr.io/google-samples/hello-app:1.0
        ports:
        - containerPort: 8080

ではこれをデプロイしてみます。

$ gcloud config set project ${project_id}
$ gcloud container clusters create hello-app --zone asia-northeast1-a --num-nodes 3
kubeconfig entry generated for hello-app.
NAME       ZONE               MASTER_VERSION  MASTER_IP      MACHINE_TYPE   NODE_VERSION  NUM_NODES  STATUS
hello-app  asia-northeast1-a  1.7.8-gke.0     35.187.196.56  n1-standard-1  1.7.8-gke.0   3          RUNNING

$ gcloud config set compute/zone asia-northeast1-a
$ gcloud config set container/cluster hello-app
$ gcloud container clusters get-credentials hello-app

$ kubectl create -f helloweb-deployment.yaml
deployment "helloweb" created

$ kubectl get pod
NAME                        READY     STATUS    RESTARTS   AGE
helloweb-1127322674-b3ndr   1/1       Running   0          1m
helloweb-1127322674-cq9xw   1/1       Running   0          1m
helloweb-1127322674-lz7h2   1/1       Running   0          1m

こんな感じでPodが3つ作成されます。 続いてServiceです。

$ vim helloweb-service-static-ip.yaml
apiVersion: v1
kind: Service   # Serviceの定義ファイルであると宣言
metadata:
  name: helloweb
  labels:
    app: hello
spec:
  selector: # 対象とするPod(Replica Set)のラベルを指定
    app: hello
    tier: web
  ports: # ロードバランサのListenPortを指定
  - port: 80
    targetPort: 8080
  type: LoadBalancer
  # loadBalancerIP: "YOUR.IP.ADDRESS.HERE" # 指定なしだと勝手にIP振られる
$ kubectl create -f helloweb-service-static-ip.yaml
service "helloweb" created

$ kubectl get service
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)        AGE
helloweb     LoadBalancer   10.59.240.23   35.200.32.20   80:31174/TCP   46s

$ curl 35.200.32.20
Hello, world!
Version: 1.0.0
Hostname: helloweb-1127322674-b3ndr

Serviceを作ることにより、外部からアクセスできるようになりました!

まとめ

KubernetesはDockerをクラスタ化するもの。

Kubernetesは

  • Master
  • Node

という役割を持ち、実際にContainerが乗るのはNodeで MasterはどのクラスタにどうPodを乗せるかなどを管理しているもの。

名称 役割
Pod Containerの集まり。記憶領域とIPを共有している。
Replica Set Podの集まり。複数のノードに跨がる。何台必要かなどはDeploymentで定義する。
Service Replica Setへのルーティングを担うもの。ロードバランサ的な役割も持つ。

Mastodonについて

Mastodonは分散型のソーシャルネットワークで、Twitterのような短文投稿システムです。
そのMastodonサーバーのことを「インスタンス」と呼び、そのインスタンスは誰でも立てられます。
そのインスタンス同士を繋ぎ大きなソーシャルネットワークを構築することができます。

そんなMastodonですがアーキテクチャに様々な技術を使用しており、インスタンスを構築してみることでインフラの勉強になるかと思います。

ということで、内部構造を簡単に見ていきます。

内部構造

Name Description
Rails サーバーサイドアプリケーション
Postgresql データベース
Redis キャッシュ
Node.js ストリーミングAPI
Sidekiq ジョブキュー。トゥートやストリーミングのたびにキューが作られる

k8sでMastodonを構築する

さて、ようやく本題です。k8sでMastodonを構築するにはどのようにすればよいのでしょうか?

まず下記のようなPodが必要そうです。

  • Nginx Pod
  • Rails Pod
  • Node.js Pod
  • Sidekiq Pod
  • Redis Pod
  • Postgresql Pod

このうちPostgreSQLはGCPのCloud SQLを使おうと思います。
が、GCPのk8sからCloud SQLに接続するにはCloud SQL ProxyというDockerコンテナが必要になるので、このPodを作成します。

Google Container Engine から接続する

またMastodonでは画像のアップロードが出来ますが、簡易に取り出したいので、GCSに保存していきたいと思います。

あとMastodonのユーザ登録にはメール送信が必要です。
これはSendGridを使用します。

k8sのレシピは下記を基本的に使用してます。
https://github.com/jviide/kubedon

上記以外で設定した項目を以下に記載していきます。

Mastodonの設定ファイル

Mastodonアプリケーションの設定ファイルを生成しますが、
k8sでは「ConfigMap」という設定ファイルリソースを生成することができます。

以下が今回作成するMastodonの設定です。

$ vim config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mastodon-config
data:
  # Service dependencies
  REDIS_HOST: redis
  REDIS_PORT: "6379"

  DB_HOST: cloudsql
  DB_NAME: mastodon
  DB_PORT: "5432"

  LOCAL_HTTPS: "true"

  SMTP_SERVER: "smtp.sendgrid.net"
  SMTP_PORT: "2525"   # GCEはPort25、465、587での送信接続が出来ないためこのPortを指定
  SMTP_LOGIN: "apikey" # 文字列で「apikey」を指定
  SMTP_PASSWORD: "${SendGridのAPIキー}"
  SMTP_FROM_ADDRESS: "${所有しているドメインのメールアドレス}"
  SMTP_AUTH_METHOD: plain
  SMTP_OPENSSL_VERIFY_MODE: none
  SMTP_ENABLE_STARTTLS_AUTO: "true"
  SMTP_DELIVERY_METHOD: smtp

  # S3 (optional)
  S3_ENABLED: "true"
  S3_BUCKET: "${GCSのバケット名}"
  AWS_ACCESS_KEY_ID: "${GCEのアクセスキーID}"
  AWS_SECRET_ACCESS_KEY: "${GCEのシークレットキー}"
  S3_REGION: asia-northeast1
  S3_PROTOCOL: https
  S3_HOSTNAME: storage.googleapis.com
  S3_ENDPOINT: https://storage.googleapis.com

SMTPとGCSの設定を行っています。

Mastodonは標準でS3をサポートしています。
GCSはS3のAPIインタフェースで使用することができるので、MastdonでもGCSをストレージとして使用することができます。

Amazon S3 から Google Cloud Storage への移行

Mastodon作成のための手順

# 事前にCloudSQLでPostgreSQLをたてておく

# CloudSQLにproxy用のユーザ作成
$ gcloud sql users create proxyuser host --instance=mastodon --password=mastodon-proxy

# k8sクラスター作成
$ export PROJECT_ID="$(gcloud config get-value project -q)"
$ export REGION="asia-northeast1-a"
$ gcloud config set project ${PROJECT_ID}
$ gcloud config set compute/zone ${REGION}
$ gcloud container clusters create mastodon --zone ${REGION} --num-nodes 3

# kubectlコマンドでmastodon k8sクラスターを使用するようにする
$ gcloud config set container/cluster mastodon
$ gcloud container clusters get-credentials mastodon

# k8sからCloudSQLにつなぐための設定。
# 事前にサービスアカウントを作成し、Keyファイルを配置しておく必要あり
$ kubectl create secret generic cloudsql-secrets \
  --from-file=credentials.json=credential.json \
  --from-literal=instance_connection_name=${PROJECT_ID}:${REGION}:mastodon

# 環境変数の設定
mastodon_domain=${Mastodonのサイトのドメイン} 

$ kubectl create secret generic mastodon-secrets \
  --from-literal=PAPERCLIP_SECRET=mas1 \
  --from-literal=SECRET_KEY_BASE=mas2 \
  --from-literal=OTP_SECRET=mas3 \
  --from-literal=LOCAL_DOMAIN=${mastodon_domain} \
  --from-literal=DB_USER=proxyuser \
  --from-literal=DB_PASS=mastodon-proxy

# Let's Encryptの証明書発行
$ wget https://dl.eff.org/certbot-auto
$ chmod a+x certbot-auto
$ ./certbot-auto

$ ./certbot-auto certonly \
  --manual \
  --domain ${mastodon_domain}  \
  -m ${自分のメールアドレス} \
  --agree-tos \
  --manual-public-ip-logging-ok \
  --preferred-challenges dns 

$ sudo cat /etc/letsencrypt/live/${mastodon_domain}/fullchain.pem > fullchain.pem
$ sudo cat /etc/letsencrypt/live/${mastodon_domain}/privkey.pem   > privkey.pem

# Nginx設定でこの証明書を参照させる設定
$ kubectl create secret generic web-certificates \
  --from-file=fullchain.pem \
  --from-file=privkey.pem

# kubedonのチェックアウト
$ git clone https://github.com/jviide/kubedon
$ cd kubedon

# 上記config.yamlの設定を行った後
$ kubectl create -f .

# nginxのexternal ipを確認(表示されるまで時間かかる)
# external ipを確認できたら、MastdonのドメインのAレコードに登録
$ kubectl get service
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                      AGE
cloudsql     ClusterIP      10.47.255.19    <none>          5432/TCP                     11m
kubernetes   ClusterIP      10.47.240.1     <none>          443/TCP                      14m
mastodon     ClusterIP      10.47.240.180   <none>          3000/TCP,4000/TCP            11m
nginx        LoadBalancer   10.47.243.203   35.200.14.137   80:31337/TCP,443:32088/TCP   11m
redis        ClusterIP      10.47.240.9     <none>          6379/TCP                     11m

# DBにデータセットアップ
$ kubectl get pod
$ kubectl exec -it ${mastodonのpod id} /bin/sh
#$ kubectl exec -it mastodon bash
$ RAILS_ENV=production bundle exec rails db:setup
$ RAILS_ENV=production bundle exec rails assets:precompile
$ exit

# 完了!

ここまで出来たら、自分が設定したMastodonのドメインをブラウザで見てみましょう!
以下の画面が出てきたら成功です。

もし開かない場合、エラーが発生しています。
GKEでは「Stackdriver」というサービスでアプリケーションログの確認ができます。
大体そこに原因が出力されてるので、対応しましょう。

ここで終わりではなく、ユーザの登録とトゥートが出来ないと意味がないのでそちらも確認しましょう。

# GCSに画像が保存されてるか確認
$ gsutil ls gs://mastodon-morix/media_attachments/files/000/000/001/original
gs://mastodon-morix/media_attachments/files/000/000/001/original/f0669c90da43564b.jpg

出来ました!!

最後に

今回のMastodon構築では、下記の設定ファイルを使わせていただきました。
https://github.com/jviide/kubedon

この記事では上記の設定ファイルに触れると量が膨大になるので触れませんでしたが、
k8sの基礎を学ぶにはとてもいい教材だと思うので、ぜひ読んでみてください!

AWSでもk8sでも出ましたが、インフラ屋としては今後も触っていかないといけないサービスだなぁと思ったので、引き続き勉強がんばろ!

では!

参考資料