Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apigateway): support custom domain names #3135

Merged
merged 7 commits into from
Jul 1, 2019
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
68 changes: 66 additions & 2 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,75 @@ to allow users revert the stage to an old deployment manually.
[Deployment]: https://docs.aws.amazon.com/apigateway/api-reference/resource/deployment/
[Stage]: https://docs.aws.amazon.com/apigateway/api-reference/resource/stage/

### Missing Features
### Custom Domains

To associate an API with a custom domain, use the `domainName` configuration when
you define your API:

### Roadmap
```ts
const api = new apigw.RestApi(this, 'MyDomain', {
domainName: {
domainName: 'example.com',
certificate: acmCertificateForExampleCom,
},
});
```

This will define a `DomainName` resource for you, along with a `BasePathMapping`
from the root of the domain to the deployment stage of the API. This is a common
set up.

To route domain traffic to an API Gateway API, use Amazon Route 53 to create an alias record. An alias record is a Route 53 extension to DNS. It's similar to a CNAME record, but you can create an alias record both for the root domain, such as example.com, and for subdomains, such as www.example.com. (You can create CNAME records only for subdomains.)

```ts
new route53.ARecord(this, 'CustomDomainAliasRecord', {
zone: hostedZoneForExampleCom,
target: route53.AddressRecordTarget.fromAlias(new route53_targets.ApiGateway(api))
});
```

You can also define a `DomainName` resource directly in order to customize the default behavior:

```ts
new apigw.DomainName(this, 'custom-domain', {
domainName: 'example.com',
certificate: acmCertificateForExampleCom,
endpointType: apigw.EndpointType.EDGE // default is REGIONAL
});
```

Once you have a domain, you can map base paths of the domain to APIs.
The following example will map the URL https://example.com/go-to-api1
to the `api1` API and https://example.com/boom to the `api2` API.

```ts
domain.addBasePathMapping(api1, { basePath: 'go-to-api1' });
domain.addBasePathMapping(api2, { basePath: 'boom' });
```

NOTE: currently, the mapping will always be assigned to the APIs
`deploymentStage`, which will automatically assigned to the latest API
deployment. Raise a GitHub issue if you require more granular control over
mapping base paths to stages.

If you don't specify `basePath`, all URLs under this domain will be mapped
to the API, and you won't be able to map another API to the same domain:

```ts
domain.addBasePathMapping(api);
```

This can also be achieved through the `mapping` configuration when defining the
domain as demonstrated above.

If you wish to setup this domain with an Amazon Route53 alias, use the `route53_targets.ApiGatewayDomain`:

```ts
new route53.ARecord(this, 'CustomDomainAliasRecord', {
zone: hostedZoneForExampleCom,
target: route53.AddressRecordTarget.fromAlias(new route53_targets.ApiGatewayDomain(domainName))
});
```

----

Expand Down
60 changes: 60 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/base-path-mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Construct, Resource, Token } from '@aws-cdk/core';
import { CfnBasePathMapping } from './apigateway.generated';
import { IDomainName } from './domain-name';
import { IRestApi, RestApi } from './restapi';

export interface BasePathMappingOptions {
/**
* The base path name that callers of the API must provide in the URL after
* the domain name (e.g. `example.com/base-path`). If you specify this
* property, it can't be an empty string.
*
* @default - map requests from the domain root (e.g. `example.com`). If this
* is undefined, no additional mappings will be allowed on this domain name.
*/
readonly basePath?: string;
}

export interface BasePathMappingProps extends BasePathMappingOptions {
/**
* The DomainName to associate with this base path mapping.
*/
readonly domainName: IDomainName;

/**
* The RestApi resource to target.
*/
readonly restApi: IRestApi;
}

/**
* This resource creates a base path that clients who call your API must use in
* the invocation URL.
*
* In most cases, you will probably want to use
* `DomainName.addBasePathMapping()` to define mappings.
*/
export class BasePathMapping extends Resource {
constructor(scope: Construct, id: string, props: BasePathMappingProps) {
super(scope, id);

if (props.basePath && !Token.isUnresolved(props.basePath)) {
if (!props.basePath.match(/^[a-z0-9$_.+!*'()-]+$/)) {
throw new Error(`A base path may only contain letters, numbers, and one of "$-_.+!*'()", received: ${props.basePath}`);
}
}

// if this is an owned API and it has a deployment stage, map all requests
// to that stage. otherwise, the stage will have to be specified in the URL.
const stage = props.restApi instanceof RestApi
? props.restApi.deploymentStage
: undefined;

new CfnBasePathMapping(this, 'Resource', {
basePath: props.basePath,
domainName: props.domainName.domainName,
restApiId: props.restApi.restApiId,
stage: stage && stage.stageName,
});
}
}
141 changes: 141 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/domain-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import acm = require('@aws-cdk/aws-certificatemanager');
import { Construct, IResource, Resource } from '@aws-cdk/core';
import { CfnDomainName } from './apigateway.generated';
import { BasePathMapping, BasePathMappingOptions } from './base-path-mapping';
import { EndpointType, IRestApi} from './restapi';

export interface DomainNameOptions {
/**
* The custom domain name for your API. Uppercase letters are not supported.
*/
readonly domainName: string;

/**
* The reference to an AWS-managed certificate for use by the edge-optimized
* endpoint for the domain name. For "EDGE" domain names, the certificate
* needs to be in the US East (N. Virginia) region.
*/
readonly certificate: acm.ICertificate;

/**
* The type of endpoint for this DomainName.
* @default REGIONAL
*/
readonly endpointType?: EndpointType;
}

export interface DomainNameProps extends DomainNameOptions {
/**
* If specified, all requests to this domain will be mapped to the production
* deployment of this API. If you wish to map this domain to multiple APIs
* with different base paths, don't specify this option and use
* `addBasePathMapping`.
*
* @default - you will have to call `addBasePathMapping` to map this domain to
* API endpoints.
*/
readonly mapping?: IRestApi;
}

export interface IDomainName extends IResource {
/**
* The domain name (e.g. `example.com`)
*
* @attribute DomainName
*/
readonly domainName: string;

/**
* The Route53 alias target to use in order to connect a record set to this domain through an alias.
*
* @attribute DistributionDomainName,RegionalDomainName
*/
readonly domainNameAliasDomainName: string;

/**
* Thje Route53 hosted zone ID to use in order to connect a record set to this domain through an alias.
*
* @attribute DistributionHostedZoneId,RegionalHostedZoneId
*/
readonly domainNameAliasHostedZoneId: string;
}

export class DomainName extends Resource implements IDomainName {

/**
* Imports an existing domain name.
*/
public static fromDomainNameAttributes(scope: Construct, id: string, attrs: DomainNameAttributes): IDomainName {
class Import extends Resource implements IDomainName {
public readonly domainName = attrs.domainName;
public readonly domainNameAliasDomainName = attrs.domainNameAliasTarget;
public readonly domainNameAliasHostedZoneId = attrs.domainNameAliasHostedZoneId;
}

return new Import(scope, id);
}

public readonly domainName: string;
public readonly domainNameAliasDomainName: string;
public readonly domainNameAliasHostedZoneId: string;

constructor(scope: Construct, id: string, props: DomainNameProps) {
super(scope, id);

const endpointType = props.endpointType || EndpointType.REGIONAL;
const edge = endpointType === EndpointType.EDGE;

const resource = new CfnDomainName(this, 'Resource', {
domainName: props.domainName,
certificateArn: edge ? props.certificate.certificateArn : undefined,
regionalCertificateArn: edge ? undefined : props.certificate.certificateArn,
endpointConfiguration: { types: [endpointType] },
});

this.domainName = resource.ref;

this.domainNameAliasDomainName = edge
? resource.attrDistributionDomainName
: resource.attrRegionalDomainName;

this.domainNameAliasHostedZoneId = edge
? resource.attrDistributionHostedZoneId
: resource.attrRegionalHostedZoneId;

if (props.mapping) {
this.addBasePathMapping(props.mapping);
}
}

/**
* Maps this domain to an API endpoint.
* @param targetApi That target API endpoint, requests will be mapped to the deployment stage.
* @param options Options for mapping to base path with or without a stage
*/
public addBasePathMapping(targetApi: IRestApi, options: BasePathMappingOptions = { }) {
const basePath = options.basePath || '/';
const id = `Map:${basePath}=>${targetApi.node.uniqueId}`;
return new BasePathMapping(this, id, {
domainName: this,
restApi: targetApi,
...options
});
}
}

export interface DomainNameAttributes {
/**
* The domain name (e.g. `example.com`)
*/
readonly domainName: string;

/**
* The Route53 alias target to use in order to connect a record set to this domain through an alias.
*/
readonly domainNameAliasTarget: string;

/**
* Thje Route53 hosted zone ID to use in order to connect a record set to this domain through an alias.
*/
readonly domainNameAliasHostedZoneId: string;
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export * from './model';
export * from './requestvalidator';
export * from './authorizer';
export * from './json-schema';
export * from './domain-name';
export * from './base-path-mapping';

// AWS::ApiGateway CloudFormation Resources:
export * from './apigateway.generated';
Expand Down
15 changes: 9 additions & 6 deletions packages/@aws-cdk/aws-apigateway/lib/lambda-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Method } from './method';
import { ProxyResource, Resource } from './resource';
import { RestApi, RestApiProps } from './restapi';

export interface LambdaRestApiProps {
export interface LambdaRestApiProps extends RestApiProps {
/**
* The default Lambda function that handles all requests from this API.
*
Expand All @@ -25,9 +25,11 @@ export interface LambdaRestApiProps {
readonly proxy?: boolean;

/**
* Further customization of the REST API.
* @deprecated the `LambdaRestApiProps` now extends `RestApiProps`, so all
* options are just available here. Note that the options specified in
* `options` will be overridden by any props specified at the root level.
*
* @default defaults
* @default - no options.
*/
readonly options?: RestApiProps;
}
Expand All @@ -41,13 +43,14 @@ export interface LambdaRestApiProps {
*/
export class LambdaRestApi extends RestApi {
constructor(scope: cdk.Construct, id: string, props: LambdaRestApiProps) {
if (props.options && props.options.defaultIntegration) {
throw new Error(`Cannot specify "options.defaultIntegration" since Lambda integration is automatically defined`);
if ((props.options && props.options.defaultIntegration) || props.defaultIntegration) {
throw new Error(`Cannot specify "defaultIntegration" since Lambda integration is automatically defined`);
}

super(scope, id, {
defaultIntegration: new LambdaIntegration(props.handler),
...props.options
...props.options, // deprecated, but we still support
...props,
});

if (props.proxy !== false) {
Expand Down
30 changes: 30 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/restapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CfnOutput, Construct, IResource as IResourceBase, Resource, Stack } fro
import { ApiKey, IApiKey } from './api-key';
import { CfnAccount, CfnRestApi } from './apigateway.generated';
import { Deployment } from './deployment';
import { DomainName, DomainNameOptions } from './domain-name';
import { Integration } from './integration';
import { Method, MethodOptions } from './method';
import { Model, ModelOptions } from './model';
Expand Down Expand Up @@ -147,6 +148,13 @@ export interface RestApiProps extends ResourceOptions {
* @default true
*/
readonly cloudWatchRole?: boolean;

/**
* Configure a custom domain name and map it to this API.
*
* @default - no domain name is defined, use `addDomainName` or directly define a `DomainName`.
*/
readonly domainName?: DomainNameOptions;
}

/**
Expand Down Expand Up @@ -196,6 +204,12 @@ export class RestApi extends Resource implements IRestApi {
*/
public deploymentStage: Stage;

/**
* The domain name mapped to this API, if defined through the `domainName`
* configuration prop.
*/
public readonly domainName?: DomainName;

private readonly methods = new Array<Method>();
private _latestDeployment: Deployment | undefined;

Expand Down Expand Up @@ -227,6 +241,10 @@ export class RestApi extends Resource implements IRestApi {
}

this.root = new RootResource(this, props, resource.attrRootResourceId);

if (props.domainName) {
this.domainName = this.addDomainName('CustomDomain', props.domainName);
}
}

/**
Expand Down Expand Up @@ -258,6 +276,18 @@ export class RestApi extends Resource implements IRestApi {
return this.deploymentStage.urlForPath(path);
}

/**
* Defines an API Gateway domain name and maps it to this API.
* @param id The construct id
* @param options custom domain options
*/
public addDomainName(id: string, options: DomainNameOptions): DomainName {
return new DomainName(this, id, {
...options,
mapping: this
});
}

/**
* Adds a usage plan.
*/
Expand Down
Loading