🐻‍❄️

2分で(説明)できる(かもしれない)ハッカソン向けインフラ

2024/09/02に公開

先日、ハッカソンに参加したkyoya0819(Twitter, GitHub)です。

2日半という短い期間で、環境構築にかける時間はできる限り最小限にしつつも、セキュリティ等に配慮したアーキテクチャを目指しました。結果として、案外良い構成になったので共有したいと思います。

誤りがありましたら、ZennのコメントやTwitterのDM等でご指摘いただけますと幸いです。
発言は個人の経験, 知見に基づくものであり、いかなる所属, 関係組織を代表するものではありません。

デプロイしたアプリケーション

バックエンドがLaravel、フロントエンドがNext.js(Pages Router)で構成されています。
サブドメインでアクセスを分けており、今回の説明ではapi.example.comをバックエンド、example.comをフロントエンドとします。

この構成における一つのポイントは、Laravel SanctumのSPAモードを利用することでexample.comからのAPIリクエストをステートフルに行えるようにし、認証認可を可能にしています。

Next.jsに統一したほうが楽じゃない?

確かにそうかもしれませんが、今回は以下の理由で意図的に全く別のFWを採用しています。

LaravelかHonoかで最後まで迷ったのですが、最終的にはLaravelを採用しました。
(理由は↓↓↓↓↓

DB, JobQueue, AuthといったLaravelの恩恵を受けたかった

今回、アプリケーションの仕様上、複数の重い処理がありリクエストのたびに行うのはUX的に避けたいため、JobQueueを使う需要が高かったです。
AWSであればSQS + Lambdaであったり、SQS + EC2 & AWS SDKを用いれば同様のことが可能だと思いますが、後述するDBの恩恵も合わせるとあまりメリットがないと感じてしまいました。

Eloquentが最強ってのはみなさんご存知だと思うので。。。ね?
(ただ、laravel-ide-helperのバグにハマってしまい、TypeScriptへの憧れを感じてました。

また、LaravelにはAuthファサードという認証機能が存在したため、そちらを活用したかったという背景もあります。
(実際に認証関係は全てAuthファサードを利用しました

同じFWを同時にいじることによる事故を減らしたかった

ハッカソンになると、同じプロジェクトのメンバーが同時に少数のファイルに変更を加えます。
この時、Next.jsのみを採用するとAPI Routesに実装箇所を分けない限りgetServerSidePropsに少なからずロジックを書くこととなり、コンフリクトであったり予期せない挙動が起きる可能性があると考えました。

API Routesを導入するくらいなら、前述のようなLaravelのメリットを享受するためにフロント・バックを完全に分離した方が楽だと考えました。

単体で開発・テストしたかった

フロントエンドとバックエンドで同時に開発をしていくため、フロントエンドはフロントエンドでモックアップとして、バックエンドはバックエンドでPostmanなどを使ってテストをし、最終的にバックエンドとフロントエンドを結合するというプロセスを選択しました。
これにより、どちらかの開発を待つ時間をできる限り無くしたいという思惑を見事実現することができました。
(最悪動かなかった時にモックアップとしてデモできるので。。。

構成図

余談ですが、AWSのサービス画面のロゴって別に最新のロゴってわけじゃないんですね。。。
調べてて知りました。


ロゴ, アイコンは公式「AWS シンプルアイコン - AWS アーキテクチャーセンター | AWS」より

※構成図は全ての構成を詳しく書いてるわけではないです(書いてみたらカオスになった

ざっくり解説

基本的な指針は、

  1. 自分でサービスを立ち上げることなく、AWSのマネージメントサービスをそのまま利用する
  2. ELBを使うことでなるべくPrivate Subnet内にリソースを配置する

の2点です。

なぜマネージメントサービスを使うのか?

確かに、EC2やLightsailなどのVPS, VPSライクなサービスを使えば確かに初動の構築は楽かもしれません。しかし、以下のようなデメリットがあると「私は」考えています。

  • さまざまな物を1つのサーバー入れるため、性能の見極めが難しい
    • コンソール上からインスタンス停止 -> ストレージ切り離し -> 性能変更後再アタッチでできるけど...
  • 標準のdnfapt getでインストール可能なものが最新であるとは限らない
    • 現にAL2023の標準だとPostgreSQLが15までしか入らない(JSON型ネイティブサポートは16から
    • 他AMIでもEPELやらRemiを追加するくらいなら、最初からマネージメントサービスで最新バージョンを動かした方が早い

Public SubnetにEC2建ててIPv4公開すればいいじゃん

これには主に以下の理由があります。

  • セキュリティの観点で、EC2にPublic IPv4アドレスを割り当て80ポート, 443ポートを公開していい思いをあまりしたことがない
  • ドメイン運用の都合で、EC2に自己証明書を入れなければならず非常にめんどくさい大変
  • ELB(ALB)を利用するため、EC2をインターネット上に公開する必要がなかった

それでは、なぜELBを使うのか。
それはELBでEC2をターゲットグループとして設定することで、

  • リスナールールの条件を設定することで、リクエスト情報をもとにしてアクセスを分岐させることができる
  • ターゲットグループのポートを設定することでポートマッピングができる

という恩恵を受けることが可能だからです。

ELBでアクセス分岐を行う

今回、ELBでは以下のようなリスナールールを設定しています。

名前タグ 優先度 条件 アクション
App 100 HTTPホストヘッダが example.com app-tg
Api 101 HTTPホストヘッダが api.example.com api-tg
デフォルト 最後 固定レスポンスを返す(レスポンスコード:404)

これにより、example.comへのアクセスはapp-tgへ、api.example.comへのアクセスはapi-tgへ振り分けを行っています。

ターゲットグループを活用しポートマッピングを行う

例えば、3000ポートで動いているアプリケーションを80あるいは443で動作させるためには、Nginxでリバースプロキシを構築することが一般的だと思います。
しかし、同じEC2上でLaravelとNext.jsが動作しているのでNginxの構築が少しめんどくさい大変なこと、NginxのリバースプロキシとELBではELBの構築の方が簡単なことから、上記の方法は採用しませんでした。
(もちろんここは人それぞれ考え方が違うと思いますが。。。。

具体的には、以下のようなターゲットグループを構築しています。

app-tg
インスタンス ID 名前 ポート ゾーン
i-hogepiyo hackathon-server 3000 ap-northeast-1
api-tg
インスタンス ID 名前 ポート ゾーン
i-hogepiyo hackathon-server 80 ap-northeast-1

EC2へのSSHはどうするん?

今回、AWS Systems ManagerのSession Manager経由でSSHを行なっています。
IAM Identity Centerを導入しているので、AWS CLI v2を用いることでSSOで認証情報を取得できるので便利です。

また、Private SubnetにあるEC2のSSM AgentがSystems Managerエンドポイントに到達するためには、VPCエンドポイント or NATゲートウェイが必要なため、もしも似たような構成で失敗する場合はVPC周りの設定を確認してみてください。

その他

NAT GatewayかVPCエンドポイント

今回、EC2から外部のPackagistやnpm等のリソースにアクセスする必要があったため、VPCエンドポイントではなくNAT Gatewayを使っています。

https://dev.classmethod.jp/articles/reduce-unnecessary-costs-for-nat-gateway/

またNAT Gatewayの場合、転送がなくても固定費がかかるのと、AWSからのデータアウトが多い場合料金が高額になります。
今回の場合、ハッカソン期間中の運用だったのと、AWSからのデータアウトはほぼ発生しない計画だったためNAT Gatewayを選択しました。

NAT GatewayとPrivate Subnet

RDSとElastiCacheは、NAT Gatewayに繋がるPrivate Subnetである必要はないです。
そのため、別のPrivate Subnetを用意したほうが良いのかもしれませんが、時間的制約から今回はSecurity Groupでアウトバンドルールを全て拒否しているので問題ないとして一回パスしました。

なぜus-east-1のBedrockにアクセス?

開発初期段階で、複数のAI言語モデルを比較するため、より多くのモデルが利用可能us-east-1リージョンを選択しました。
最終的にはClaude 3 Haikuを使用することになり、ap-northeast-1リージョンでも利用可能でした。しかし、時間的制約のため、既に申請, 設定済みのus-east-1リージョンでデプロイを進めることにしました。

ちなみに、EC2にアタッチしたロールには以下のようなInline Policyを定義しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "bedrock:InvokeModel",
            "Resource": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0",
            "Effect": "Allow"
        }
    ]
}

最後に

昨年, 一昨年とハッカソンの運営側にはなったことがあるのですが、参加者として参加するのは3年ぶり?くらいで、とても楽しい3日間でした。
IaC(CDK)をもっと勉強して、すんなりコードベースでインフラを構築できるようになりたいです。


2分じゃ絶対説明できなかったですね。

Discussion