0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】管理アプリPWA サーバーレス スマホから ログイン付き

0
Posted at

はじめに

 
管理アプリをPWAとして作りました。
 
サーバーを常時起動させずに、ログイン付き・スマホから使える・データはクラウドに残る、という条件で構成を組んでいます。
 
 

概要

 
構成:
 

ブラウザ(PWA)
  ├─ CloudFront ─(OAC)─ S3        … HTML/JS/CSS
  ├─ Cognito Hosted UI            … ログイン(PKCE)
  └─ API Gateway(HTTP API)        … JWTオーソライザー
        └─ Lambda(Node.js 20)
              ├─ DynamoDB         … 管理データ
              └─ S3(private)      … 写真(署名付きURL)

 
特徴:
 
常時起動するリソースがない(すべてリクエスト課金)
S3はすべて非公開、CloudFrontと署名付きURLからのみアクセス
ログインはCognito、ユーザーは管理者が作成した1人だけ
 

技術スタック

 

  • S3 + CloudFront
  • Cognito
  • API Gateway HTTP API
  • Lambda
  • DynamoDB
     

実際の請求

 
Cost Explorerで直近3か月をサービス別に確認しました。
 

サービス 月額(USD)
API Gateway 0.002
Lambda 0
DynamoDB 0
Cognito 0
CloudFront 0
S3 0.1未満
 
すべて無料枠の範囲内です。
 

各サービスの設定

 

S3 + CloudFront

 

  • S3はパブリックアクセスブロックをすべてON
  • CloudFrontからOACで読む。バケットポリシーで自分のディストリビューションだけにs3:GetObjectを許可
  • S3の静的ウェブサイトホスティングは使わない
  • CloudFrontのカスタムエラーレスポンスで、403と404を/index.html(200)に戻す
     
    OAC経由で存在しないキーを取りに行くと、S3は404ではなく403を返します。403も拾わないと、URL直打ちで真っ白になります。
     

DynamoDB

 

  • テーブルは1つ
  • プロビジョンドモード、1RCU/1WCU
  • パーティションキー:userId、ソートキー:itemId
  • PITR(ポイントインタイムリカバリ)はON
  • GSIなし
     

Cognito

 
ユーザープール:
 

  • AllowAdminCreateUserOnlyをONにしてサインアップ画面を封じる
  • ユーザー名はメールアドレス
  • ユーザーはCLIのadmin-create-userで1人だけ作る
     
    アプリクライアント:
     
  • クライアントシークレットなし
  • OAuthフローはcode、スコープはopenid email profile
  • コールバックURLに本番のCloudFrontドメインとhttp://localhost:5180/の両方を登録
     

API Gateway

 

  • HTTP API
  • JWTオーソライザーを追加。IssuerはCognitoユーザープール、Audienceはアプリクライアント ID
  • ルートANY /{proxy+}にJWTオーソライザーを付けてLambdaへプロキシ
  • ルートOPTIONS /{proxy+}は認可なしで別に作る
     
    HTTP APIはREST APIより安くて個人向きだったため利用
     

Lambdaの実行ロール

 

  • DynamoDB: 対象テーブルに対してQuery / PutItem / DeleteItemのみ
  • S3: 画像バケットに対してGetObject / PutObjectのみ
     

Lambda

 

ユーザーIDの取得

 

const userId = event.requestContext?.authorizer?.jwt?.claims?.sub;
if (!userId) return response(401, { error: "Unauthorized" });

 
 

写真は署名付きURLで直接S3へ

 

import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
 
async function createUploadUrl(userId, itemId, contentType) {
  const key = `${userId}/${itemId}/photo-${Date.now()}.jpg`;
  const command = new PutObjectCommand({ Bucket: IMAGE_BUCKET, Key: key, ContentType: contentType });
  const url = await getSignedUrl(s3, command, { expiresIn: 900 });
  return { key, url };
}

 
Lambdaは署名付きURLを返すだけで、ブラウザがそのURLに直接PUT

フロント

 

Cognitoログイン(PKCE)

 

async function login() {
  const verifier = randomString(64);
  const challenge = await sha256Base64Url(verifier);
  sessionStorage.setItem("pkceVerifier", verifier);
 
  const url = new URL(`${CONFIG.cognitoDomain}/oauth2/authorize`);
  url.searchParams.set("client_id", CONFIG.userPoolClientId);
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", "openid email profile");
  url.searchParams.set("redirect_uri", CONFIG.redirectUri);
  url.searchParams.set("code_challenge_method", "S256");
  url.searchParams.set("code_challenge", challenge);
  window.location.href = url.toString();
}
 
async function exchangeCode(code) {
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    client_id: CONFIG.userPoolClientId,
    code,
    redirect_uri: CONFIG.redirectUri,
    code_verifier: sessionStorage.getItem("pkceVerifier"),
  });
  const res = await fetch(`${CONFIG.cognitoDomain}/oauth2/token`, {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body,
  });
  return res.json(); // id_token / access_token / refresh_token
}

 
 

Service Worker

 

const CACHE_NAME = "my-app-v60";
 
self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))
    )
  );
});

 
 

ハマったところ

 

  • Cognitoの仮パスワードメールがGmailの迷惑メールに入る
  • Windows環境でDynamoDBに日本語が入っているとaws dynamodb scanがcp932エラーで落ちる。PYTHONIOENCODING=utf-8を付けると通る
     

コストを増やさないために

 

  • 固定費のあるサービス(VPCエンドポイント、NAT Gateway、RDS、Secrets Manager)を足さない
  • 画像はDynamoDBに入れずS3へ
  • 独自ドメインは別にいらないのでつけない
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?