Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ MESH_MEMBER_HEARTBEAT_TTL_SECONDS=600
# Maximum connection time (expiresAt) for a group
MESH_MAX_CONNECTION_TIME_SECONDS=1500

# Custom Domain Settings
# APPSYNC_CUSTOM_DOMAIN=graphql.api.smalruby.app
# ROUTE53_PARENT_ZONE_NAME=api.smalruby.app

# Development Recommended Values (uncomment to use)
# MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=15
# MESH_HOST_HEARTBEAT_TTL_SECONDS=60
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ jobs:

- name: Synthesize CDK stack
run: npx cdk synth --context stage=stg
env:
APPSYNC_CUSTOM_DOMAIN: 'false'

security:
name: Security Audits
Expand Down
17 changes: 4 additions & 13 deletions bin/mesh-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,8 @@ const stackName = stage === 'prod' ? 'MeshV2Stack' : `MeshV2Stack-${stage}`;

new MeshV2Stack(app, stackName, {
stackName: stackName,
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but a single synthesized template can be deployed anywhere. */

/* Uncomment the next line to specialize this stack for the AWS Account
* and Region that are implied by the current CLI configuration. */
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

/* Uncomment the next line if you know exactly what Account and Region you
* want to deploy the stack to. */
// env: { account: '123456789012', region: 'us-east-1' },

/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
env: {
account: process.env.CDK_DEFAULT_ACCOUNT || process.env.AWS_ACCOUNT_ID,
region: process.env.CDK_DEFAULT_REGION || process.env.AWS_REGION || 'ap-northeast-1',
},
});
6 changes: 5 additions & 1 deletion cdk.context.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"acknowledged-issue-numbers": [
34892
]
],
"hosted-zone:account=007325983811:domainName=api.smalruby.app:region=ap-northeast-1": {
"Id": "/hostedzone/Z04680371PP076PW332Q5",
"Name": "api.smalruby.app."
}
}
51 changes: 51 additions & 0 deletions lib/mesh-v2-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import * as cdk from 'aws-cdk-lib/core';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
import * as path from 'path';
import { Construct } from 'constructs';

Expand All @@ -16,6 +19,33 @@ export class MeshV2Stack extends cdk.Stack {
const stage = this.node.tryGetContext('stage') || process.env.STAGE || 'stg';
const stageSuffix = stage === 'prod' ? '' : `-${stage}`;

// Custom Domain configuration from environment variables
const parentZoneName = process.env.ROUTE53_PARENT_ZONE_NAME || 'api.smalruby.app';
const defaultCustomDomain = stage === 'prod' ? `graphql.${parentZoneName}` : `${stage}.graphql.${parentZoneName}`;
const customDomain = process.env.APPSYNC_CUSTOM_DOMAIN === 'false'
? undefined
: (process.env.APPSYNC_CUSTOM_DOMAIN || defaultCustomDomain);

let domainOptions: appsync.DomainOptions | undefined;
let zone: route53.IHostedZone | undefined;

if (customDomain) {
zone = route53.HostedZone.fromLookup(this, 'HostedZone', {
domainName: parentZoneName,
});

const certificate = new acm.DnsValidatedCertificate(this, 'ApiCertificate', {
domainName: customDomain,
hostedZone: zone,
region: 'us-east-1',
});

domainOptions = {
certificate,
domainName: customDomain,
};
}

// Stack全体にタグ付与
cdk.Tags.of(this).add('Project', 'MeshV2');
cdk.Tags.of(this).add('Stage', stage);
Expand Down Expand Up @@ -77,6 +107,7 @@ export class MeshV2Stack extends cdk.Stack {
this.api = new appsync.GraphqlApi(this, 'MeshV2Api', {
name: `MeshV2Api${stageSuffix}`,
definition: appsync.Definition.fromFile(path.join(__dirname, '../graphql/schema.graphql')),
domainName: domainOptions,
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
Expand All @@ -100,6 +131,18 @@ export class MeshV2Stack extends cdk.Stack {
},
});

// Route53 Alias record for Custom Domain
if (customDomain && zone) {
// Extract subdomain from customDomain (e.g., "graphql.api.smalruby.app" -> "graphql")
const subdomain = customDomain.replace(`.${parentZoneName}`, '');

new route53.ARecord(this, 'ApiAliasRecord', {
zone,
recordName: subdomain,
target: route53.RecordTarget.fromAlias(new targets.AppSyncTarget(this.api)),
});
}

// AppSync APIにタグ付与
cdk.Tags.of(this.api).add('ResourceType', 'GraphQLAPI');

Expand Down Expand Up @@ -420,5 +463,13 @@ export class MeshV2Stack extends cdk.Stack {
value: this.api.apiId,
description: 'AppSync GraphQL API ID',
});

// Output Custom Domain URL
if (customDomain) {
new cdk.CfnOutput(this, 'CustomDomainUrl', {
value: `https://${customDomain}/graphql`,
description: 'AppSync Custom Domain URL',
});
}
}
}
2 changes: 1 addition & 1 deletion spec/requests/subscriptions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
puts "2. Connect to AppSync WebSocket endpoint:"
puts " API_URL='#{ENV["APPSYNC_ENDPOINT"]}'"
puts " API_KEY='#{ENV["APPSYNC_API_KEY"]}'"
puts " WS_URL=$(echo $API_URL | sed 's/https:/wss:/g' | sed 's/graphql$/graphql\\/connect/g')"
puts " WS_URL=$(echo $API_URL | sed 's/https:/wss:/g' | sed 's/graphql$/graphql\\/realtime/g')"
puts ""
puts "3. Subscribe to onMessageInGroup:"
puts " subscription {"
Expand Down
13 changes: 10 additions & 3 deletions spec/support/appsync_subscription_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,16 @@ def subscribe_and_execute(query, variables = {}, wait_time: 2, timeout: 15, &blo
private

def convert_to_websocket_endpoint(https_endpoint)
https_endpoint
.sub("https://", "wss://")
.sub(".appsync-api.", ".appsync-realtime-api.")
if https_endpoint.include?(".appsync-api.")
https_endpoint
.sub("https://", "wss://")
.sub(".appsync-api.", ".appsync-realtime-api.")
else
# For custom domains, AppSync WebSocket endpoint requires /realtime path
https_endpoint
.sub("https://", "wss://")
.sub(/\/graphql\/?$/, "/graphql/realtime")
end
end

def extract_host(endpoint)
Expand Down
Loading