Skip to content

Commit

Permalink
feat(apigatewayv2): http api - custom domain & stage mapping (#8027)
Browse files Browse the repository at this point in the history
- [x] implementation
- [x] README
- [x] integ test
- [x] 100% unit test coverage

### Commit Message
feat(apigatewayv2): http api - custom domain & stage mapping (#8027)

- Add new `DomainName` and `HttpApiMapping` construct classes and `addDomainName()` method for `HttpApi` resource.
- Add `defaultDomainMapping` construct property for `HttpApi`
- Add `domainMapping` attribute for `addStage`

Closes #7847 

### End Commit Message

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
pahud authored Jul 6, 2020
1 parent a035784 commit 5e43348
Show file tree
Hide file tree
Showing 18 changed files with 1,089 additions and 7 deletions.
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigatewayv2/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*.snk
!*.d.ts
!*.js
**/cdk.out

# Coverage
coverage
Expand Down
66 changes: 66 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [Defining HTTP APIs](#defining-http-apis)
- [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors)
- [Publishing HTTP APIs](#publishing-http-apis)
- [Custom Domain](#custom-domain)

## Introduction

Expand Down Expand Up @@ -134,3 +135,68 @@ If you omit the `stageName` will create a `$default` stage. A `$default` stage i
the API's URL - `https://{api_id}.execute-api.{region}.amazonaws.com/`.

Note that, `HttpApi` will always creates a `$default` stage, unless the `createDefaultStage` property is unset.



### Custom Domain

Custom domain names are simpler and more intuitive URLs that you can provide to your API users. Custom domain name are associated to API stages.

The code snippet below creates a custom domain and configures a default domain mapping for your API that maps the
custom domain to the `$default` stage of the API.

```ts
const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate';
const domainName = 'example.com';

const dn = new DomainName(stack, 'DN', {
domainName,
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', certArn),
});

const api = new HttpApi(stack, 'HttpProxyProdApi', {
defaultIntegration: new LambdaProxyIntegration({ handler }),
// https://${dn.domainName} goes to prodApi $default stage
defaultDomainMapping: {
domainName: dn,
mappingKey: '/',
},
});
```

To associate a specifc `Stage` to a custom domain mapping -

```ts
api.addStage('beta', {
stageName: 'beta',
autoDeploy: true,
// https://${dn.domainName}/beta goes to the beta stage
domainMapping: {
domainName: dn,
mappingKey: 'beta',
},
});
```

The same domain name can be associated with stages across different `HttpApi` as so -

```ts
const apiDemo = new HttpApi(stack, 'DemoApi', {
defaultIntegration: new LambdaProxyIntegration({ handler }),
// https://${dn.domainName}/demo goes to apiDemo $default stage
defaultDomainMapping: {
domainName: dn,
mappingKey: 'demo',
},
});
```

The `mappingKey` determines the `path` of the URL with the custom domain. Each custom domain is only allowed
to have one API mapping with the root(/) `mappingKey`. In the sample above, the custom domain is associated
with 3 API mapping resources across different APIs and Stages.

| API | Stage | URL |
| :------------: | :---------: | :----: |
| api | $default | `https://${domainName}` |
| api | beta | `https://${domainName}/beta` |
| apiDemo | $default | `https://${domainName}/demo` |
13 changes: 13 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IResource } from '@aws-cdk/core';

/**
* Represents an ApiGatewayV2 ApiMapping resource
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-apimapping.html
*/
export interface IApiMapping extends IResource {
/**
* ID of the api mapping
* @attribute
*/
readonly apiMappingId: string;
}
117 changes: 117 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ICertificate } from '@aws-cdk/aws-certificatemanager';
import { Construct, IResource, Resource, Token } from '@aws-cdk/core';
import { CfnDomainName, CfnDomainNameProps } from '../apigatewayv2.generated';

/**
* Represents an APIGatewayV2 DomainName
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-domainname.html
*/
export interface IDomainName extends IResource {
/**
* The custom domain name
*
* @attribute
*
*/
readonly domainName: string;

/**
* The domain name associated with the regional endpoint for this custom domain name.
*
* @attribute
*/
readonly regionalDomainName: string;

/**
* The region-specific Amazon Route 53 Hosted Zone ID of the regional endpoint.
*
* @attribute
*/
readonly regionalHostedZoneId: string;
}

/**
* custom domain name attributes
*/
export interface DomainNameAttributes {
/**
* domain name string
*/
readonly domainName: string;

/**
* The domain name associated with the regional endpoint for this custom domain name.
*/
readonly regionalDomainName: string;

/**
* The region-specific Amazon Route 53 Hosted Zone ID of the regional endpoint.
*/
readonly regionalHostedZoneId: string;
}

/**
* properties used for creating the DomainName
*/
export interface DomainNameProps {
/**
* The custom domain name
*/
readonly domainName: string;
/**
* The ACM certificate for this domain name
*/
readonly certificate: ICertificate;
}

/**
* Custom domain resource for the API
*/
export class DomainName extends Resource implements IDomainName {
/**
* import from attributes
*/
public static fromDomainNameAttributes(scope: Construct, id: string, attrs: DomainNameAttributes): IDomainName {
class Import extends Resource implements IDomainName {
public readonly regionalDomainName = attrs.regionalDomainName;
public readonly regionalHostedZoneId = attrs.regionalHostedZoneId;
public readonly domainName = attrs.domainName;
}
return new Import(scope, id);
}

/**
* The custom domain name for your API in Amazon API Gateway.
*
* @attribute
*/
public readonly domainName: string;

/**
* The domain name associated with the regional endpoint for this custom domain name.
*/
public readonly regionalDomainName: string;

/**
* The region-specific Amazon Route 53 Hosted Zone ID of the regional endpoint.
*/
public readonly regionalHostedZoneId: string;

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

const domainNameProps: CfnDomainNameProps = {
domainName: props.domainName,
domainNameConfigurations: [
{
certificateArn: props.certificate.certificateArn,
endpointType: 'REGIONAL',
},
],
};
const resource = new CfnDomainName(this, 'Resource', domainNameProps);
this.domainName = props.domainName ?? resource.ref;
this.regionalDomainName = Token.asString(resource.getAtt('RegionalDomainName'));
this.regionalHostedZoneId = Token.asString(resource.getAtt('RegionalHostedZoneId'));
}
}
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './integration';
export * from './route';
export * from './stage';
export * from './stage';
export * from './domain-name';
export * from './api-mapping';
78 changes: 78 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Construct, Resource } from '@aws-cdk/core';
import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated';
import { IApiMapping, IDomainName } from '../common';
import { IHttpApi } from '../http/api';
import { IHttpStage } from './stage';

/**
* Properties used to create the HttpApiMapping resource
*/
export interface HttpApiMappingProps {
/**
* Api mapping key. The path where this stage should be mapped to on the domain
* @default '/'
*/
readonly apiMappingKey?: string;

/**
* The HttpApi to which this mapping is applied
*/
readonly api: IHttpApi;

/**
* custom domain name of the mapping target
*/
readonly domainName: IDomainName;

/**
* stage for the HttpApiMapping resource
*
* @default - the $default stage
*/
readonly stage?: IHttpStage;
}

/**
* The attributes used to import existing HttpApiMapping
*/
export interface HttpApiMappingAttributes {
/**
* The API mapping ID
*/
readonly apiMappingId: string;
}

/**
* Create a new API mapping for API Gateway HTTP API endpoint.
* @resource AWS::ApiGatewayV2::ApiMapping
*/
export class HttpApiMapping extends Resource implements IApiMapping {
/**
* import from API ID
*/
public static fromHttpApiMappingAttributes(scope: Construct, id: string, attrs: HttpApiMappingAttributes): IApiMapping {
class Import extends Resource implements IApiMapping {
public readonly apiMappingId = attrs.apiMappingId;
}
return new Import(scope, id);
}
/**
* ID of the API Mapping
*/
public readonly apiMappingId: string;

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

const apiMappingProps: CfnApiMappingProps = {
apiId: props.api.httpApiId,
domainName: props.domainName.domainName,
stage: props.stage?.stageName ?? '$default',
apiMappingKey: props.apiMappingKey,
};

const resource = new CfnApiMapping(this, 'Resource', apiMappingProps);
this.apiMappingId = resource.ref;
}

}
20 changes: 19 additions & 1 deletion packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Construct, Duration, IResource, Resource } from '@aws-cdk/core';
import { CfnApi, CfnApiProps } from '../apigatewayv2.generated';
import { DefaultDomainMappingOptions } from '../http/stage';
import { IHttpRouteIntegration } from './integration';
import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route';
import { HttpStage, HttpStageOptions } from './stage';
Expand Down Expand Up @@ -43,6 +44,13 @@ export interface HttpApiProps {
* @default - CORS disabled.
*/
readonly corsPreflight?: CorsPreflightOptions;

/**
* Configure a custom domain with the API mapping resource to the HTTP API
*
* @default - no default domain mapping configured. meaningless if `createDefaultStage` is `false`.
*/
readonly defaultDomainMapping?: DefaultDomainMappingOptions;
}

/**
Expand Down Expand Up @@ -118,6 +126,9 @@ export class HttpApi extends Resource implements IHttpApi {
}

public readonly httpApiId: string;
/**
* default stage of the api resource
*/
private readonly defaultStage: HttpStage | undefined;

constructor(scope: Construct, id: string, props?: HttpApiProps) {
Expand Down Expand Up @@ -166,8 +177,14 @@ export class HttpApi extends Resource implements IHttpApi {
this.defaultStage = new HttpStage(this, 'DefaultStage', {
httpApi: this,
autoDeploy: true,
domainMapping: props?.defaultDomainMapping,
});
}

if (props?.createDefaultStage === false && props.defaultDomainMapping) {
throw new Error('defaultDomainMapping not supported with createDefaultStage disabled',
);
}
}

/**
Expand All @@ -182,10 +199,11 @@ export class HttpApi extends Resource implements IHttpApi {
* Add a new stage.
*/
public addStage(id: string, options: HttpStageOptions): HttpStage {
return new HttpStage(this, id, {
const stage = new HttpStage(this, id, {
httpApi: this,
...options,
});
return stage;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './api';
export * from './route';
export * from './integration';
export * from './integrations';
export * from './stage';
export * from './stage';
export * from './api-mapping';
Loading

0 comments on commit 5e43348

Please sign in to comment.