推しアイデア
ハックツの布教(仮)と我々の布教(本音)をするためにこれまでの作品を展示会場を作ったよ! それだけだと過去作のリブートなので、おまけで新作を作ったよ!まさかりを投げられるように技術力を上げよう!
ハックツの布教(仮)と我々の布教(本音)をするためにこれまでの作品を展示会場を作ったよ! それだけだと過去作のリブートなので、おまけで新作を作ったよ!まさかりを投げられるように技術力を上げよう!
これまで、ハッカソンで暴れ回った弊チーム。しかし、4月から各々の新天地に旅立つことに。学生時代最後の思い出として、これまでの振り返りとかつてハックツハッカソンを荒らした人達の記憶を遺すための作品を作ったよ。
今回は過去作のリブートを含めてマイクロサービスが立ち上がってるよ。それらを安定稼働させるためのインフラ力、アプリケーション設計などこれまでの知識と経験の総力が結集されてるよ! ハックツハッカソンを通じてこんな技術力が身につけられる!みんな、もっと出よう!!
これまでの作品の展示会場を作りました。
Rust(WASM)を使って良い感じに展示会場を作りました。 GitHubのIDを入れると、登録してるアイコンでが動き回れます! 各作品の上でEnterを推すとサービスに遷移することが出来ます。 また、掲示アイテムをクリックすることで作品紹介を開くことが出来ます。
そう思ったあなた!ですよねですよね!過去作品のリブートをするだけでは面白くないのですよね? というわけで、新作をご用意しました。
新作は...
まさかりを投げてレガシーなサービスを倒そう!
というものです。UIやGitHubのソースコードを眺めて鋭い指摘をしましょう! 鋭ければ鋭いほど、まさかりの攻撃力が増しサーバーにダメージを与えることが出来ます。
コードは生み出したその瞬間からレガシーとなる(哲学) だから、私達は常に技術を追い続けより良いコードを書くようにしていかなければならない。 では、何が良くて何が悪いのか?
それを知る機会が中々無いのが実情です。だから、レガシーと向き合う最初のステップとしてレガシーと主張しているシステムのコードを端から端まで読み解き、鋭いまさかりを投げられるようなりましょう。車の運転と同じでプログラミングも「認知」「判断」「行動」が大事です。その「認知」を養うサービスとして本サービスを開発しました。
我々が本当に布教したかったもの....それはレガシーと向き合う沼みたいな楽しさ!そういう世界で戦うエンジニアの姿! 我々が辿り付いたエンジニア像の答えの1つです。
この世で動く大半のサービスはレガシーと戦いながら提供されている物です。 よおこそ、レガシーコードの世界へ。 レガシーと向き合い、レガシーを克服し、サービスを提供し続けられるエンジニアを目指そう!
きっと1年後にはこのコードもレガシーな物になります。リポジトリはパブリックなので、どんどんまさかりを投げてね!
(コードは自動生成されるようになっても、嗅覚は人間にしか無いからね?)
情報量が多すぎるので伝えたいところをピックアップします。
技術スタック
notch_manの推し技術ポイント
技術スタック
推し技術ポイント
インフラ構成図
このインフラを安定稼働させる技術、すごい!!
技術スタック
押しの技術ポイント
レガシーとは何か考えた時に、少なくとも可用性や冗長性のないものだと考え、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による双方向通信を基本にしています。 実装した物は以下の通りです。
工夫点は先ほどさらっと紹介した通り、以下の通りです。
今回は外部サービスの呼び出しが多いですが、一般的に外部サービスの呼び出しはレスポンスまでの時間が増大する要因になります。 OpenAIは仕方ないとして、それ以外のサービスについてはキャッシュを挟むことで無駄な呼び出しと高速化を狙っています。サーバーのステータスは5分刻みでメトリックスが変わるのでキャッシュの有効期限を1分にしています。ローカルではmemcachedとredisを立ち上げて、本番ではElasticCacheを使っています。
まさかりはVegataというGo製のベンチマークツール使って投げています。ただ、公式リファレンス通りに実装すると大量にリクエストを投げた時にポートが枯渇してnet/httpがハングする問題に直面しました。net/httpがハングするとpanicが呼ばれてシステムが強制終了してしまうので安定性に欠けてしまいます。よって、この部分をWorkerとして切り出すことを目的にジョブキューを実装しました。実はPythonでは簡易的なジョブキューをフルスクラッチで実装した経験がありますが、Goではやったことがありませんでした。それを成果発表会15時間前にやるのは得策ではないのでasynqという薄いライブラリを作って仕組みを実装しました。 まさかりの攻撃力を決めるレートはジャロ・ウィンクラー距離という文字列類似度判定の計算式を利用しています。こちらは出力が既に標準化されており最大リクエスト数に対してratioとしてかけることで算出しています。ちなみに、最大マサカリ攻撃力は100、つまり秒間100リクエストをサーバーに送ることができます。これで大体5~12%くらいCPUを食うことが出来ます。
このバックエンドは以下の外部サービスを叩いています。
そのため、外部サービスが応答しなくなる可能性なども考えないといけないです。その場合にキャッシュや固定値レスポンスを返すなど真摯な対応をすることで、極力ユーザー体験を損ねないように工夫しました。
ちなみに攻撃対象のレガシーサーバーは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が整えられています
実はElastiCache(Redis、Memcached)が冗長化のために複数台立っている - 趣味です
レガシースタック
下記図の黄色の部分が中心です。
展示場フロントエンドスタックとバックエンドスタックに別れています。 フロントエンドスタックはアプリが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担当(とEC2メトリクスAPI)のTanakaです。 今回のテーマがふきょうということで自分はRustを布教するためにRust(yew)でフロントエンド開発を行いました。
yewはRustのフロントエンドフレームワークでReactライクに開発することができます。(関数コンポーネントやhooksが使えます) Rustでフロントエンド開発を行うなら現状これが最善かなと個人的には思います。(最新のversion=0.20でSSRにも対応しました...!) スタイリングは周辺フレームワークが壊滅(数ヶ月更新されていない)状態なので無難にTailwind.cssを選択しました。
Rustで書けること。以上。 何を言っているのかわからないと思いますが現在それ以外ないというのが私の感想です。 Rustなので高パフォーマンス!というのはWASMにおいてはそうだったりそうでなかったりします。WASMからDOMを操作するコストが結構高いのでベンチマークもそんなにスコアがでなかったり・・・。 とはいえ今後改善されるようなので皆さんもRustでフロントエンドを書きましょう。
このアプリのe2eテストもRust(headless_chrome)で書きました! PuppeteerのRust版みたいなものです。
今回は展示場ということで某すぺしゃるなちゃっと風で展示物を見て回ることができるようにしました。 チャットもできるのでわいわいしてください!
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しまくらないといけないで仕方のないことなんです)