diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index 4d7c6f1014a45..721fa10390784 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -285,6 +285,43 @@ ds.createResolver({ }); ``` +## Custom Domain Names + +For many use cases you may want to associate a custom domain name with your +GraphQL API. This can be done during the API creation. + +```ts +import * as acm from '@aws-cdk/aws-certificatemanager'; +import * as route53 from '@aws-cdk/aws-route53'; + +const myDomainName = 'api.example.com'; +const certificate = new acm.Certificate(this, 'cert', { domainName: myDomainName }); +const api = new appsync.GraphqlApi(this, 'api', { + name: 'myApi', + domainName: { + certificate, + domainName: myDomainName, + }, +}); + +// hosted zone and route53 features +declare const hostedZoneId: string; +declare const zoneName = 'example.com'; + +// hosted zone for adding appsync domain +const zone = route53.HostedZone.fromHostedZoneAttributes(this, `HostedZone`, { + hostedZoneId, + zoneName, +}); + +// create a cname to the appsync domain. will map to something like xxxx.cloudfront.net +new route53.CnameRecord(this, `CnameApiRecord`, { + recordName: 'api', + zone, + domainName: myDomainName, +}); +``` + ## Schema Every GraphQL Api needs a schema to define the Api. CDK offers `appsync.Schema` diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index a63b757ba6b0b..5d7cce7131cbb 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -1,9 +1,10 @@ +import { ICertificate } from '@aws-cdk/aws-certificatemanager'; import { IUserPool } from '@aws-cdk/aws-cognito'; import { ManagedPolicy, Role, IRole, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; import { ArnFormat, CfnResource, Duration, Expiration, IResolvable, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'; +import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema, CfnDomainName, CfnDomainNameApiAssociation } from './appsync.generated'; import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base'; import { Schema } from './schema'; import { IIntermediateType } from './schema-base'; @@ -254,6 +255,21 @@ export interface LogConfig { readonly role?: IRole; } +/** + * Domain name configuration for AppSync + */ +export interface DomainOptions { + /** + * The certificate to use with the domain name. + */ + readonly certificate: ICertificate; + + /** + * The actual domain name. For example, `api.example.com`. + */ + readonly domainName: string; +} + /** * Properties for an AppSync GraphQL API */ @@ -292,6 +308,16 @@ export interface GraphqlApiProps { * @default - false */ readonly xrayEnabled?: boolean; + + /** + * The domain name configuration for the GraphQL API + * + * The Route 53 hosted zone and CName DNS record must be configured in addition to this setting to + * enable custom domain URL + * + * @default - no domain name + */ + readonly domainName?: DomainOptions; } /** @@ -391,7 +417,7 @@ export class GraphqlApi extends GraphqlApiBase { class Import extends GraphqlApiBase { public readonly apiId = attrs.graphqlApiId; public readonly arn = arn; - constructor (s: Construct, i: string) { + constructor(s: Construct, i: string) { super(s, i); } } @@ -450,7 +476,7 @@ export class GraphqlApi extends GraphqlApiBase { const additionalModes = props.authorizationConfig?.additionalAuthorizationModes ?? []; const modes = [defaultMode, ...additionalModes]; - this.modes = modes.map((mode) => mode.authorizationType ); + this.modes = modes.map((mode) => mode.authorizationType); this.validateAuthorizationProps(modes); @@ -472,6 +498,19 @@ export class GraphqlApi extends GraphqlApiBase { this.schema = props.schema ?? new Schema(); this.schemaResource = this.schema.bind(this); + if (props.domainName) { + new CfnDomainName(this, 'DomainName', { + domainName: props.domainName.domainName, + certificateArn: props.domainName.certificate.certificateArn, + description: `domain for ${this.name} at ${this.graphqlUrl}`, + }); + + new CfnDomainNameApiAssociation(this, 'DomainAssociation', { + domainName: props.domainName.domainName, + apiId: this.apiId, + }); + } + if (modes.some((mode) => mode.authorizationType === AuthorizationType.API_KEY)) { const config = modes.find((mode: AuthorizationMode) => { return mode.authorizationType === AuthorizationType.API_KEY && mode.apiKeyConfig; diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index db0d74aa48788..f69c2d957e031 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -89,6 +89,7 @@ "jest": "^27.5.1" }, "dependencies": { + "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", @@ -104,6 +105,7 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts index 9c75c0c7a28c9..350fbff6229db 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import { Template } from '@aws-cdk/assertions'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as appsync from '../lib'; @@ -155,3 +156,38 @@ test('appsync GraphqlApi should not use custom role for CW Logs when not specifi }, }); }); + +test('appsync GraphqlApi should be configured with custom domain when specified', () => { + const domainName = 'api.example.com'; + // GIVEN + const certificate = new Certificate(stack, 'AcmCertificate', { + domainName, + }); + + // WHEN + new appsync.GraphqlApi(stack, 'api-custom-cw-logs-role', { + authorizationConfig: {}, + name: 'apiWithCustomRole', + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), + domainName: { + domainName, + certificate, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DomainNameApiAssociation', { + ApiId: { + 'Fn::GetAtt': [ + 'apicustomcwlogsrole508EAC74', + 'ApiId', + ], + }, + DomainName: domainName, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::DomainName', { + CertificateArn: { Ref: 'AcmCertificate49D3B5AF' }, + DomainName: domainName, + }); +});