Slide 1

Slide 1 text

S3静的ホスティング+Next.js静的エクスポート で格安webアプリ構築 CDKで構築するTODOアプリ Takai Haruka 1

Slide 2

Slide 2 text

自己紹介 Takai Haruka 株式会社Ridge-i AIアプリエンジニア(Python, LangChain) モンハン好き X, Zenn(@iharuoru) AWS, Web初心者なので間違えている点は遠慮なくご指摘ください! 2

Slide 3

Slide 3 text

アジェンダ 1. 目的と概要 LTの目的 作成するアプリケーション 2. 実装内容 アーキテクチャ コスト最適化 静的サイトホスティング CDKによるインフラ構築 3. まとめと展望 プロジェクト構成 今後の展望 3

Slide 4

Slide 4 text

LTの目的 初心者でもデプロイしたい AWS無料枠の活用 請求に怖がらずに実装したい インフラ構築の実践 AmplifyじゃなくCDKで勉強 したい 4

Slide 5

Slide 5 text

今回作るもの 普通のTODOアプリ 機能 追加 一覧表示 更新・削除 詳細 実装 フロント Next.jsの静的エクスポート インフラ CDK 5

Slide 6

Slide 6 text

TODOアプリ例 6

Slide 7

Slide 7 text

トップページ TODO追加フォーム 入力フィールド 送信ボタン TODO一覧表示 編集リンク 削除ボタン 7

Slide 8

Slide 8 text

トップページ 編集モーダル 入力フィールド 送信ボタン キャンセルボタン 詳細ボタン 8

Slide 9

Slide 9 text

詳細ページ TODO詳細表示 戻るボタン 9

Slide 10

Slide 10 text

アーキテクチャ サーバーレス S3とAPI Gatewayをどちらもオリジンに設定 10

Slide 11

Slide 11 text

コスト最適化 無料利用枠の活用 サービス 無料利用枠 期間 CloudFront 月間50GBのデータ転送アウト 12ヶ月間 S3 5GBまで無料 12ヶ月間 API Gateway 月間100万回のAPI呼び出し 12ヶ月間 Lambda 月100万リクエストまで無料 常に無料 DynamoDB 25GBまで無料 常に無料 11

Slide 12

Slide 12 text

Next.jsで静的サイト実装 12

Slide 13

Slide 13 text

ページルーティング frontend/ # Next.js ├── app/ # ページ・API実装 │ ├── page.tsx # トップページ │ └── detail │ └── page.tsx # 詳細ページ ├── components/ # UIコンポーネント └── lib/ # 共通ロジック Next.jsではディレクトリに対応したルーティン グが作られる / /detail?id={id} 13

Slide 14

Slide 14 text

クエリパラメータの取得 next/navigationの useSearchParams で取得 function TodoDetail() { const searchParams = useSearchParams(); const id = searchParams.get('id'); useEffect(() => { const data = await fetchTodo(id); setTodo(data); }, [id]); } 14

Slide 15

Slide 15 text

クエリパラメータの取得 動的ルーティング スラグ( [id] )を使って /detail/[id] とアクセスできる 静的エクスポートするためには idを指定しなければいけない app/ └── detail └── [id] # スラグ └── page.tsx # 詳細ページ 今回はTODOを追加するたびにidが生成されるので 15

Slide 16

Slide 16 text

CRUD操作 frontend/ # Next.js フロントエンド ├── app/ # ページ・API実装 ├── components/ # UIコンポーネント └── lib/ # 共通ロジック └── api.ts # CRUD操作関数 一覧表示に fetchTodos 削除ボタンに deleteTodo etc を割り当て // fetchTodos fetch(`${API_ENDPOINT}/todos/`) // fetchTodo fetch(`${API_ENDPOINT}/todos/${id}`) // createTodo fetch(`${API_ENDPOINT}/todos/`, {...}) //updateTodo fetch(`${API_ENDPOINT}/todos/${id}`, {...}) // deleteTodo fetch(`${API_ENDPOINT}/todos/${id}`, {...}) {...} には method, headers, body が入る 16

Slide 17

Slide 17 text

Next.jsビルド設定 next.config.js 静的エクスポート設定 S3ホスティング用の画像設定 const nextConfig = { // 静的エクスポート設定 output: 'export', // S3ホスティング用の画像設定 images: { unoptimized: true }, } module.exports = nextConfig; npm run build で/outに html, css, js などが出力される これをS3に入れるだけ! 17

Slide 18

Slide 18 text

CDKでインフラ実装 18

Slide 19

Slide 19 text

CDK構成 エントリーポイント スタック コンストラクト バックエンド API フロントエンド infrastructure/ # CDK ├── bin/ # エントリーポイント │ └── todo-app.ts └── lib/ # スタック定義 ├── todo-app-stack.ts └── constructs/ # 各コンストラクト ├── todo-backend.ts ├── todo-api.ts └── todo-frontend.ts 19

Slide 20

Slide 20 text

エントリーポイント・メインスタック todo-app.ts エントリーポイント メインスタックを呼び出し 複数スタックを 同時にデプロイ可能 const app = new cdk.App(); // CDKアプリの初期化 new TodoAppStack(app, 'TodoAppStack') // メインスタックの呼び出し todo-app-stack.ts メインスタック コンストラクトを呼び出し CFnの1単位になる export class TodoAppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { // バックエンド → API → フロントエンドの順でデプロイ const backend = new Backend(scope, 'Backend'); const api = new Api(scope, 'Api', { lambdaFunctions: backend.lambdaFunctions }); const frontend = new Frontend(scope, 'Frontend', { apiEndpoint: api.apiEndpoint }); } } 20

Slide 21

Slide 21 text

Backend コンストラクト DynamoDBテーブル id: PK Lambda関数 IAMロール設定 などを定義 export class TodoBackend extends Construct { constructor(scope: Construct, id: string) { super(scope, id); // DynamoDB テーブルの作成 this.todoTable = new dynamodb.Table(this, 'TodoTable', { partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, removalPolicy: cdk.RemovalPolicy.DESTROY, }); // Lambda関数の作成 this.lambdaFunctions = { getTodos: new nodejs.NodejsFunction(this, 'GetTodosFunction', { ...commonProps, entry: path.join(__dirname, 'getTodos/index.ts'), }), // Lambda関数にDynamoDBへのアクセス権限を付与 this.todoTable.grantReadWriteData(this.lambdaFunctions.getTodos); } } 21

Slide 22

Slide 22 text

Apiコンストラクト API Gateway REST API エンドポイント /todos /todos/{id} Lambdaインテグレーション などを定義 export class TodoApi extends Construct { constructor(scope: Construct, id: string, props: TodoApiProps) { // API Gatewayの作成 this.api = new apigateway.RestApi(this, 'TodoApi', { restApiName: 'Todo API', }); // /todos エンドポイントの作成 const todos = this.api.root.addResource('todos'); // GET /todos todos.addMethod('GET', new apigateway.LambdaIntegration(props.lambdaFunctions.getTodos), { methodResponses: [{ statusCode: '200', responseParameters: SECURITY_HEADERS }] }); // /todos/{id} エンドポイントの作成 const todo = todos.addResource('{id}'); // GET /todos/{id} todo.addMethod('GET', new apigateway.LambdaIntegration(props.lambdaFunctions.getTodo), { methodResponses: [{ statusCode: '200', responseParameters: SECURITY_HEADERS }] }); } } 22

Slide 23

Slide 23 text

Frontend コンストラクト S3バケット (静的ホスティング) CloudFront ディストリビューション S3オリジン API Gatewayオリジン などを定義 export class TodoFrontend extends Construct { constructor(scope: Construct, id: string, props: TodoFrontendProps) { // S3バケットの作成 this.websiteBucket = new s3.Bucket(this, 'TodoWebsiteBucket', { removalPolicy: cdk.RemovalPolicy.DESTROY, autoDeleteObjects: true, enforceSSL: true, }); // CloudFrontディストリビューションの作成 this.distribution = new cloudfront.Distribution(this, 'TodoDistribution', { defaultBehavior: { origin: origins.S3BucketOrigin.withOriginAccessControl(this.websiteBucket), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, }, additionalBehaviors: { 'todos*': { origin: new origins.RestApiOrigin(props.todoApi), cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, // キャッシュを無効化 viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, // HTTPSへリダイレクト allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, // すべてのHTTPメソッドを許可 }, }, defaultRootObject: 'index.html', }); } } 23

Slide 24

Slide 24 text

CDKの細かい設定 24

Slide 25

Slide 25 text

S3オリジン オリジンアクセスコントロール (OAC) S3の直接アクセスを禁止する // CloudFront OACの作成 const originAccessControl = new cloudfront.S3OriginAccessControl(this, 'OAC', { signing: { protocol: cloudfront.SigningProtocol.SIGV4, behavior: cloudfront.SigningBehavior.ALWAYS, }, }); // CloudFrontディストリビューションの作成 this.distribution = new cloudfront.Distribution(this, 'TodoDistribution', { defaultBehavior: { origin: origins.S3BucketOrigin.withOriginAccessControl(this.websiteBucket, { originAccessControl: originAccessControl, }), // 他の設定... }, }); 25

Slide 26

Slide 26 text

アクセスの確認 CloudFrontから https://{ディストリビューションドメイン名}.cloudfront.net/index.html S3 オブジェクト URLから https://{S3バケット名}.s3.ap-northeast-1.amazonaws.com/index.html 26

Slide 27

Slide 27 text

API Gatewayオリジン パスパターン マッチしたらオリジンを振り 分ける( todos* ) オリジンパス ドメイン名の後に追加するパ ス( /prod ) // CloudFrontディストリビューションの作成 this.distribution = new cloudfront.Distribution(this, 'TodoDistribution', { additionalBehaviors: { 'todos*': { origin: new origins.RestApiOrigin(props.todoApi, { originPath: '/prod', }), }, }, }); // API Gatewayの作成 this.api = new apigateway.RestApi(this, 'TodoApi', { restApiName: 'Todo API', deploy: true, deployOptions: { stageName: 'prod', description: 'Production stage', } }); {cloudfrontドメイン}/details → {S3ドメイン}/details {cloudfrontドメイン}/todos → {API Gatewayドメイン}/prod/todos 27

Slide 28

Slide 28 text

API Gatewayオリジン カスタムヘッダー CloudFrontで設定できるリ クエストヘッダー API Gatewayで検証すること で直接アクセスを禁止する // CloudFrontディストリビューションの作成 this.distribution = new cloudfront.Distribution(this, 'TodoDistribution', { additionalBehaviors: { 'todos*': { origin: new origins.RestApiOrigin(props.todoApi, { originPath: '/prod', customHeaders: { 'Referer': props.customReferer }, }), }, }, }); // API Gatewayの作成 this.api = new apigateway.RestApi(this, 'TodoApi', { restApiName: 'Todo API', defaultCorsPreflightOptions: { allowHeaders: ['Content-Type', 'Authorization', 'Referer'], }, policy: new iam.PolicyDocument({ statements: [ // カスタムヘッダーのRefererを検証するポリシー new iam.PolicyStatement({ effect: iam.Effect.DENY, principals: [new iam.AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], conditions: { StringNotEquals: { 'aws:Referer': props.customReferer } } }) ] }), }), 28

Slide 29

Slide 29 text

アクセスの確認 CloudFrontから https://{ディストリビューションドメイン名}.cloudfront.net/todos API Gatewayから https://{API Gatewayドメイン名}.execute-api.ap-northeast-1.amazonaws.com/prod/todos 29

Slide 30

Slide 30 text

最終的なプロジェクト構成 . ├── frontend/ # Next.js フロントエンド │ ├── app/ # ページ・API実装 │ ├── components/ # UIコンポーネント │ └── lib/ # 共通ロジック ├── backend/ # Lambda関数 │ └── functions/ # CRUD操作の実装 └── infrastructure/ # CDKスタック ├── bin/ # エントリーポイント └── lib/ # スタック定義 30

Slide 31

Slide 31 text

今後の展望 Amplifyに近づけるために LambdaでSSR CodeBuildでCI/CD 31

Slide 32

Slide 32 text

まとめ サーバーレスアーキテクチャ S3静的ホスティング+α 動的なサイトも実現可能 インフラのコード化 CDKによる一貫した管理 再利用可能なコンストラクト 適切なアクセス制御 S3 OAC API Gateway カスタムヘッダー認証 AWS無料利用枠の活用 主要サービスが無料枠内 低コストでの本番運用が可能 を初心者でもデプロイできた! 32