アロカップ

masakari exhibition

https://github.com/orgs/Carriage-Horse-Technologies/repositories

Go

React

AWS

Rust

Terraform

ハックツの布教(建前)をするために過去作品の展示会場を作ったよ!

notch_man

推しアイデア

ハックツの布教(仮)と我々の布教(本音)をするためにこれまでの作品を展示会場を作ったよ! それだけだと過去作のリブートなので、おまけで新作を作ったよ!まさかりを投げられるように技術力を上げよう!

作った背景

これまで、ハッカソンで暴れ回った弊チーム。しかし、4月から各々の新天地に旅立つことに。学生時代最後の思い出として、これまでの振り返りとかつてハックツハッカソンを荒らした人達の記憶を遺すための作品を作ったよ。

推し技術

今回は過去作のリブートを含めてマイクロサービスが立ち上がってるよ。それらを安定稼働させるためのインフラ力、アプリケーション設計などこれまでの知識と経験の総力が結集されてるよ! ハックツハッカソンを通じてこんな技術力が身につけられる!みんな、もっと出よう!!

プロジェクト詳細

作ったサービスについて

これまでの作品の展示会場を作りました。

展示会場を作ったよ!

Rust(WASM)を使って良い感じに展示会場を作りました。 GitHubのIDを入れると、登録してるアイコンでが動き回れます! 各作品の上でEnterを推すとサービスに遷移することが出来ます。 また、掲示アイテムをクリックすることで作品紹介を開くことが出来ます。

キャラクター操作方法

  • マウスでキャラをドラッグ
  • WASD or 矢印キー
  • 展示物をクリックでTopa'zのプロジェクトページが見れます
  • 展示物の場所に行き、Enterボタンを押すとその展示物のサイトにジャンプします

え、それだけかよ?

そう思ったあなた!ですよねですよね!過去作品のリブートをするだけでは面白くないのですよね? というわけで、新作をご用意しました。

新作は...

まさかりを投げてレガシーなサービスを倒そう!

というものです。UIやGitHubのソースコードを眺めて鋭い指摘をしましょう! 鋭ければ鋭いほど、まさかりの攻撃力が増しサーバーにダメージを与えることが出来ます。

なぜ、レガシーコードにまさかりを投げようと思ったのか?

コードは生み出したその瞬間からレガシーとなる(哲学) だから、私達は常に技術を追い続けより良いコードを書くようにしていかなければならない。 では、何が良くて何が悪いのか?

それを知る機会が中々無いのが実情です。だから、レガシーと向き合う最初のステップとしてレガシーと主張しているシステムのコードを端から端まで読み解き、鋭いまさかりを投げられるようなりましょう。車の運転と同じでプログラミングも「認知」「判断」「行動」が大事です。その「認知」を養うサービスとして本サービスを開発しました。

我々が本当に布教したかったもの....それはレガシーと向き合う沼みたいな楽しさ!そういう世界で戦うエンジニアの姿! 我々が辿り付いたエンジニア像の答えの1つです。

この世で動く大半のサービスはレガシーと戦いながら提供されている物です。 よおこそ、レガシーコードの世界へ。 レガシーと向き合い、レガシーを克服し、サービスを提供し続けられるエンジニアを目指そう!

きっと1年後にはこのコードもレガシーな物になります。リポジトリはパブリックなので、どんどんまさかりを投げてね!

(コードは自動生成されるようになっても、嗅覚は人間にしか無いからね?)

技術のお話(概要)

情報量が多すぎるので伝えたいところをピックアップします。

notch_man担当

技術スタック

  • React SPA(masakari-frontend)
  • Go(REST, WebSocket, Worker)
  • Legacy Stack(PHP7.4 + Smarty3)
  • Pupperteer
  • OpenAI(Chat API)
  • Ansible

notch_manの推し技術ポイント

  1. 展示会では全ユーザーの位置情報を収集して1秒毎にブロードキャストしています(座標だけ送って移動はフロントサイドで計算)
  2. まさかりはvegataというベンチマークツールを採用しました。安定性が渋いのでジョブキューを生やしています。
  3. サーバーからの有り難い言葉はOpenAPIで実装しています。ユーザーの入力もキャッシュに積んでリクエストしています。
  4. OpenAPIやサーバーステータスの収集APIにリクエストを飛ばし過ぎると辛いのでmemcachedでクエリキャッシュを挟んでいます
  5. masakari-frontendには手元でpupperteerでE2Eテストを動かしていました(forked from private library of fusioncomplab The University of Tsukuba)
  6. 文章類似度はジャロ・ウィンクラー距離を計算しています。これをrateRatioとしてmaxCountにかけて火力を決めてワーカーからリクエストを飛ばします

ニッシー☆担当

技術スタック

  • Terraform
  • AWS
  • Ansible
  • NewRelic

推し技術ポイント

インフラ構成図

このインフラを安定稼働させる技術、すごい!!

田中担当

技術スタック

  • Rust
  • WASM(yew)
  • AWS Lambda

押しの技術ポイント

  • 誰かがGoしか書かないけど、時代はRust!
  • チャット機能を作ったよ
  • 接続中の全ユーザーがGather風に表示されるよ
  • デザインに謎の拘りがあります

まさかりを投げられるレガシースタック

レガシーとは何か考えた時に、少なくとも可用性や冗長性のないものだと考え、EC2一台構成で組むことにしました。現代ならLightsailで簡単に構築できますが、そのようなものは無いと仮定しています。 EC2そのものはコンソールから手動で起動させましたが、EC2内部はAnsibleでプロビジョニングしています。地味に初めての経験でした。 まさか、ハッカソン中にSSHしてOSのアップデートや手動SSL証明書発行等をするとは思っていませんでしたが、原点回帰です。最初のハッカソンではEC2一台構成でした。 CentOS 7を選んだことがこだわりです。 SSHしたり、git pullで同期したり、docker compose up -dで環境起動させたり、SELinuxを無効化させていたり、あえてツッコミどころたくさんな環境にしています。レガシーとは(哲学)


多分、発表では伝えきれない推しポイント

何でも屋担当

どうも、何でも屋のnotch_manです。例の如く、フロントエンドからバックエンドアプリまで1人で立ち上げました。 最近は、研究室で運用中のクラウドソーシングプラットフォームの運用やら開発やらで精神を抉られているので徹底的な保守路線で開発をしました。その結果、堅実な物に仕上がったのではないでしょうか??

フロントエンド

新サービスmasakariのフロントエンドを実装しました。 これまで同様にReactを採用しました。これまで同様に堅実な設計ですが、WebSocketのState管理クラスのリファクタリングを行なうなどしてイベントドリブンにUIを変えやすいように設計を工夫しています。ビルドシステムにはViteを使っています、とても早い!

バックエンド

いつも通りGoでAPIを実装しています。WebSocketによる双方向通信を基本にしています。 実装した物は以下の通りです。

  • 展示会場のユーザー位置情報のプロバイダー
  • 展示会場のユーザーチャット基盤
  • masakariで使用するAPI
  • 各アプリケーションのコンテナ化などなど

工夫点は先ほどさらっと紹介した通り、以下の通りです。

  • 随所にキャッシュを挟んでいる
  • ジョブキューを使って非同期並行処理を実現
  • 様々なサービスを横断しつつも安定動作する設計

今回は外部サービスの呼び出しが多いですが、一般的に外部サービスの呼び出しはレスポンスまでの時間が増大する要因になります。 OpenAIは仕方ないとして、それ以外のサービスについてはキャッシュを挟むことで無駄な呼び出しと高速化を狙っています。サーバーのステータスは5分刻みでメトリックスが変わるのでキャッシュの有効期限を1分にしています。ローカルではmemcachedとredisを立ち上げて、本番ではElasticCacheを使っています。

まさかりはVegataというGo製のベンチマークツール使って投げています。ただ、公式リファレンス通りに実装すると大量にリクエストを投げた時にポートが枯渇してnet/httpがハングする問題に直面しました。net/httpがハングするとpanicが呼ばれてシステムが強制終了してしまうので安定性に欠けてしまいます。よって、この部分をWorkerとして切り出すことを目的にジョブキューを実装しました。実はPythonでは簡易的なジョブキューをフルスクラッチで実装した経験がありますが、Goではやったことがありませんでした。それを成果発表会15時間前にやるのは得策ではないのでasynqという薄いライブラリを作って仕組みを実装しました。 まさかりの攻撃力を決めるレートはジャロ・ウィンクラー距離という文字列類似度判定の計算式を利用しています。こちらは出力が既に標準化されており最大リクエスト数に対してratioとしてかけることで算出しています。ちなみに、最大マサカリ攻撃力は100、つまり秒間100リクエストをサーバーに送ることができます。これで大体5~12%くらいCPUを食うことが出来ます。

このバックエンドは以下の外部サービスを叩いています。

  • サーバーメトリクス収集用API
  • OpenAI API

そのため、外部サービスが応答しなくなる可能性なども考えないといけないです。その場合にキャッシュや固定値レスポンスを返すなど真摯な対応をすることで、極力ユーザー体験を損ねないように工夫しました。

ちなみに攻撃対象のレガシーサーバーはPHP7.4+Smarty3な古来のWebサービスって構成で構築をしています。負荷をかけるためのエンドポイントも用意しており、512個の配列をバブルソートさせるプログラムを走らせています。1アクセスにつき4プロセスが負荷を掛けて良い感じに攻撃が出来ます。

インフラ担当

インフラ担当のニッシー☆です。インフラ構成について説明します。

インフラ構成図

まずはこちらをご覧ください!(のちに1つずつ解説します)

前提と背景

今回は新規に3つのアプリがあります。

  • 歴代作品を展示する展示場アプリ
  • 新作アプリである「まさかり」アプリ
  • 「まさかり」アプリに必要なレガシーアプリ

展示場アプリとまさかりアプリはフロントはSPA、バックはコンテナアプリになっています。 レガシーアプリはEC2の上のdockerで動かすことになっていました。

ここがポイント

AWSのほとんどのリソースをIaCツールであるTerraformで用意しています

ElastiCacheやSecretsManagerはハッカソン中に初めて書きました。Terraformを用いている理由はハッカソンという短時間でもインフラ要件に対して高速で確実に応えるためです。CDKよりも早いことが多い所感です。

全てSSL化

以下はハッカソンで開発したフロントやバックエンドのホスト一覧です。SSL化はそれぞれのAWSリソース(CloudFront、Amplify、ALB、EC2)によってやり方が変わりますが、設定しました。

全ての環境にCI/CDが整えられています

  • GitHub Actionsに実装しています。
  • 自動デプロイはインフラ屋の作業負荷を減らせて最高です。

実はElastiCache(Redis、Memcached)が冗長化のために複数台立っている - 趣味です

レガシースタック

  • CentOS7のEC2で立っています
  • アプリケーションはDockerで動いています
  • PHP5系のコンテナはarm Mac環境で渋すぎたので諦めました。技術力不足...

詳細

展示場スタック

下記図の黄色の部分が中心です。

展示場フロントエンドスタックとバックエンドスタックに別れています。 フロントエンドスタックはアプリがRust製WASMであるため、AmplifyのCI上ではビルドできないのでGitHub Actions上でビルドし、コンテンツの配信基盤はCloudFrontとS3で作りました。 CloudFront*S3の組み合わせの場合はCloudFrontのキャッシュパージをデプロイごとに行う必要がありますが、AWS SDKとLambda、それからS3のイベント通知の機能を組み合わせることで自動化させています。 バックエンドスタックはTerraformでFargateを組んでおりElastiCacheと通信できるようにしています。 WebSocketを実装したアプリが動くため、SSL化の要件に応えています。 ECSタスクのデプロイにecspressoを用いています。詳細は省きますが、デプロイの自動化をする上でとても便利です。

まさかりアプリスタック

下記図の赤色の部分です。

バックエンドスタックには展示場アプリと同様にTerraformでFargateを構築しています。違いとしてはタスク定義内にバックエンドのサイドカーとしてWorkerアプリが追加されていることと、ElastiCache for Memcachedが追加されていることです。 他にはSecrets ManagerからOpenAI APIのトークンを取得することもしました。 EC2メトリクスを取得するスタックにはRustとLambdaを用いています。 cargo lambda deploy するだけでRustアプリがLambdaにデプロイされるので感動します。 フロントスタックはAmplifyにデプロイしているだけなので省略します。

他にもやったことがある気がしますが深夜帯で頭が回っていませんのでこの辺で失礼します。ニッシー☆でした。

展示場LP担当

展示場LP担当(とEC2メトリクスAPI)のTanakaです。 今回のテーマがふきょうということで自分はRustを布教するためにRust(yew)でフロントエンド開発を行いました。

yew

yewはRustのフロントエンドフレームワークでReactライクに開発することができます。(関数コンポーネントやhooksが使えます) Rustでフロントエンド開発を行うなら現状これが最善かなと個人的には思います。(最新のversion=0.20でSSRにも対応しました...!) スタイリングは周辺フレームワークが壊滅(数ヶ月更新されていない)状態なので無難にTailwind.cssを選択しました。

Rust Onlyでフロントエンドを書くメリット

Rustで書けること。以上。 何を言っているのかわからないと思いますが現在それ以外ないというのが私の感想です。 Rustなので高パフォーマンス!というのはWASMにおいてはそうだったりそうでなかったりします。WASMからDOMを操作するコストが結構高いのでベンチマークもそんなにスコアがでなかったり・・・。 とはいえ今後改善されるようなので皆さんもRustでフロントエンドを書きましょう。

e2eテスト

このアプリのe2eテストもRust(headless_chrome)で書きました! PuppeteerのRust版みたいなものです。

今回は展示場ということで某すぺしゃるなちゃっと風で展示物を見て回ることができるようにしました。 チャットもできるのでわいわいしてください!

メトリクス取得API

AWS SDK for Rust(CloudWatch)を用いてCloudWatchに流れてきたEC2のCPUUtilization等を取得してJSONで返しています。 exampleコードが全然なくてどうやって取得するかわからなかったのですが、他言語と設計自体は一緒なのでAWS SDK for pythonを参考にしてなんとかしました。

追記

CloudWatchのメトリクスだと5分か1分に一度しか取得できません。これだとユーザー体験が悪いのでリアルタイムでメトリクスを取得できるAPIをPHPとvmstatとawkで実装しました。

<?php $output = shell_exec("echo $(vmstat 1 2 | awk 'NR>3{ print 100 - $15 }')"); header('Content-Type: application/json'); echo json_encode(['cpuutilization' => floatval($output), "disk_read_bytes" => 0.0, "disk_write_bytes"=>0.0,"network_in"=>0.0,"network_out"=>0.0]);

それからsystemdで管理できるように構成ファイルも追加しました。

[Unit] Description=CPU usage API [Service] ExecStart=/usr/bin/php -S 0.0.0.0:8080 /home/centos/cpu-usage-provider/main3.php WorkingDirectory=/home/centos/cpu-usage-provider/ Restart=always User=nobody [Install] WantedBy=multi-user.target

これを実行することで下記のようにCPU使用率をJSONを得ることができるようになりました。(他の値はクライアントへのモックです)

{"cpuutilization":46,"disk_read_bytes":0,"disk_write_bytes":0,"network_in":0,"network_out":0}

今回のネストが深いコードです。これ以上にネストが深いコードを書いた方はこっそり教えて下さい。 (Rust言語特有の所有権の関係でブロックを作ってcloneしまくらないといけないで仕方のないことなんです)

notch_man

@notch_man8600