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 マネージドサービスを使う開発で「ローカル確認 → サーバー反映」をどう成立させるか

0
Posted at

この記事で扱う課題

ローカルで動かして確認してから本番に上げる、という当たり前の開発サイクルは、構成要素がすべて自分の手元で動くうちは難しくない。問題は、認証もデータ層もキャッシュも AWS のマネージドサービスに乗っている場合だ。

このプロジェクトの構成要素は以下のとおり。

  • Cognito — ユーザー認証(ログイン・サインアップ)
  • AppSync — GraphQL API(Cognito 認証)
  • DynamoDB — データストア
  • ElastiCache (Redis) — サーバーサイドセッション
  • Next.js — 本番では ECS Fargate 上で動くアプリケーション本体

このうちアプリ本体(Next.js)はローカルで npm run dev すれば動く。だが、それが依存する Cognito・AppSync・Redis をローカルでどう用意するかが悩みどころになる。選択肢は大きく分けて 3 つある。

  1. すべてをエミュレータ・モックでローカルに再現する
  2. すべてを共有の 1 つの dev 環境に集約し、全員がそれを叩く
  3. AWS マネージドサービスは本物を使い、ローカルで等価に再現できるものだけコンテナで立てる

このプロジェクトは 3 を選んだ。以下、その判断と具体的な仕組みを説明する。

方針 1: AWS マネージドサービスは「個人用の本物」を CDK で立てる

Cognito や AppSync をローカルでエミュレートしようとすると、認証トークンの細かい挙動や GraphQL リゾルバーの解決順序など、本番との差異が必ずどこかで顔を出す。「ローカルでは通るのに本番で落ちる」を生む典型的な原因だ。

そこでこのプロジェクトは、Cognito・AppSync・DynamoDB は本物の AWS リソースを使う。ただし共有の 1 つを全員で叩くのではなく、1 つの開発用アカウントの中に、開発者ごとの専用 dev スタックを立てる。アカウントは共有でも、リソースは個人ごとに分かれている状態をつくる。

リソースは CDK スタックとしてソースコードに定義されているので、誰でも 1 コマンドで個人用 dev 環境を作れる。同じアカウント内でスタック名が衝突しないよう、スタック名に開発者を識別する名前を含める。

// infra/bin/infra.ts
// Dev: Cognito + AppSync + DynamoDB のみ (ローカル開発用)
// devName は開発者ごとに異なる識別子(環境変数などで渡す)
const devName = process.env.DEV_NAME;
const devAuth = new AuthStack(app, `AppName-dev-${devName}-Auth`, devConfig);
new BackendStack(app, `AppName-dev-${devName}-Backend`, {
  ...devConfig,
  userPool: devAuth.userPool,
});

prod スタック(VPC・ElastiCache・ECS・CodePipeline …)とは別に、dev は認証とバックエンドだけの軽量な 2 スタックに切り出してある。ローカル開発で必要なのはここまでで、ネットワークやコンテナ基盤は要らないからだ。

cd infra
DEV_NAME=alice npx cdk deploy AppName-dev-alice-Auth AppName-dev-alice-Backend

これで開発用アカウントの中に、自分専用の Cognito User Pool と AppSync API ができる。アカウントは共有でもリソースは個人ごとに分かれているので、他人のデータやスキーマ変更に邪魔されずに並行開発できるのがこの方式の利点だ。共有の 1 つの dev を全員で叩くと「誰かがスキーマを壊すと全員止まる」が起きるが、それを構造的に避けられる。

方針 2: ローカルで等価に再現できるものは docker-compose で立てる

すべてを「本物の AWS」にすると、今度は Redis が問題になる。本番の ElastiCache は VPC 内のプライベートサブネットに置かれ、外部からは到達できない。セキュリティ上それが正しい姿だが、ローカルの Next.js からは繋げない。

ここで効くのが、Redis はステートレスなキャッシュ/セッションストアであり、ローカルに立てた素の Redis が ElastiCache とまったく同じプロトコル・同じ操作を提供するという性質だ。Cognito や AppSync と違い、ローカルのコンテナと本番のマネージドサービスとの間に意味のある差が生じない。

だから Redis だけは docker-compose でローカルに立てる。

# docker-compose.yml
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
docker compose up -d

判断基準はシンプルで、**「ローカルのコンテナが本番と同じ振る舞いを保証できるか」**だ。保証できるもの(Redis)はコンテナ、保証できないもの(Cognito・AppSync)は本物の dev リソースを使う。この線引きによって「何がローカルで、何が AWS か」が一目で分かり、環境差に起因するデバッグを最小化できる。

コンポーネント ローカル開発での扱い 理由
Cognito 本物(個人用 dev スタック) 認証挙動をエミュレータで再現しきれない
AppSync / DynamoDB 本物(個人用 dev スタック) リゾルバー・スキーマの挙動を本番と揃えたい
Redis ローカルコンテナ(docker-compose) プロトコルが同一で、本番と等価に再現できる

方針 3: ローカルとサーバーの切り替えは「環境変数」だけで行う

ここまでの構成で、接続先は「ローカルの Redis」「個人用 dev の Cognito/AppSync」「本番の ElastiCache/Cognito/AppSync」と環境ごとに変わる。これをコードの分岐で書くと if (isLocal) が散らばって破綻する。

このプロジェクトは接続先をすべて環境変数に追い出し、アプリのコードは接続先を一切知らない形にしている。Redis クライアントは REDIS_URL が指す先がローカルか ElastiCache かを意識しない。

// apps/web/src/app/lib/redis.ts
client = new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379', {
  lazyConnect: true,
  maxRetriesPerRequest: 1,
  enableOfflineQueue: false,
});

AppSync の呼び出しも同様に、URL を環境変数から受け取るだけだ。

// apps/web/src/app/lib/appsync.ts
const APPSYNC_URL = process.env.APPSYNC_URL!;

ローカルの .env.local はこうなる。Redis だけ localhost を指し、Cognito と AppSync は dev スタックの出力値を入れる。

# apps/web/.env.local
# Cognito (dev: cdk deploy AppName-dev-Auth で出力される値)
NEXT_PUBLIC_USER_POOL_ID=ap-northeast-1_XXXXXXXXX
NEXT_PUBLIC_USER_POOL_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXX

# AppSync (dev: cdk deploy AppName-dev-Backend で出力される値)
APPSYNC_URL=https://XXXXXXXXXX.appsync-api.ap-northeast-1.amazonaws.com/graphql

# Redis (ローカル: docker compose up)
REDIS_URL=redis://localhost:6379

本番(ECS タスク)では同じ環境変数に、ElastiCache のエンドポイントと prod の AppSync URL を入れる。**コードは 1 行も変わらない。**ログインからセッション保存、AppSync 呼び出しまで、ローカルと本番で完全に同じコードパスが走る。これが「ローカルで確認したものがそのまま本番で動く」ことの根拠になる。

ひとつ注意: ビルド時に焼き込む値とランタイムで渡す値は分けている

環境変数といっても、Next.js では渡すタイミングが 2 種類ある。この区別を曖昧にすると、同じ Docker イメージを環境ごとに使い回せなくなる。

  • サーバー専用の接続情報APPSYNC_URLREDIS_URLSESSION_SECRET)はランタイムで渡す。サーバーサイドでしか参照されないので、ビルド済みイメージにそのまま注入できる。
  • ブラウザに露出するクライアント設定NEXT_PUBLIC_USER_POOL_ID など Cognito の ID)は、Next.js の仕様上ビルド時にバンドルへ焼き込まれる。そのため Dockerfile では ARG として受け取っている。
# apps/web/Dockerfile (builder ステージ)
ARG NEXT_PUBLIC_USER_POOL_ID
ARG NEXT_PUBLIC_USER_POOL_CLIENT_ID
ENV NEXT_PUBLIC_USER_POOL_ID=$NEXT_PUBLIC_USER_POOL_ID
ENV NEXT_PUBLIC_USER_POOL_CLIENT_ID=$NEXT_PUBLIC_USER_POOL_CLIENT_ID
RUN npm run build --workspace=apps/web

つまり「環境差はすべてランタイム環境変数で吸収する」が理想だが、NEXT_PUBLIC_* だけはブラウザに露出する性質上ビルド時に確定する。サーバー側の接続先(DB・キャッシュ・API)はランタイム、クライアント公開値はビルド時、という線引きを意識しておくと混乱しない。

まとめ

AWS マネージドサービスを前提にしつつ「ローカルで確認してからサーバーに上げる」を成立させるための 3 つの方針はこうだ。

  1. エミュレートが難しいマネージドサービス(Cognito・AppSync・DynamoDB)は、1 つの開発用アカウントの中に開発者ごとの本物のリソースを立てる。 アカウントは共有でもリソースは個人ごとに分かれるので、並行開発で互いに壊し合わない。
  2. ローカルで本番と等価に再現できるもの(Redis)だけ docker-compose で立てる。 「コンテナが本番と同じ振る舞いを保証できるか」を線引きの基準にする。
  3. ローカルとサーバーの切り替えはコード分岐ではなく環境変数で行う。 アプリは接続先を知らず、同一コードパスがどちらでも走る。ただしクライアント公開値(NEXT_PUBLIC_*)だけはビルド時に確定する点に注意する。

この 3 つを守ると、「ローカルで通ったのに本番で落ちる」の主要因である環境差が、設計の段階で構造的に潰れる。

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?