この記事はミクシィグループ Advent Calendar 2021 18日目の記事です。
LifeMemoryTeamの@atponsです。今回のSECCON CTF 2021も参加いただきありがとうございました。
SECCON 2021 CTFのスコアサーバーとそのインフラ構成についてまとめて反省しようと思います。
SECCON CTF 2021
SECCONは日本の情報セキュリティコンテストイベントです。@atponsはボランティアでSECCON実行委員/LifeMemoryTeamという有志の団体のメンバーとしてインフラ周りの整備/NOCを行っています。その中でも、SECCON CTFはSECCONが実施するCTF (Capture The Flag)です。2019年までは本戦などはオフラインCTFとして実施していましたが2020年よりオンラインCTFとして実施しています。今回のSECCON CTF 2021では、
- SECCON初の賞金付き大会として実施(総額100万円)
- 総問題数28問 (Jeopardy)
- 1400以上のユーザー/500以上のアクティブチーム(ざっくり集計なので、正確な値は公式のアナウンスでご確認ください)
という内容で実施しました。
今回は豪華な作問者に加え、賞金やスポンサー賞などのコンテンツを用意し、たくさんの方にSECCON CTF 2021をお楽しみいただきました。ありがとうございました。
このCTFを実施したスコアサーバーについて実際にまとめていこうと思います。
スコアサーバー基盤
今年の5月に開催されたSECCON Beginners CTF 2021で利用した、LMTdを引き続き利用しました。基本的なコンポーネントはスコアサーバーとして動作するサーバー (Scoreserver Pod)と、管理画面用のAPIサーバー (Admin Pod)、スコアを集計しランキングを生成しMemcachedに載せるサービス (Score Aggregation Pod)があります。ランキングは基本的にMemcachedから返しますが今回はそれ以外は特にキャッシュはしていません。User Collector Jobと書いてあるのは開催後に各種データベースからユーザー情報を集計するKubernetes Jobです。
この他に↓にあるNICTのAMATERAS 千と呼ばれる可視化システムへの連携部分の作り込みも行いました。これはGatewayサービスとして別途データベースも分離して開発し、今回は競技時間中Twitchで配信するという試みを行いました。かっこいい。
#SECCON CTF 2021 has started!!https://t.co/zv2ZFqRs1s
— SECCON (@secconctf) 2021年12月11日
During the contest, you can watch the super cool visualization of CTF. This AMATERAS Thousand is developed and streamed by #NICT !! pic.twitter.com/gp5YmfsNdL
基本的なインフラ構成は前回と同様です。ログも今回もFluent Bitに生ログを全部渡すのと、LokiとPromtailによる可視化のための収集をどちらも行いました。また、今回もMicrosoft Azureとさくらのクラウドに構築されたインフラを利用させていただきました。この場を借りてお礼申し上げます。
利用した技術スタックはこちらです。
- Azure Kubernetes Service
- Azure Database for MariaDB
- Firebase Authentication
- Cloud Firestore
- Flux CD
- Azure Key Vault
- memcached
- Go
- GORM
- oapi-codgen
- uber-go/zap
- ozzo-validation
- tbls
- Bazel
- Grafana Loki
- Promtail
- Fluent Bit
- Prometheus
- さくらのクラウド
- Next.js
今回導入した技術スタックは太字で記載しています。
LMTdのバックエンドの開発は、今回については完全に一人でやりました。今回は上手くいくかな、と思って見ましたがやはり開催してみて、反省がいくつかありました。
前回の反省とその対策
- 認証周りをほとんど独自実装して載せたこと
- 認証系をバックエンドで処理したこと
- フロントエンドのサポートが手厚いIDaaSを利用したい
- バックエンドのコードを綺麗にしたい
- DIする
- DDDする
- テストを書く(ほとんどテストがなかった)
- APIの定義をちゃんとしたい
- ドキュメントをちゃんと書く、フロントエンドライブラリに切り出す
- 普通のコードにする
- 何かあっても他の人が見て分かるコードにする
これらを踏まえて,今回は以下を実施しました。
- 認証周りの乗換え
- Firebase Authenticationの利用
- バックエンドのコードを綺麗にする
- DDDのパターンに近いアーキテクチャを導入
- インターフェースを用意してDIをする
- テストを書く
認証周りの乗換え
これに関してはFirebase Authenticationを導入してフロントエンドに処理を寄せながら、セッションの払い出しなどはサーバーから行いました。
バックエンドのコードを綺麗にする
まず、バックエンドのコードを綺麗にするためにDDDのようなアーキテクチャを導入しました。これまではMVC + Serviceみたいな自分にしか分からないコードになっていましたが大分他のコードに近づいたのかなと思います。
また、DIをするようにしてテストは簡単にできるようにしました。ただし、DBに依存したテストも書くことがあるのでDATADOG/go-txdbを活用してテストの並列化を上げてできるだけ t.Parallel()
できるようにしました。
今回は前回導入したORMであるentではなくGORMを採用しました。これはentはコードを生成できる良いORMではありましたが、entに関する知識が必要になるため自分以外が触れなくなってしまうという反省があったので今回はごくごく使われているORMであるGORMを採用しました。
テストを書く
HTTPルーターのテストカバレッジは6割ぐらいをキープしてました。大事なところはとりあえずテストでカバーしていたので競技中に致命的なバグが見つかることはありませんでした。gavv/httpexpectを活用してリクエスト/レスポンスの検証をしていました。
その他
今回はOpenAPIを採用し、APIのスキーマをフロントエンドと共有しながら開発を進めていきました。バックエンド側でもフロントエンド側でも同じスキーマを共有して開発できるのはかなりメリットでした。
他に今回やってよかったこと
Cloud Firestoreの利用
今回もSECCONでは競技参加者へのアンケートを実施しましたが、アンケートの結果については聞く内容が直前まで変化することがあるため、スキーマにある程度柔軟にできるCloud Firestoreを導入しました。サーバ側で検証する必要があるため特段Firebase Authenticationとの連携はしていませんが、柔軟に対応できたという点で良かったです。
また、クオータに気をつける必要があり、アンケートの回答結果は変化しないことから基本的にFirestoreへはWriteのみを行うように、Readは基本的にはスコアサーバーのmemcachedを経由させています。
tblsの利用
今回は「他の人がみても分かる」コードにしたかったのでDBのスキーマも想定外のものが完成しないかに気をつける必要がありました。そのため、k1LoW/tblsを導入しました。 データベースの現状のスキーマからドキュメントを生成しつつ確認できたのは良かったです。
管理画面のNext.jsの利用
これはまだ実験的ですが、主催者が見れる管理画面をNext.jsにしました。以前はhtml/templateだったのでそれからはちょっと進化したと思います。この管理画面のAPIについてもOpenAPIで管理しつつやっています。OpenAPIについてはaspida/aspidaでYAMLからクライアントの自動生成が出来て非常に助かりました。
Continuous Deliveryをちゃんとやる
スコアサーバーはいくつかのコンテナイメージからなり、GitHub Actionsでイメージをプッシュし、その後Flux CD用のリポジトリでKustomizeのYAMLを新しいイメージにする作業(インフラリポジトリにPRする)のが億劫になっていました。これを踏まえてCIでイメージを生成したら、Kustomizeのイメージタグを書き換えてPRを出すボットを作成しました。
上記のような通知とともに、
このようなPRが作成されるのであとはマージすればデプロイされるという仕組みになりました。コンテナイメージのビルドにBazelを使ってる関係もあるので自分達でこのようなツールを作りましたが、他でも全然活用できるツールだなと思いました。
リポジトリ層へのアクセスにアクセスコンテキストを引き回してRequest IDをつける
これは普通の事なんですが、HTTPルーターから受け取ったリクエストでRequest IDをつけてDBへのクエリするところまでもこれを引き回すとエラーになったところが追いやすくなるのでちゃんとやって良かったです。
今回を踏まえた反省
負荷対策が甘かった
競技開始直後、アクセスの急増に伴い数分間アクセスしづらい現象が発生しご迷惑をおかけしました。これは今回入れた競技状態を入れているテーブルへのアクセス増加にともない、アクセスが困難になりました。
- 競技開始後、アクセスが急増する
- ルーターのミドルウェアに競技開始を見に行くコードがあるのでそこでアクセスが増加
- MySQLのコネクションが増加してMySQLがアクセス不可になる(ネットワークI/O増)
- CPU、メモリ、ネットワークというあらゆる負荷の増加に伴いスコアサーバーが入っているKubernetesクラスタがやられて、監視系(Grafana/Promtail/Prometheusなど)もダウン
という感じでした。数分後にアクセスが落ち着きましたがこれは十分な負荷試験ができてなかったことの裏返しでもあります。もう少し検証するべきだったと思います。アクセス急増に伴い最初はKubernetesノードの異常なCPU利用率からノードプールへのノード追加で収まると思い、追加しました。結果的に原因はデータベースへの接続部分でしたが、こうしてすぐ負荷をオフロードできたのはKubernetesの良かったところかなと思います。
開始直後と終了直後のアクセスすごい...
適切なアーキテクチャだったのかどうかが分からない
開発は一人だったのでこれが正解だったのかは分かってません...。個人的にはいいなと思う設計もいくつかありましたので収穫にはなりました。
おわり
以上でSECCON CTF 2021のスコアサーバー(バックエンド)編を振り返りました。2021/12/18-19開催のSECCON CTF 2021電脳会議の19日のセッション(12:30-13:30)でもお話できたらと思います。よろしくお願いします。
他にも何か思い出したら書こうと思います。
※ SECCONおよびLifeMemoryTeamは所属組織とは関係なく、業務外で行っている活動です