AWS SAM：Cognito認証付きアプリを構築してみたハンズオンやってみた！

## はじめに

AWS SAM（Serverless Application Model）を使って、Cognito認証付きのサーバーレスアプリケーションを構築してみました。

昨夜のうちに検証して投稿しようと頑張っていたのですが、検証でうまくいかない部分があり、最終的にまとめ終わったのは翌朝になってしまいました…！（***思った以上に手こずりました...Cognito久しぶりだったから***）

今回は、S3によるWebホスティング、CloudFrontによるHTTPS配信、API Gateway + Lambda + DynamoDB のバックエンド、そしてCognitoによるユーザー認証を統合した構成です。

***AWS SAM 本当に便利で連日、感動中です...！！***

## 書こうと思ったきっかけ

個人的に「**SAMでCognitoを使ってみたい！！**」という気持ちがずっとありました。普段は手動でCognitoを設定していましたが、インフラをすべてコード化することで理解が深まると思い、試してみることにしました...！

**前回の記事の追加検証になります！**

https://qiita.com/free-honda/items/ff66081fc59237bb7fb0

**構成図はこんな感じになります（CloudFrontとCognitoを追加）**

![Screenshot 2025-06-11 at 7.23.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/24093b4c-fcc1-4977-84d3-7e0395bebed6.png)

※内容に不備などがございましたら、お手数ですが優しくご指摘いただけますと幸いです。

## ディレクトリ構成

本構成のディレクトリ構成は以下のようになっています。

```bash
sam-cognito-api-example/
├── template.yaml         # SAMテンプレート（Cognito追加あり）
├── samconfig.toml        # デプロイ設定ファイル
├── hello_world/
│   ├── app.py            # Lambda関数（DynamoDB読み取り）
│   └── __init__.py       # 空でOK
└── frontend/
    ├── index.html        # Cognitoログインボタンあり
    ├── callback.html     # トークン取得・API呼び出し
    └── script.js         # トークン処理＆APIアクセス
```

※今回は、フロントエンドやバックエンドのコードには触れず、SAM テンプレートに焦点を当てて、デプロイ手順を中心に解説していきます。

## テンプレート内容

今回の構成に含まれるリソースは以下の通りです：

* **S3バケット**：`index.html` をホスティングする静的ウェブサイト
* **CloudFront**：HTTPSでの配信対応（`redirect-to-https`）
* **DynamoDB**：`ItemsTable` に保存されたデータをAPI経由で取得
* **Cognito User Pool / Client / Domain**：ログイン機能を提供
* **API Gateway + Lambda**：APIとして`/items`エンドポイントを提供し、DynamoDBの内容を返す
* **Cognito Authorizer**：API Gateway によるCognito認証

このテンプレートでは、CloudFormationテンプレートでCognito認証付きの構成全体を自動展開できるようになっています。

ユーザーは、CloudFront経由で配信されるWebアプリにアクセスし、Cognitoログイン後にAPIを呼び出してDynamoDBのデータを取得するという流れです。

以下は、実際に使用したテンプレートコードです：

```yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: API Gateway + Lambda + DynamoDB + S3 + Cognito Auth

Globals:
  Function:
    Timeout: 10

Resources:

  WebHostingBucket:
    Type: AWS::S3::Bucket
    Properties:
      WebsiteConfiguration:
        IndexDocument: index.html
      OwnershipControls:
        Rules:
          - ObjectOwnership: ObjectWriter
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref WebHostingBucket
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub "${WebHostingBucket.Arn}/*"
            Principal: "*"

  ItemsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ItemsTable
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  MyUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: MyUserPool
      AutoVerifiedAttributes:
        - email
      UsernameAttributes:
        - email

  # CloudFront Distribution for HTTPS
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - DomainName: !Select [2, !Split ["/", !GetAtt WebHostingBucket.WebsiteURL]]
            Id: S3Origin
            CustomOriginConfig:
              HTTPPort: 80
              OriginProtocolPolicy: http-only
        Enabled: true
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods: [GET, HEAD]
          CachedMethods: [GET, HEAD]
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
        Comment: !Sub "${AWS::StackName} CloudFront Distribution"

  MyUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: MyUserPoolClient
      UserPoolId: !Ref MyUserPool
      GenerateSecret: false
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthFlows:
        - implicit
      AllowedOAuthScopes:
        - email
        - openid
      CallbackURLs:
        - !Sub "https://${CloudFrontDistribution.DomainName}/callback.html"
      SupportedIdentityProviders:
        - COGNITO

  # Cognito User Pool Domain（標準ドメイン）
  MyUserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: !Sub "myapp-${AWS::StackName}-${AWS::AccountId}"
      UserPoolId: !Ref MyUserPool

  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: MyApi
      StageName: Prod
      Cors:
        AllowMethods: "'GET,OPTIONS'"
        AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'"
        AllowOrigin: "'*'"
      Auth:
        DefaultAuthorizer: CognitoAuthorizer
        AddDefaultAuthorizerToCorsPreflight: false
        Authorizers:
          CognitoAuthorizer:
            UserPoolArn: !GetAtt MyUserPool.Arn

  GetItemsFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.12
      Environment:
        Variables:
          TABLE_NAME: !Ref ItemsTable
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref ItemsTable
  Events:
    GetItemsApi:
      Type: Api
      Properties:
        RestApiId: !Ref MyApi
        Path: /items
        Method: get

    OptionsItemsApi:
      Type: Api
      Properties:
        RestApiId: !Ref MyApi
        Path: /items
        Method: options

Outputs:
  WebURL:
    Value: !GetAtt WebHostingBucket.WebsiteURL
    Description: "S3 Static Website URL (HTTP)"

  CloudFrontURL:
    Value: !Sub "https://${CloudFrontDistribution.DomainName}"
    Description: "CloudFront Distribution URL (HTTPS)"

  ApiURL:
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/items"

  UserPoolId:
    Value: !Ref MyUserPool

  UserPoolClientId:
    Value: !Ref MyUserPoolClient

  CognitoDomainURL:
    Value: !Sub "https://${MyUserPoolDomain}.auth.${AWS::Region}.amazoncognito.com"
    Description: "Cognito認証用ドメインURL"

  CallbackURL:
    Value: !Sub "https://${CloudFrontDistribution.DomainName}/callback.html"
    Description: "Cognito Callback URL (HTTPS)"
```

## AWS環境にデプロイしてみた

上記の構成を `sam deploy` でデプロイしたところ、以下のような流れでサーバーレスアプリが立ち上がりました。

* `sam build && sam deploy` で全リソースを一括作成

![Screenshot 2025-06-11 at 7.38.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/18558030-7b4d-4e46-8dc1-0c896b14aa12.png)

* S3バケットに `frontend/index.html` などをアップロード

→実際に使ったコマンド

```
aws s3 cp frontend/ s3://バケット名 --recursive
```

**問題なくアップロードされていることが確認できました！**

![Screenshot 2025-06-11 at 7.38.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/773714d4-635e-4813-abfd-be8e6647283c.png)

* CloudFrontのURLにアクセスすると、Cognitoログインボタン付きのトップページが表示される

![Screenshot 2025-06-11 at 7.40.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/14b26708-7916-455e-8712-950cadd054a5.png)

**ログイン画面**

![Screenshot 2025-06-11 at 7.41.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/4a322607-0965-475e-a2a0-6053aa02064e.png)

すでにCognito側でユーザーを作成済みとなりますので、そのユーザーでログインを試していきます。

![Screenshot 2025-06-11 at 7.44.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/2893fbb3-eff2-499a-a567-64921e7874c4.png)

**問題なくログインできることが確認できましたので、今回の検証はこれで以上とします！**

![Screenshot 2025-06-11 at 7.47.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/25c2f05e-a018-436a-9c78-ec92c8a1294f.png)

CognitoのOAuth連携やトークンの扱いも `callback.html` + `script.js` で処理しており、かなり実践的な構成になったと思います...！（少し難しかったです！）

## まとめ

ここまでお読みいただき、ありがとうございました！

AWS SAMを使うことで、サーバーレスアプリの複雑な構成もテンプレート1枚で管理でき、Cognito認証との統合もスムーズに行えました。

:::note info
**今後はこの構成をベースに、ユーザー登録・データ投稿機能の追加などを行っていきたいと考えています。**
:::

Cognito連携の自動化に挑戦したい方にとっても、良い学びの機会になる構成だと思います...！！

不要になったリソースは以下のコマンドで削除できますので、適宜削除しておくことをおすすめします。

```sh
aws cloudformation delete-stack --stack-name sam-app
```

今後も応用的な構成に対応できるよう、引き続きキャッチアップを進めていきたいと思います...！

## 参考文献

https://dev.classmethod.jp/articles/aws-hands-on-for-beginners-serverless2/

https://zenn.dev/mjxo/articles/21f0dd659a174d

https://qiita.com/i3no29/items/cca2ede577774bbb7274

https://dev.classmethod.jp/articles/swagger-aws-sam-cognito-auth-rest-api-http/