Upgrade to Pro — share decks privately, control downloads, hide ads and more …

S3静的ホスティング+Next.js静的エクスポート で格安webアプリ構築

S3静的ホスティング+Next.js静的エクスポート で格安webアプリ構築

登壇資料

つながりテック #2 AWSトークで締めくくる年度末LT大会 ~初登壇も大歓迎!~
https://tunagari-tech.connpass.com/event/344667/

iharuoru

March 26, 2025
Tweet

Other Decks in Programming

Transcript

  1. コスト最適化 無料利用枠の活用 サービス 無料利用枠 期間 CloudFront 月間50GBのデータ転送アウト 12ヶ月間 S3 5GBまで無料

    12ヶ月間 API Gateway 月間100万回のAPI呼び出し 12ヶ月間 Lambda 月100万リクエストまで無料 常に無料 DynamoDB 25GBまで無料 常に無料 11
  2. ページルーティング frontend/ # Next.js ├── app/ # ページ・API実装 │ ├──

    page.tsx # トップページ │ └── detail │ └── page.tsx # 詳細ページ ├── components/ # UIコンポーネント └── lib/ # 共通ロジック Next.jsではディレクトリに対応したルーティン グが作られる / /detail?id={id} 13
  3. クエリパラメータの取得 next/navigationの useSearchParams で取得 function TodoDetail() { const searchParams =

    useSearchParams(); const id = searchParams.get('id'); useEffect(() => { const data = await fetchTodo(id); setTodo(data); }, [id]); } 14
  4. クエリパラメータの取得 動的ルーティング スラグ( [id] )を使って /detail/[id] とアクセスできる 静的エクスポートするためには idを指定しなければいけない app/

    └── detail └── [id] # スラグ └── page.tsx # 詳細ページ 今回はTODOを追加するたびにidが生成されるので 15
  5. 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
  6. 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
  7. CDK構成 エントリーポイント スタック コンストラクト バックエンド API フロントエンド infrastructure/ # CDK

    ├── bin/ # エントリーポイント │ └── todo-app.ts └── lib/ # スタック定義 ├── todo-app-stack.ts └── constructs/ # 各コンストラクト ├── todo-backend.ts ├── todo-api.ts └── todo-frontend.ts 19
  8. エントリーポイント・メインスタック 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 最終的なプロジェクト構成 . ├── frontend/ # Next.js フロントエンド │ ├── app/

    # ページ・API実装 │ ├── components/ # UIコンポーネント │ └── lib/ # 共通ロジック ├── backend/ # Lambda関数 │ └── functions/ # CRUD操作の実装 └── infrastructure/ # CDKスタック ├── bin/ # エントリーポイント └── lib/ # スタック定義 30
  16. まとめ サーバーレスアーキテクチャ S3静的ホスティング+α 動的なサイトも実現可能 インフラのコード化 CDKによる一貫した管理 再利用可能なコンストラクト 適切なアクセス制御 S3 OAC

    API Gateway カスタムヘッダー認証 AWS無料利用枠の活用 主要サービスが無料枠内 低コストでの本番運用が可能 を初心者でもデプロイできた! 32