KMC活動ブログ

京大マイコンクラブの活動の様子を紹介します!!

部内Kubernetesクラスタに部員向けWebサービスを移設しました

はじめに

おはもに~。id:utgwkk です。最近の京都は夏のような日もあって、計算機にはつらい季節になりつつありますね。

今日は、部員向けWebサービスを部内Kubernetes (以下、k8s) クラスタに移設した話をします。

部内k8sクラスタについて

KMCでは、サークルの部内サーバーでk8sクラスタを運用しています。KMCの部員であれば誰でも自由にアプリケーションをk8sクラスタ上で稼動させることができます。k8sクラスタを構築した経緯や技術的な詳細については、以下の記事をごらんください。

blog.kmc.gr.jp

移設したWebサービスについて

今回移設したWebサービスは、部員向けのイラスト投稿サービス (通称 God Illust Uploader、以下では神ロダと呼びます) です。KMCでは毎年お絵描きプロジェクトという勉強会・練習会を開催しており、課題を提出する場所や、描いたイラストを部員向けにアピールしたりする場所として神ロダが使われています。

移設前の神ロダの構成は以下のようになっています。

  • 実装言語: Python
  • データベース: SQLite
  • APIインタフェース: GraphQL
  • フロントエンド: TypeScript + React + react-router
    • いわゆるシングルページアプリケーション

全てのソースコードやデータベースは部内のNFS上に設置されており、nginxとPassengerを経由してアプリケーションにリクエストが届く、という構成になっていました。また、部員だけが閲覧できるように前段には部内共通の認証機構が設置されていました*1

モデルの関係は以下のようになっています。ありていに言えば簡単なpixivみたいなイメージです。

  • ユーザー (account) が複数の作品 (artwork) を投稿できる
  • 作品に1つ以上のイラスト (illust) が紐づいている
  • 作品に0個以上のタグ (tag) を付けられる
  • 作品にいいね (like) を付けられる
  • 作品にコメント (comment) を投稿できる

移設前の課題

神ロダはしばらく前節のような構成で稼動していましたが、いくつかの課題を抱えていました。

デプロイするためにサーバーで各種コマンドを打つ必要がある

神ロダの以前のデプロイ方法は以下のような流れでした。

  • mainブランチに変更を取り込む
  • サーバーにSSHして git pull する
  • フロントエンド・サーバーサイドのアプリケーションをビルド・再起動する

このうち最後の手順は make コマンドだけで完結するようにしてありますが、それでも自動化がされておらず手間でした。今からCapistranoに入門するのか? できることならmainブランチの変更を自動でデプロイしてほしい! と思いつつ月日が過ぎていきます。

言語処理系のバージョンアップが手間

言語処理系 (Python) のバージョンを上げるにあたって考慮すべきことがいろいろあります。

以下のような手順書を書いてバージョンアップを実施しましたが、やることが多い!! 異なるバージョンの依存ライブラリをそのまま使えるとは限らないのでvirtualenvを作りなおす必要があるとか、なぜかpoetryが壊れていたので直す*2とか、単にアップデートするだけでは終わらないのが大変です。また、この方法だとダウンタイムが出てしまうので、深夜にこっそりバージョンアップ作業をやっていました。

f:id:utgwkk:20220415180820p:plain
Pythonバージョンアップの手順書 (一部)

コンテナ化していたら、言語処理系のバージョンを上げるのはbase imageのtagを書き換えてデプロイするだけで*3完了するはずです。

依存モジュールや言語処理系のバージョンが構成管理されていない

ここでの依存モジュールとはPythonのライブラリのことではなく、imagemagickやWebP、ネイティブの依存ライブラリなどを指しています。言語処理系のバージョンもとくに管理していない*4ので、ちょっと sudo apt upgrade をかけたら壊れてた、ということもあるかもしれません。半分ぐらいは構成管理をサボっていただけなのですが……。

SQLiteの制約が強い

SQLiteは、MySQLPostgreSQLなどのRDBMSに比べるとスキーマ変更の制約が強いです。具体的には、インデックスや外部キー制約を追加したあとに ALTER TABLE 句で削除することができません*5。単純な ALTER TABLE 句では実現できないスキーマ変更を反映するには、一度テーブルをコピーして作り直す必要があります。

使っているO/Rマッパー (SQLAlchemy) はSQLite向けのバッチでのマイグレーションに対応していますが、考慮することが増えるのでできるだけシンプルに済ませたいです。部内サービスだから気楽にやってよいはずなのですが、DBスキーマリファクタリングが気軽にできないとなると困ります。

やっていきましょう

ここまで述べた課題を抱えつつ神ロダを開発・運用していたのですが、ある日やっていきが発生したので、やっていくことにしました。

移設の流れを考える

まずは移行作戦について考えていきます。

何はともあれ、アプリケーションサーバーが動くDockerイメージがないとk8sクラスタにデプロイできません。したがってDockerfileを書く必要があります。

データベースはこの機にMySQLに移行できるとよいと考えましたが、いったんNFS上のSQLiteのデータベースファイルをマウントした状態で移行して、後からMySQLに差し替える作戦を取りました。一気にいろいろやると切り分けが困難になるので、区切りをつけてからやっていきます。

フロントエンドはいったんNFS上にデプロイする方針で考えていました。ただし後述するように、結局フロントエンド用のDockerイメージを用意する方針に切り替えることになります。振り返ってみると、NFS上にデプロイするためのスクリプトを整える必要がある・フロントエンド用のDockerfileを書くのとどちらが手間か・自動化しやすいか、という点ではDockerイメージを作る方針でよかったと思っています。

アプリケーションのDocker化

まずはアプリケーションが動くDockerfileを用意しました。これはそんなに難しいこともなく、淡々とDockerfileを書いて調整する、を繰り返すだけです。あわせて、Docker Composeを使ってアプリケーションを1コマンドで立ち上げられるように調整しました。

この時点ではアプリケーションサーバーだけなので単なるコマンドラッパーのような感じでしたが、MySQL化するにあたって環境構築をやりやすくしておくのがよいと考えてサクッとやりました。また、早いうちからDocker上で動くアプリケーションを用意しておくことで、Docker環境と手元環境との差異を潰していくことができます。

アプリケーションの設定 (DSNやシークレットなど) は元から環境変数で差し込めるようにしていたので、とくに手間にはなりませんでした。

GitHub ActionsでDockerイメージをビルドする

Dockerfileができたので、ビルドパイプラインを整えていきましょう。GitHub Actionsでのdocker buildにはdocker/build-push-actionを使うのが便利です。push先のコンテナレジストリGitHub Container Registryを使っています。

Dockerイメージのタグは、FluxCDのImage Update Automationとの兼ね合いから {commit hash}-{ビルド日時} という形式にしています。commitにバージョンのタグを付けたときだけビルドする、という方式も取れますが、そんなにセマンティックにやらなくてもよかろう、ということで楽な方法に倒しました。

- name: Extract metadata (tags, labels) for Docker
  id: meta
  uses: docker/metadata-action@v3.6.2
  with:
    images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
    tags: |
      type=raw,value={{sha}}-{{date 'YYYYMMDDHHmmss'}}

YAML大好き

アプリケーションサーバーのDockerイメージができたので、デプロイを試せるようになりました。ここからk8s (kustomize) のYAMLをゴリゴリ書いていくことになります。

k8sの公式ドキュメントや部内wikiYAMLの例があるのですが、イチから書くのは初めてなのでかなり id:nonylene さんにレビューしてもらいながらYAMLを整えていきました。最初はお試し用のnamespaceで作って、あとからdefault namespaceに移動する方針にしました。

kustomize自体はk8sYAMLを生成するツールという感じで、生成されたYAMLが妥当かどうかはデプロイしてみないと分からない場合も多く、何度か書き直してはデプロイし直す、を繰り返しました。id:nonylene さんにk8s-lintというlinterを導入してもらってからは前もってエラーに気づきやすくなったと思います。

こうして試行錯誤して、いろいろなエラーを読み解きまくって、ついにアプリケーションサーバーをk8sクラスタ上にデプロイすることができました。よかったですね。

MySQL

次はMySQL移行をやっていきます。

手元の開発環境をDocker化してdocker-compose.ymlを書いてあるので、開発環境にDBを追加するのは一瞬です。アプリケーションの実装を精査して、MySQLに移行できるように修正していきます。意図せず自分自身のカラムに外部キー制約を貼ってある*6のを発見して直す、などの地道な活動が行われました……。

いよいよMySQL化の準備ができたので、SQLiteのデータをdumpしてMySQL向けに修正したあと流し込んでいきます。移行手順は SQLite3のデータをdumpしてMySQLに移行する - Qiita を参考にしました。

実際にデータを流し込んでみると、いくつかのタグがユニーク制約違反でINSERTできませんでした。SQLiteでは COLLATION nocase (大文字小文字を区別しない) になっていたカラムが、MySQLでは COLLATION utf8mb4_0900_ai_ci になっており、記号などより多くの文字列の大文字小文字が同一視されるようになっていたためです。ところで「パパ」と「ハハ」が同一視されるのはどう考えてもおかしいと思うのですが、我々はMySQL上で大文字小文字とどのように向き合えばいいのか……。

INSERTできなかったデータは数件だったので、目視確認して既存の別のタグに貼り替えるなどの対応を行いました。

MySQLk8sクラスタ上にデプロイしたあと、安全のためにk8s上のMySQLからバックアップ (mysqldump) をNFS上に保存するCronJobを書きました。crontab的なものもk8sにあって便利ですね。試運転してみて確かにバックアップができていることを見届けました。

secretの値をもとに環境変数を設定しつつexecしたい

ところで MYSQL_PASSWORD 環境変数の値はsecretに入っており、かつ同じ値をアプリケーションサーバーとDBで流用することを考えました。が、アプリケーションサーバーではDBの接続先をURL形式で指定する必要があるので MYSQL_PASSWORD 環境変数のままだと都合がよくありません。以下のように環境変数を設定してexecするラッパースクリプトを用意しました。

#!/bin/sh
set -e

export DB_URL=mysql+mysqlconnector://${MYSQL_USER}:${MYSQL_PASSWORD}@${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}
exec "$@"

他の環境変数をもとに環境変数の値を組み立ててセットする、みたいな機能があるとよいのですが、あるんでしょうか? 誰か教えてください。

namespaceを移動したら問題発生、そして転ばぬ先の杖

お試し用のnamespaceにひと通りデプロイできたので、default namespaceに移動します。基本的にはnamespaceを変えて回ればよいはず……と思ったらSealedSecretが復元できなくなってハマりました。

しばらく悩んでいたのですが、これはSealedSecretのスコープがkustomizeのnamePrefixを考慮したものになっていなかった*7のが原因でした。スコープを namespace-wide に変えたら復元できるようになりました。

namespaceを移動したことでMySQL向けのPersistent Volumeが作り直されてしまい、データがまっさらになりましたが、前段階でDBのバックアップを取っていたのでそれを流し込むだけで済みました。バックアップって便利ですね。

フロントエンドのDocker化

当初はフロントエンドをNFS上にデプロイすることを考えていましたが、ここまでくると全部Docker化してしまうとシンプルになるのでは?? ということでやりました。

神ロダのフロントエンドはシングルページアプリケーションなので、nginxイメージにnginx.confを書いて、フロントエンドのビルド成果物をCOPYして配信すればOKでした。アプリケーションサーバーに比べるとかなりシンプルに済みました。

CI/CDパイプライン

GitHub ActionsでDockerイメージをビルドする環境を整えたついでに、テストも走らせるようにしました。PRに対してテストが自動で走ると安心感が段違いですね。

docker-compose.ymlを用意しているのでCIでもそのまま使えばよい気もしますが、せっかくなのでGitHub Actions側にDB用のコンテナを用意します。GitHub Actionsのワークフローで使い捨てのMySQLを利用する(サービスコンテナ) | のりおが思考停止するブログ にあるような手順で用意すればよいですが、コンテナの準備完了を待たずにテストの直前でwait-for-it.shを走らせることで投機実行感を高めました。

まとめ

このようにして部内k8sクラスタに部員向けWebサービスを移設し、ついでに開発環境やCI/CDパイプラインを整えることができました。部員向けWebサービスとしては初めてのk8s移行だったので、いろいろ調べたり教えてもらったり、足りない設定を整備していったりしつつの作業でなかなか大変でしたが、完成してgit pushするだけで全てがデプロイされる体験はライフチェンジングでした。

また、アプリケーションをコンテナ環境でも動くように実装を修正したり、異なるRDBMS間でデータを移行したりするのはたぶん初めてのことだったので、いろいろ罠を踏み抜くことができたのはよかったと思います。

KMCM

KMCでは、部内k8sクラスタに部員向けサービスをデプロイしたい部員の方を募集しています。また、今年の新入生プロジェクトにはWebサービスの作り方を学べる勉強会があるようです。気になりますね~。

興味のある方は、以下の入部案内を参考にメールやTwitterのDMなどでいつでもご連絡ください。お待ちしております!

www.kmc.gr.jp

*1:もちろん現在も設置されています

*2:なんで壊れたのか不明

*3:実際には、バージョンアップに伴う非互換変更に対応していく必要もある

*4:KMCでは、itamaeを使って部内サーバーの構成管理を行っています

*5:https://sqlite.org/lang_altertable.html

*6:これSQLiteのときはなんでエラーにならなかったんでしょうか

*7:https://github.com/bitnami-labs/sealed-secrets#scopes