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(apigatewayv2): domain endpoint type, security policy and endpoint migration #17518

Merged
merged 9 commits into from
Nov 22, 2021
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ const domainName = 'example.com';
const dn = new apigwv2.DomainName(this, 'DN', {
domainName,
certificate: acm.Certificate.fromCertificateArn(this, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
securityPolicy: SecurityPolicy.TLS_1_2,
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved
});

declare const handler: lambda.Function;
Expand All @@ -204,6 +206,14 @@ const api = new apigwv2.HttpApi(this, 'HttpProxyProdApi', {
});
```

The properties certificate, endpointType, securityPolicy, certificateName, and ownershipVerificationCertificate are mapped in CFN to a DomainNameConfiguration.
See [DomainNameConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-domainname-domainnameconfiguration.html)
for more details.
You need only one DomainNameConfiguration, unless you're migrating from one endpoint type to another. To migrate to a new endpoint type,
you add a new domain name configuration via `addDomainNameConfiguration` and then configure DNS records to route traffic to the new endpoint.
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved
After that, you can remove the previous DomainNameConfiguration.
Learn more at [Migrating a custom domain name](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-regional-api-custom-domain-migrate.html)
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved

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

```ts
Expand Down
88 changes: 79 additions & 9 deletions packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import { ICertificate } from '@aws-cdk/aws-certificatemanager';
import { IBucket } from '@aws-cdk/aws-s3';
import { IResource, Resource, Token } from '@aws-cdk/core';
import { IResource, Lazy, Resource, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnDomainName, CfnDomainNameProps } from '../apigatewayv2.generated';

/**
* The minimum version of the SSL protocol that you want API Gateway to use for HTTPS connections.
*/
export enum SecurityPolicy {
/** Cipher suite TLS 1.0 */
TLS_1_0 = 'TLS_1_0',

/** Cipher suite TLS 1.2 */
TLS_1_2 = 'TLS_1_2',
}

/**
* Endpoint type for a domain name.
*/
export enum EndpointType {
/**
* For an edge-optimized custom domain name.
*/
EDGE = 'EDGE',
/**
* For a regional custom domain name.
*/
REGIONAL = 'REGIONAL',
}

/**
* Represents an APIGatewayV2 DomainName
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-domainname.html
Expand Down Expand Up @@ -57,14 +82,42 @@ export interface DomainNameProps {
*/
readonly domainName: string;
/**
* The ACM certificate for this domain name
* The ACM certificate for this domain name.
* Certificate can be both ACM issued or imported.
*/
readonly certificate: ICertificate;
/**
* The mutual TLS authentication configuration for a custom domain name.
* @default - mTLS is not configured.
*/
readonly mtls?: MTLSConfig
readonly mtls?: MTLSConfig;
/**
* The user-friendly name of the certificate that will be used by the endpoint for this domain name.
* This property is optional and is helpful if you have too many certificates and it is easier to remember
* certificates by some name rather that the domain they are valid for.
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved
* @default - No friendly certificate name
*/
readonly certificateName?: string;

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

/**
* The Transport Layer Security (TLS) version + cipher suite for this domain name.
* @default SecurityPolicy.TLS_1_2
*/
readonly securityPolicy?: SecurityPolicy;

/**
* A public certificate issued by ACM to validate that you own a custom domain. This parameter is required
* only when you configure mutual TLS authentication and you specify an ACM imported or private CA certificate
* for `certificate`. The ownership verification certificate validates that you have permissions to use the domain name.
* @default - only required when configuring mTLS
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly ownershipVerificationCertificate?: ICertificate;
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -107,6 +160,7 @@ export class DomainName extends Resource implements IDomainName {
public readonly name: string;
public readonly regionalDomainName: string;
public readonly regionalHostedZoneId: string;
private readonly domainNameConfigurations: CfnDomainName.DomainNameConfigurationProperty[] = [];

constructor(scope: Construct, id: string, props: DomainNameProps) {
super(scope, id);
Expand All @@ -118,18 +172,17 @@ export class DomainName extends Resource implements IDomainName {
const mtlsConfig = this.configureMTLS(props.mtls);
const domainNameProps: CfnDomainNameProps = {
domainName: props.domainName,
domainNameConfigurations: [
{
certificateArn: props.certificate.certificateArn,
endpointType: 'REGIONAL',
},
],
domainNameConfigurations: Lazy.any({ produce: () => this.domainNameConfigurations }),
mutualTlsAuthentication: mtlsConfig,
};
const resource = new CfnDomainName(this, 'Resource', domainNameProps);
this.name = resource.ref;
this.regionalDomainName = Token.asString(resource.getAtt('RegionalDomainName'));
this.regionalHostedZoneId = Token.asString(resource.getAtt('RegionalHostedZoneId'));

if (props.certificate) {
this.addDomainNameConfiguration(props);
}
}

private configureMTLS(mtlsConfig?: MTLSConfig): CfnDomainName.MutualTlsAuthenticationProperty | undefined {
Expand All @@ -139,4 +192,21 @@ export class DomainName extends Resource implements IDomainName {
truststoreVersion: mtlsConfig.version,
};
}

/**
* Adds a configuration to a domain name. Properties like certificate, endpoint type and security policy can be set using this method.
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-domainname-domainnameconfiguration.html
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved
* @param props - domain name properties to be set
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved
*/
public addDomainNameConfiguration(props: DomainNameProps) : void {
const domainNameConfig: CfnDomainName.DomainNameConfigurationProperty = {
certificateArn: props.certificate.certificateArn,
certificateName: props.certificateName,
endpointType: props.endpointType?.toString(),
ownershipVerificationCertificateArn: props.ownershipVerificationCertificate?.certificateArn,
securityPolicy: props.securityPolicy?.toString(),
};

this.domainNameConfigurations.push(domainNameConfig);
}
}
80 changes: 79 additions & 1 deletion packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { Template } from '@aws-cdk/assertions';
import { Certificate } from '@aws-cdk/aws-certificatemanager';
import { Bucket } from '@aws-cdk/aws-s3';
import { Stack } from '@aws-cdk/core';
import { DomainName, HttpApi } from '../../lib';
import { DomainName, EndpointType, HttpApi, SecurityPolicy } from '../../lib';

const domainName = 'example.com';
const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate';
const certArn2 = 'arn:aws:acm:us-east-1:111111111111:certificate2';
const ownershipCertArn = 'arn:aws:acm:us-east-1:111111111111:ownershipcertificate';

describe('DomainName', () => {
test('create domain name correctly', () => {
Expand All @@ -16,6 +18,7 @@ describe('DomainName', () => {
new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
SmritiVashisth marked this conversation as resolved.
Show resolved Hide resolved
});

// THEN
Expand Down Expand Up @@ -53,6 +56,7 @@ describe('DomainName', () => {
const dn = new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
});

// WHEN
Expand All @@ -79,6 +83,7 @@ describe('DomainName', () => {
const dn = new DomainName(stack, 'DN', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
});

api.addStage('beta', {
Expand Down Expand Up @@ -117,6 +122,7 @@ describe('DomainName', () => {
const dn = new DomainName(stack, 'DN', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
});

// WHEN
Expand Down Expand Up @@ -155,6 +161,7 @@ describe('DomainName', () => {
const dn = new DomainName(stack, 'DN', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
});
const t = () => {
new HttpApi(stack, 'Api', {
Expand All @@ -179,6 +186,7 @@ describe('DomainName', () => {
new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
mtls: {
bucket,
key: 'someca.pem',
Expand Down Expand Up @@ -209,6 +217,7 @@ describe('DomainName', () => {
new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
mtls: {
bucket,
key: 'someca.pem',
Expand All @@ -231,4 +240,73 @@ describe('DomainName', () => {
},
});
});

test('domain with mutual tls configuration and ownership cert', () => {
// GIVEN
const stack = new Stack();
const bucket = Bucket.fromBucketName(stack, 'testBucket', 'example-bucket');

// WHEN
new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert2', certArn2),
ownershipVerificationCertificate: Certificate.fromCertificateArn(stack, 'ownershipCert', ownershipCertArn),
endpointType: EndpointType.REGIONAL,
securityPolicy: SecurityPolicy.TLS_1_2,
mtls: {
bucket,
key: 'someca.pem',
version: 'version',
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::DomainName', {
DomainName: 'example.com',
DomainNameConfigurations: [
{
CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate2',
EndpointType: 'REGIONAL',
SecurityPolicy: 'TLS_1_2',
OwnershipVerificationCertificateArn: 'arn:aws:acm:us-east-1:111111111111:ownershipcertificate',
},
],
MutualTlsAuthentication: {
TruststoreUri: 's3://example-bucket/someca.pem',
TruststoreVersion: 'version',
},
});
});

test('add new configuration to a domain name for migration', () => {
// GIVEN
const stack = new Stack();

// WHEN
const dn = new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
});
dn.addDomainNameConfiguration({
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert2', certArn2),
endpointType: EndpointType.EDGE,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::DomainName', {
DomainName: 'example.com',
DomainNameConfigurations: [
{
CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate',
EndpointType: 'REGIONAL',
},
{
CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate2',
EndpointType: 'EDGE',
},
],
});
});
});