はじめに
おはもに~。 id:utgwkk です。最近の京都は夏のような日もあって、計算機にはつらい季節になりつつありますね。
今日は、部員向けWebサービス を部内Kubernetes (以下、k8s ) クラスタ に移設した話をします。
KMCでは、サークルの部内サーバーでk8s クラスタ を運用しています。KMCの部員であれば誰でも自由にアプリケーションをk8s クラスタ 上で稼動させることができます。k8s クラスタ を構築した経緯や技術的な詳細については、以下の記事をごらんください。
blog.kmc.gr.jp
今回移設した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 とか、単にアップデートするだけでは終わらないのが大変です。また、この方法だとダウンタイムが出てしまうので、深夜にこっそりバージョンアップ作業をやっていました。
Python バージョンアップの手順書 (一部)
コンテナ化していたら、言語処理系のバージョンを上げるのはbase imageのtagを書き換えてデプロイするだけで*3 完了するはずです。
依存モジュールや言語処理系のバージョンが構成管理されていない
ここでの依存モジュールとはPython のライブラリのことではなく、imagemagick やWebP、ネイティブの依存ライブラリなどを指しています。言語処理系のバージョンもとくに管理していない*4 ので、ちょっと sudo apt upgrade
をかけたら壊れてた、ということもあるかもしれません。半分ぐらいは構成管理をサボっていただけなのですが……。
SQLite は、MySQL やPostgreSQL などの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' }}
アプリケーションサーバ ーのDockerイメージができたので、デプロイを試せるようになりました。ここからk8s (kustomize) のYAML をゴリゴリ書いていくことになります。
k8s の公式ドキュメントや部内wiki にYAML の例があるのですが、イチから書くのは初めてなのでかなり id:nonylene さんにレビューしてもらいながらYAML を整えていきました。最初はお試し用のnamespaceで作って、あとからdefault namespaceに移動する方針にしました。
kustomize自体はk8s のYAML を生成するツールという感じで、生成されたYAML が妥当かどうかはデプロイしてみないと分からない場合も多く、何度か書き直してはデプロイし直す、を繰り返しました。 id:nonylene さんにk8s-lint というlinterを導入してもらってからは前もってエラーに気づきやすくなったと思います。
こうして試行錯誤して、いろいろなエラーを読み解きまくって、ついにアプリケーションサーバ ーをk8s クラスタ 上にデプロイすることができました。よかったですね。
次は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できなかったデータは数件だったので、目視確認して既存の別のタグに貼り替えるなどの対応を行いました。
MySQL をk8s クラスタ 上にデプロイしたあと、安全のために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