Skip to content

AppSync WebSocket カスタムドメイン設定 (graphql.smalruby.app) #10

@takaokouji

Description

@takaokouji

概要

AppSync の WebSocket エンドポイントにカスタムドメイン graphql.api.smalruby.app を設定し、エンドポイントの URL を固定化します。これにより、WAF やネットワークフィルターの管理、将来的なバックエンドの切り替えが容易になります。

背景

現在、AppSync API は AWS が自動生成する URL(例: https://xxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql)を使用しています。この URL は以下の課題があります:

  • API ID が変わるとエンドポイント URL も変わる
  • WAF やファイアウォールのホワイトリスト管理が困難
  • ブランディングやユーザー体験の観点で改善の余地がある

カスタムドメインを設定することで、これらの課題を解決できます。

STAGE による振る舞い

カスタムドメイン設定は、デプロイする STAGE(環境)によってドメイン名が異なりますが、設定方法は統一されています。

prod(本番環境)

  • ドメイン: graphql.api.smalruby.app
  • 証明書: CDK が自動作成(既存の場合は再利用)
  • Route 53 ホストゾーン: api.smalruby.app(事前に手動作成、すでに存在)
  • CNAME レコード: CDK が自動作成(既存の場合は再利用)
  • 削除: cdk destroy 時に証明書と CNAME を自動削除

stg / stg2(ステージング環境)

  • ドメイン: stg.graphql.api.smalruby.app / stg2.graphql.api.smalruby.app
  • 証明書: CDK が自動作成(既存の場合は再利用)
  • Route 53 ホストゾーン: api.smalruby.app(事前に手動作成、すでに存在、prod と共通)
  • CNAME レコード: CDK が自動作成(既存の場合は再利用)
  • 削除: cdk destroy 時に証明書と CNAME を自動削除

メリット: すべての環境で CDK による自動管理が可能。ホストゾーン api.smalruby.app のみ手動で事前作成(すでに存在)。

タスク

1. 環境変数の設定

1.1 infra/mesh-v2/.env.example

infra/mesh-v2/.env.example に以下の環境変数を追加:

# AppSync Custom Domain
# Production: graphql.api.smalruby.app
# Staging: stg.graphql.api.smalruby.app / stg2.graphql.api.smalruby.app
APPSYNC_CUSTOM_DOMAIN=graphql.api.smalruby.app

# Route 53 Parent Hosted Zone Name (required)
# This hosted zone should already exist (api.smalruby.app)
ROUTE53_PARENT_ZONE_NAME=api.smalruby.app

注意:

  • APPSYNC_CERTIFICATE_ARN は不要(CDK が自動作成)
  • ROUTE53_HOSTED_ZONE_ID は不要(CDK が lookup で検索)

1.2 smalruby3-develop/.env.example

smalruby3-develop/.env.exampleMESH_GRAPHQL_ENDPOINT をカスタムドメインに更新:

# Mesh V2 拡張機能設定
MESH_GRAPHQL_ENDPOINT=https://graphql.api.smalruby.app/graphql
MESH_API_KEY=da2-your-api-key
MESH_AWS_REGION=ap-northeast-1
MESH_SECRET_KEY=your-secret-key-for-domain-hashing

1.3 gui/scratch-vm/.env.example

gui/scratch-vm/.env.exampleMESH_GRAPHQL_ENDPOINT をカスタムドメインに更新:

# Mesh V2 Extension Configuration

# AppSync GraphQL API Endpoint (Production)
MESH_GRAPHQL_ENDPOINT=https://graphql.api.smalruby.app/graphql

# AppSync API Key (Production)
# Example: da2-xxxxx
MESH_API_KEY=da2-your-api-key

# AWS Region (Production)
# Example: ap-northeast-1
MESH_AWS_REGION=ap-northeast-1

1.4 gui/smalruby3-gui/.env.example

gui/smalruby3-gui/.env.exampleMESH_GRAPHQL_ENDPOINT をカスタムドメインに更新:

# Mesh V2 Extension Configuration
# See https://github.com/smalruby/scratch-vm/tree/develop/ for backend setup

# AppSync GraphQL API Endpoint (Production)
MESH_GRAPHQL_ENDPOINT=https://graphql.api.smalruby.app/graphql

# AppSync API Key (Production)
# Example: da2-xxxxx
MESH_API_KEY=da2-your-api-key

# AWS Region (Production)
# Example: ap-northeast-1
MESH_AWS_REGION=ap-northeast-1

2. CDK実装 (infra/mesh-v2/lib/mesh-v2-stack.ts)

// Add imports
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';

// Read custom domain configuration from environment variables
const customDomainBase = process.env.APPSYNC_CUSTOM_DOMAIN || '';
const parentZoneName = process.env.ROUTE53_PARENT_ZONE_NAME || 'api.smalruby.app';

// Domain name based on stage
const stageDomain = stage === 'prod' ? customDomainBase : `${stage}.${customDomainBase}`;

// Certificate and DNS configuration
let certificate: acm.ICertificate | undefined;
let hostedZone: route53.IHostedZone | undefined;

if (customDomainBase) {
  // Lookup existing parent hosted zone (api.smalruby.app)
  hostedZone = route53.HostedZone.fromLookup(this, 'ParentHostedZone', {
    domainName: parentZoneName,
  });

  // Create certificate with DNS validation (reuses existing if available)
  certificate = new acm.Certificate(this, 'AppSyncCertificate', {
    domainName: stageDomain,
    validation: acm.CertificateValidation.fromDns(hostedZone),
  });
}

// AppSync GraphQL API with custom domain
this.api = new appsync.GraphqlApi(this, 'MeshV2Api', {
  name: `MeshV2Api${stageSuffix}`,
  definition: appsync.Definition.fromFile(path.join(__dirname, '../graphql/schema.graphql')),
  authorizationConfig: {
    // ... existing config ...
  },
  // Custom domain configuration
  domainName: certificate && stageDomain
    ? {
        certificate,
        domainName: stageDomain,
      }
    : undefined,
  // ... existing config ...
});

// Create CNAME record in parent hosted zone if custom domain is configured
if (customDomainBase && hostedZone) {
  new route53.CnameRecord(this, 'AppSyncCnameRecord', {
    recordName: stageDomain,
    zone: hostedZone,
    domainName: this.api.appSyncDomainName,
    ttl: cdk.Duration.seconds(300),
  });
}

// Output custom domain endpoints if configured
if (customDomainBase) {
  new cdk.CfnOutput(this, 'CustomDomainName', {
    value: stageDomain,
    description: 'Custom domain name for AppSync API',
  });

  new cdk.CfnOutput(this, 'AppSyncDomainName', {
    value: this.api.appSyncDomainName,
    description: 'AppSync CloudFront domain (CNAME target)',
  });

  if (certificate) {
    new cdk.CfnOutput(this, 'CertificateArn', {
      value: certificate.certificateArn,
      description: 'ACM Certificate ARN',
    });
  }
}

3. Route 53 ホストゾーンの確認

ホストゾーン api.smalruby.app がすでに存在することを確認します。

3.1 AWS Console での確認

  1. AWS マネジメントコンソールで Route 53 を開く
  2. 「ホストゾーン」を選択
  3. api.smalruby.app が存在することを確認
  4. 存在しない場合は新規作成(以下の手順)

3.2 ホストゾーンの新規作成(必要な場合のみ)

  1. 「ホストゾーンの作成」をクリック
  2. ホストゾーン設定:
    • ドメイン名: api.smalruby.app
    • タイプ: パブリックホストゾーン
  3. 「ホストゾーンの作成」をクリック

3.3 親ドメインへの NS レコード追加(必要な場合)

smalruby.app の DNS 設定で、api.smalruby.app のネームサーバーを委任:

  1. smalruby.app のホストゾーンを開く
  2. 「レコードを作成」をクリック
  3. レコード設定:
    • レコード名: api
    • レコードタイプ: NS
    • 値: api.smalruby.app のネームサーバー(4つ)
    • TTL: 172800(48時間)
  4. 「レコードを作成」をクリック

4. 環境変数の設定

すべての環境で同じ環境変数を使用します。

# infra/mesh-v2/.env
APPSYNC_CUSTOM_DOMAIN=graphql.api.smalruby.app
ROUTE53_PARENT_ZONE_NAME=api.smalruby.app

ステージング環境の場合:

# STAGE を指定するだけで、ドメイン名が自動的に stg.graphql.api.smalruby.app になります
STAGE=stg  # または stg2

5. CDKデプロイ

5.1 本番環境(prod)

cd infra/mesh-v2
npx cdk deploy --context stage=prod

5.2 ステージング環境(stg/stg2)

cd infra/mesh-v2
npx cdk deploy --context stage=stg
# または
npx cdk deploy --context stage=stg2

デプロイ完了後、以下の情報が出力されます:

  • CustomDomainName: 設定したカスタムドメイン
  • AppSyncDomainName: CloudFront ドメイン(CNAME ターゲット)
  • CertificateArn: 作成された証明書の ARN

6. クライアント環境変数の更新

カスタムドメイン設定完了後、各環境の .env ファイルを更新します:

6.1 本番環境

# smalruby3-develop/.env
MESH_GRAPHQL_ENDPOINT=https://graphql.api.smalruby.app/graphql

# gui/scratch-vm/.env
MESH_GRAPHQL_ENDPOINT=https://graphql.api.smalruby.app/graphql

# gui/smalruby3-gui/.env
MESH_GRAPHQL_ENDPOINT=https://graphql.api.smalruby.app/graphql

6.2 ステージング環境

# smalruby3-develop/.env
MESH_GRAPHQL_ENDPOINT=https://stg.graphql.api.smalruby.app/graphql

# gui/scratch-vm/.env
MESH_GRAPHQL_ENDPOINT=https://stg.graphql.api.smalruby.app/graphql

# gui/smalruby3-gui/.env
MESH_GRAPHQL_ENDPOINT=https://stg.graphql.api.smalruby.app/graphql

注意: これらの環境変数は webpack でビルド時に埋め込まれるため、変更後は再ビルドが必要です:

# scratch-vm の再ビルド
cd gui/scratch-vm
npm run build

# smalruby3-gui の再ビルド
cd gui/smalruby3-gui
npm run build

7. WAF設定(オプション)

特定のオリジンからのアクセスのみを許可するWAFルールを設定します。

7.1 WAF WebACL の作成(CDK実装)

import * as wafv2 from 'aws-cdk-lib/aws-wafv2';

// Allowed origins
const allowedOrigins = [
  'http://localhost:8601',
  'https://smalruby.app',
  'https://smalruby.jp',
];

// Create WAF WebACL
const webAcl = new wafv2.CfnWebACL(this, 'AppSyncWebACL', {
  scope: 'REGIONAL',
  defaultAction: { block: {} },
  rules: [
    {
      name: 'AllowSpecificOrigins',
      priority: 1,
      statement: {
        orStatement: {
          statements: allowedOrigins.map(origin => ({
            byteMatchStatement: {
              searchString: origin,
              fieldToMatch: { singleHeader: { name: 'origin' } },
              textTransformations: [{ priority: 0, type: 'LOWERCASE' }],
              positionalConstraint: 'EXACTLY',
            },
          })),
        },
      },
      action: { allow: {} },
      visibilityConfig: {
        sampledRequestsEnabled: true,
        cloudWatchMetricsEnabled: true,
        metricName: 'AllowSpecificOrigins',
      },
    },
  ],
  visibilityConfig: {
    sampledRequestsEnabled: true,
    cloudWatchMetricsEnabled: true,
    metricName: 'AppSyncWebACL',
  },
});

// Associate WAF with AppSync API
new wafv2.CfnWebACLAssociation(this, 'AppSyncWafAssociation', {
  resourceArn: this.api.arn,
  webAclArn: webAcl.attrArn,
});

// Output WAF ARN
new cdk.CfnOutput(this, 'WebACLArn', {
  value: webAcl.attrArn,
  description: 'WAF WebACL ARN',
});

7.2 WAF設定の注意点

  • Origin ヘッダーの制限: ブラウザからのリクエストでは Origin ヘッダーが送信されますが、サーバー間通信やネイティブアプリからのリクエストでは送信されない場合があります
  • WebSocket の制限: WebSocket 接続の場合、初期の HTTP ハンドシェイク時のみ Origin ヘッダーがチェックされます
  • 開発環境: localhost:8601 からのアクセスを許可していますが、本番環境では削除を検討してください
  • CORS設定との関係: WAF は CORS とは独立して動作します。CORS ヘッダーは AppSync 側で自動的に処理されます

検証手順

1. DNS伝播の確認

# CNAME レコードの確認(本番環境)
dig graphql.api.smalruby.app CNAME

# CNAME レコードの確認(ステージング環境)
dig stg.graphql.api.smalruby.app CNAME

# 結果例:
# graphql.api.smalruby.app. 300 IN CNAME d123456789.cloudfront.net.

DNS が世界中に浸透するまで数分から最大 1 時間程度かかる場合があります。

2. カスタムドメインでの接続テスト

# GraphQL エンドポイントへのリクエスト(本番環境)
curl -X POST https://graphql.api.smalruby.app/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{"query":"{ __typename }"}'

# GraphQL エンドポイントへのリクエスト(ステージング環境)
curl -X POST https://stg.graphql.api.smalruby.app/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{"query":"{ __typename }"}'

# 期待される応答:
# {"data":{"__typename":"Query"}}

3. WebSocket接続テスト

JavaScript クライアントでの接続テスト:

import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({
  uri: 'https://graphql.api.smalruby.app/graphql',  // または https://stg.graphql.api.smalruby.app/graphql
  headers: {
    'x-api-key': 'YOUR_API_KEY',
  },
});

const wsLink = new GraphQLWsLink(createClient({
  url: 'wss://graphql.api.smalruby.app/graphql/realtime',  // または wss://stg.graphql.api.smalruby.app/graphql/realtime
  connectionParams: {
    headers: {
      'x-api-key': 'YOUR_API_KEY',
    },
  },
}));

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

4. アプリケーションでの動作確認

環境変数を更新してビルドした後、実際のアプリケーションでMesh拡張機能が動作することを確認:

# Docker環境で開発サーバーを起動
docker compose up gui

# ブラウザで http://localhost:8601 にアクセス
# Mesh拡張機能でグループ作成・参加が正常に動作することを確認

参考資料

完了条件

共通(すべての環境)

  • Route 53 ホストゾーン api.smalruby.app の確認(すでに存在)
  • 環境変数の設定完了(infra/mesh-v2)
  • CDK実装の完了
  • CDKデプロイ(証明書と CNAME が自動作成される)
  • DNS伝播の確認
  • カスタムドメインでの GraphQL リクエスト成功
  • カスタムドメインでの WebSocket 接続成功
  • クライアント環境変数の更新と再ビルド(scratch-vm, smalruby3-gui, smalruby3-develop)
  • アプリケーションでの動作確認
  • (オプション)WAF設定の完了と動作確認

追加情報

ロールバック手順

すべての環境で同じ手順:

  1. クライアントの .env ファイルを AWS 自動生成 URL に戻す
  2. npx cdk destroy --context stage=<環境> を実行(証明書と CNAME が自動削除)
  3. 再ビルド

注意: ホストゾーン api.smalruby.app は削除されません(手動作成したため)。

コスト影響

すべての環境共通

  • ACM証明書: 無料
  • Route 53 ホストゾーン api.smalruby.app: 月額 $0.50(共通で使用)
  • Route 53 CNAME レコード: クエリ料金のみ($0.40 / 100万クエリ)
  • CloudFront(AppSync背後): カスタムドメイン使用による追加コストなし
  • WAF(オプション): WebACL あたり月額 $5.00 + ルールごと $1.00 + リクエスト料金

コスト削減のヒント

  • ステージング環境(stg/stg2)は不要な場合 cdk destroy で削除
  • ホストゾーン api.smalruby.app は共通で使用するためコスト効率が良い

セキュリティ考慮事項

  1. 証明書の管理: ACM証明書は自動更新されますが、DNS検証レコードを削除しないように注意
  2. WAFルール: 本番環境では localhost からのアクセスを削除することを推奨
  3. APIキー: カスタムドメイン設定後も既存の認証設定(API キー)は継続して必要
  4. 環境変数の管理: .env ファイルは .gitignore に含まれているため、各環境で個別に設定が必要
  5. ステージング環境の削除: 不要な stg/stg2 環境は cdk destroy で削除してセキュリティリスクを最小化

影響範囲

このカスタムドメイン設定は以下のコンポーネントに影響します:

  1. infra/mesh-v2: CDK スタック(AppSync API、ACM証明書、Route53 CNAME の設定)
  2. gui/scratch-vm: Mesh拡張機能のGraphQLクライアント
  3. gui/smalruby3-gui: Webpackビルド設定(環境変数の埋め込み)
  4. smalruby3-develop: 全体的な開発環境設定

すべてのコンポーネントで環境変数を統一することで、一貫性のある設定を維持できます。

STAGE による動作の違い(まとめ)

項目 prod stg / stg2
ドメイン graphql.api.smalruby.app stg.graphql.api.smalruby.app / stg2.graphql.api.smalruby.app
ホストゾーン api.smalruby.app(手動作成、共通) api.smalruby.app(手動作成、共通)
証明書作成 自動(CDK) 自動(CDK)
CNAME作成 自動(CDK) 自動(CDK)
削除方法 cdk destroy で証明書と CNAME 削除 cdk destroy で証明書と CNAME 削除
環境変数 APPSYNC_CUSTOM_DOMAIN
ROUTE53_PARENT_ZONE_NAME
APPSYNC_CUSTOM_DOMAIN
ROUTE53_PARENT_ZONE_NAME
STAGE=stg または stg2
用途 本番運用 開発・検証

重要: すべての環境で同じ CDK コードと環境変数を使用。STAGE の値だけでドメイン名が自動的に変わります。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions