From adb63fc1bb4ab576d573acc7fa2477a4fe02b201 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 15 Jun 2020 20:53:26 +0200 Subject: [PATCH 1/9] feat(certificatemanager): native CloudFormation DNS validated certificate Automatically adding Amazon Route 53 CNAME records for DNS validation is now natively supported by CloudFormation. Add a `validation` prop to `Certificate` to handle both email and DNS validation. Deprecate `DnsValidatedCertificate`. The default remains email validation (non-breaking). Closes #5831 Closes #5835 Closes #6081 Closes #6516 Closes #7150 Closes #7941 Closes #7995 Closes #7996 --- .../@aws-cdk/aws-certificatemanager/README.md | 29 +-- .../lib/index.js | 1 + .../aws-certificatemanager/lib/certificate.ts | 183 ++++++++++++++---- .../lib/dns-validated-certificate.ts | 6 +- .../test/example.dns-validated-request.lit.ts | 26 --- .../test/example.dns.lit.ts | 34 ++++ .../test/test.certificate.ts | 98 +++++++++- 7 files changed, 302 insertions(+), 75 deletions(-) delete mode 100644 packages/@aws-cdk/aws-certificatemanager/test/example.dns-validated-request.lit.ts create mode 100644 packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts diff --git a/packages/@aws-cdk/aws-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index 9545a7bf21224..6d37b62380006 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -45,19 +45,26 @@ records for your domain. See [Validate with DNS](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html) in the Amazon Certificate Manager User Guide. -### Automatic DNS-validated certificates using Route53 +```ts +new Certificate(this, 'Certificate', { + domainName: 'hello.example.com', + validation: CertificateValidation.fromDns(), +}); +``` + +If Amazon Route 53 is your DNS provider for the requested domain, the DNS record can be +created automatically: -The `DnsValidatedCertificateRequest` class provides a Custom Resource by which -you can request a TLS certificate from AWS Certificate Manager that is -automatically validated using a cryptographically secure DNS record. For this to -work, there must be a Route 53 public zone that is responsible for serving -records under the Domain Name of the requested certificate. For example, if you -request a certificate for `www.example.com`, there must be a Route 53 public -zone `example.com` that provides authoritative records for the domain. +```ts +new Certificate(this, 'Certificate', { + domainName: 'hello.example.com', + validation: CertificateValidation.fromDns(myHostedZone), +}); +``` -Example: +When working with multiple domains, you can specify a default validation hosted zone: -[request a validated certificate example](test/example.dns-validated-request.lit.ts) +[multiple domains DNS validation](test/example.dns.lit.ts) ### Importing @@ -71,4 +78,4 @@ const certificate = Certificate.fromCertificateArn(this, 'Certificate', arn); ### Sharing between Stacks To share the certificate between stacks in the same CDK application, simply -pass the `Certificate` object between the stacks. \ No newline at end of file +pass the `Certificate` object between the stacks. diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js index 866e9405b049e..b6cad5d0112fa 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js @@ -225,6 +225,7 @@ const deleteCertificate = async function(arn, region) { * Main handler, invoked by Lambda */ exports.certificateRequestHandler = async function(event, context) { + console.log('The `DnsValidatedCertificate` construct is deprecated. See `validation` prop in `Certificate`.') var responseData = {}; var physicalResourceId; var certificateArn; diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts index fcc7ec3cf33c2..c284751ab9655 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts @@ -1,5 +1,6 @@ +import * as route53 from '@aws-cdk/aws-route53'; import { Construct, IResource, Resource, Token } from '@aws-cdk/core'; -import { CfnCertificate } from './certificatemanager.generated'; +import { CfnCertificate } from './certificatemanager.generated'; import { apexDomain } from './util'; /** @@ -40,6 +41,7 @@ export interface CertificateProps { * Has to be a superdomain of the requested domain. * * @default - Apex domain is used for every domain that's not overridden. + * @deprecated use `validation` instead. */ readonly validationDomains?: {[domainName: string]: string}; @@ -47,26 +49,112 @@ export interface CertificateProps { * Validation method used to assert domain ownership * * @default ValidationMethod.EMAIL + * @deprecated use `validation` instead. */ readonly validationMethod?: ValidationMethod; + + /** + * How to validate this certifcate + * + * @default CertificateValidation.fromEmail() + */ + readonly validation?: CertificateValidation; +} + +/** + * Properties for certificate validation + */ +export interface CertificationValidationProps { + /** + * Validation method + * + * @default ValidationMethod.EMAIL + */ + readonly method?: ValidationMethod; + + /** + * Hosted zone to use for DNS validation + * + * @default - use email validation + */ + readonly hostedZone?: route53.IHostedZone; + + /** + * A map of hosted zones to use for DNS validation + * + * @default - use `hostedZone` + */ + readonly hostedZones?: { [domainName: string]: route53.IHostedZone }; + + /** + * Validation domains to use for email validation + * + * @default - Apex domain + */ + readonly validationDomains?: { [domainName: string]: string }; +} + +/** + * How to validate a certificate + */ +export class CertificateValidation { + /** + * Validate the certifcate with DNS + * + * IMPORTANT: If neither `hostedZone` nor `hostedZones` is specified, DNS records + * must be added manually and the stack will not complete creating until the + * records are added. + * + * @param hostedZone the default hosted zone to use for all domains in the certificate + * @param hostedZones a map of hosted zones to use for domains in the certificate + */ + public static fromDns(hostedZone?: route53.IHostedZone, hostedZones?: { [domainName: string]: route53.IHostedZone }) { + return new CertificateValidation({ + method: ValidationMethod.DNS, + hostedZone, + hostedZones, + }); + } + + /** + * Validate the certifcate with Email + * + * IMPORTANT: if you are creating a certificate as part of your stack, the stack + * will not complete creating until you read and follow the instructions in the + * email that you will receive. + * + * ACM will send validation emails to the following addresses: + * + * admin@domain.com + * administrator@domain.com + * hostmaster@domain.com + * postmaster@domain.com + * webmaster@domain.com + * + * For every domain that you register. + * + * @param validationDomains a map of validation domains to use for domains in the certificate + */ + public static fromEmail(validationDomains?: { [domainName: string]: string }) { + return new CertificateValidation({ + method: ValidationMethod.EMAIL, + validationDomains, + }); + } + + /** + * The validation method + */ + public readonly method: ValidationMethod; + + /** @param props Certification validation properties */ + private constructor(public readonly props: CertificationValidationProps) { + this.method = props.method ?? ValidationMethod.EMAIL; + } } /** * A certificate managed by AWS Certificate Manager - * - * IMPORTANT: if you are creating a certificate as part of your stack, the stack - * will not complete creating until you read and follow the instructions in the - * email that you will receive. - * - * ACM will send validation emails to the following addresses: - * - * admin@domain.com - * administrator@domain.com - * hostmaster@domain.com - * postmaster@domain.com - * webmaster@domain.com - * - * For every domain that you register. */ export class Certificate extends Resource implements ICertificate { @@ -89,33 +177,29 @@ export class Certificate extends Resource implements ICertificate { constructor(scope: Construct, id: string, props: CertificateProps) { super(scope, id); + let validation: CertificateValidation; + if (props.validation) { + validation = props.validation; + } else { // Deprecated props + if (props.validationMethod === ValidationMethod.DNS) { + validation = CertificateValidation.fromDns(); + } else if (props.validationDomains) { + validation = CertificateValidation.fromEmail(props.validationDomains); + } else { + validation = CertificateValidation.fromEmail(); + } + } + const allDomainNames = [props.domainName].concat(props.subjectAlternativeNames || []); const cert = new CfnCertificate(this, 'Resource', { domainName: props.domainName, subjectAlternativeNames: props.subjectAlternativeNames, - domainValidationOptions: allDomainNames.map(domainValidationOption), - validationMethod: props.validationMethod, + domainValidationOptions: renderDomainValidation(validation, allDomainNames), + validationMethod: validation.method, }); this.certificateArn = cert.ref; - - /** - * Return the domain validation options for the given domain - * - * Closes over props. - */ - function domainValidationOption(domainName: string): CfnCertificate.DomainValidationOptionProperty { - let validationDomain = props.validationDomains && props.validationDomains[domainName]; - if (validationDomain === undefined) { - if (Token.isUnresolved(domainName)) { - throw new Error('When using Tokens for domain names, \'validationDomains\' needs to be supplied'); - } - validationDomain = apexDomain(domainName); - } - - return { domainName, validationDomain }; - } } } @@ -136,4 +220,33 @@ export enum ValidationMethod { * @see https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html */ DNS = 'DNS', -} \ No newline at end of file +} + +// tslint:disable-next-line:max-line-length +function renderDomainValidation(validation: CertificateValidation, domainNames: string[]): CfnCertificate.DomainValidationOptionProperty[] | undefined { + const domainValidation: CfnCertificate.DomainValidationOptionProperty[] = []; + + switch (validation.method) { + case ValidationMethod.DNS: + for (const domainName of domainNames) { + const hostedZone = validation.props.hostedZones?.[domainName] ?? validation.props.hostedZone; + if (hostedZone) { + domainValidation.push({ domainName, hostedZoneId: hostedZone.hostedZoneId }); + } + } + break; + case ValidationMethod.EMAIL: + for (const domainName of domainNames) { + const validationDomain = validation.props.validationDomains?.[domainName]; + if (!validationDomain && Token.isUnresolved(domainName)) { + throw new Error('When using Tokens for domain names, \'validationDomains\' needs to be supplied'); + } + domainValidation.push({ domainName, validationDomain: validationDomain ?? apexDomain(domainName) }); + } + break; + default: + throw new Error(`Unknown validation method ${validation.method}`); + } + + return domainValidation.length !== 0 ? domainValidation : undefined; +} diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts index d6b7a18a345e3..7b66332335ddf 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -8,7 +8,8 @@ import { CertificateProps, ICertificate } from './certificate'; /** * Properties to create a DNS validated certificate managed by AWS Certificate Manager * - * @experimental + * @deprecated use the `validation` prop with `CertificateValidation.fromDns()` + * on `Certificate`. */ export interface DnsValidatedCertificateProps extends CertificateProps { /** @@ -52,7 +53,8 @@ export interface DnsValidatedCertificateProps extends CertificateProps { * validated using DNS validation against the specified Route 53 hosted zone. * * @resource AWS::CertificateManager::Certificate - * @experimental + * @deprecated use the `validation` prop with `CertificateValidation.fromDns()` + * on `Certificate`. */ export class DnsValidatedCertificate extends cdk.Resource implements ICertificate { public readonly certificateArn: string; diff --git a/packages/@aws-cdk/aws-certificatemanager/test/example.dns-validated-request.lit.ts b/packages/@aws-cdk/aws-certificatemanager/test/example.dns-validated-request.lit.ts deleted file mode 100644 index 3852b9ea8ac6c..0000000000000 --- a/packages/@aws-cdk/aws-certificatemanager/test/example.dns-validated-request.lit.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as route53 from '@aws-cdk/aws-route53'; -import { App, Construct, Stack } from '@aws-cdk/core'; -import * as certmgr from '../lib'; - -class CertStack extends Stack { - constructor(scope: Construct, id: string) { - super(scope, id); - /// !show - const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { - domainName: 'example.com', - privateZone: false, - }); - - const certificate = new certmgr.DnsValidatedCertificate(this, 'TestCertificate', { - domainName: 'test.example.com', - hostedZone, - }); - /// !hide - - Array.isArray(certificate); - } -} - -const app = new App(); -new CertStack(app, 'MyStack4'); -app.synth(); diff --git a/packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts b/packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts new file mode 100644 index 0000000000000..3989019d5e1b5 --- /dev/null +++ b/packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts @@ -0,0 +1,34 @@ +import * as route53 from '@aws-cdk/aws-route53'; +import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core'; +import * as acm from '../lib'; + +class AcmStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// !show + const exampleCom = new route53.HostedZone(this, 'ExampleCom', { + zoneName: 'example.com', + }); + + const exampleNet = new route53.HostedZone(this, 'ExampelNet', { + zoneName: 'example.net', + }); + + const cert = new acm.Certificate(this, 'Certificate', { + domainName: 'test.example.com', + subjectAlternativeNames: ['cool.example.com', 'test.example.net'], + validation: acm.CertificateValidation.fromDns(exampleCom, { + 'test.example.net': exampleNet, + }), + }); + /// !hide + + new CfnOutput(this, 'Output', { + value: cert.certificateArn, + }); + } +} + +const app = new App(); +new AcmStack(app, 'AcmStack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts b/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts index 01f3da457d700..e11334a018457 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts @@ -1,7 +1,8 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import * as route53 from '@aws-cdk/aws-route53'; import { Lazy, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { Certificate, ValidationMethod } from '../lib'; +import { Certificate, CertificateValidation, ValidationMethod } from '../lib'; export = { 'apex domain selection by default'(test: Test) { @@ -104,4 +105,99 @@ export = { test.done(); }, + + 'CertificateValidation.fromEmail'(test: Test) { + const stack = new Stack(); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + subjectAlternativeNames: ['extra.example.com'], + validation: CertificateValidation.fromEmail({ + 'test.example.com': 'example.com', + }), + }); + + expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + SubjectAlternativeNames: ['extra.example.com'], + DomainValidationOptions: [ + { + DomainName: 'test.example.com', + ValidationDomain: 'example.com', + }, + { + DomainName: 'extra.example.com', + ValidationDomain: 'example.com', + }, + ], + ValidationMethod: 'EMAIL', + })); + + test.done(); + }, + + 'CertificateValidation.fromDns'(test: Test) { + const stack = new Stack(); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + subjectAlternativeNames: ['extra.example.com'], + validation: CertificateValidation.fromDns(), + }); + + expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + SubjectAlternativeNames: ['extra.example.com'], + ValidationMethod: 'DNS', + })); + + test.done(); + }, + + 'CertificateValidation.fromDns with hosted zones'(test: Test) { + const stack = new Stack(); + + const exampleCom = new route53.HostedZone(stack, 'ExampleCom', { + zoneName: 'example.com', + }); + + const exampleNet = new route53.HostedZone(stack, 'ExampleNet', { + zoneName: 'example.com', + }); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + subjectAlternativeNames: ['cool.example.com', 'test.example.net'], + validation: CertificateValidation.fromDns(exampleCom, { + 'test.example.net': exampleNet, + }), + }); + + expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + DomainValidationOptions: [ + { + DomainName: 'test.example.com', + HostedZoneId: { + Ref: 'ExampleCom20E1324B', + }, + }, + { + DomainName: 'cool.example.com', + HostedZoneId: { + Ref: 'ExampleCom20E1324B', + }, + }, + { + DomainName: 'test.example.net', + HostedZoneId: { + Ref: 'ExampleNetF7CA40C9', + }, + }, + ], + ValidationMethod: 'DNS', + })); + + test.done(); + }, }; From fb22718752dd12b81587c119bfb93e27c3c29047 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 15 Jun 2020 21:58:40 +0200 Subject: [PATCH 2/9] update ecs-patterns --- .../application-load-balanced-service-base.ts | 6 +- ...ion-multiple-target-groups-service-base.ts | 8 +- .../aws-ecs-patterns/test/ec2/test.l3s.ts | 27 ++- ...oad-balanced-fargate-service.expected.json | 170 +----------------- 4 files changed, 29 insertions(+), 182 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index f5908a12e704f..27da4d31e3be0 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -1,4 +1,4 @@ -import { DnsValidatedCertificate, ICertificate } from '@aws-cdk/aws-certificatemanager'; +import { Certificate, CertificateValidation, ICertificate } from '@aws-cdk/aws-certificatemanager'; import { IVpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, @@ -335,9 +335,9 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { if (props.certificate !== undefined) { this.certificate = props.certificate; } else { - this.certificate = new DnsValidatedCertificate(this, 'Certificate', { + this.certificate = new Certificate(this, 'Certificate', { domainName: props.domainName, - hostedZone: props.domainZone, + validation: CertificateValidation.fromDns(props.domainZone), }); } } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index e5bb9ba08ce7f..5764b3fcdaeb5 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -1,4 +1,4 @@ -import { DnsValidatedCertificate, ICertificate } from '@aws-cdk/aws-certificatemanager'; +import { Certificate, CertificateValidation, ICertificate } from '@aws-cdk/aws-certificatemanager'; import { IVpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerDefinition, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Protocol, Secret } from '@aws-cdk/aws-ecs'; @@ -536,9 +536,9 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru if (certificate !== undefined) { return certificate; } else { - return new DnsValidatedCertificate(this, `Certificate${listenerName}`, { + return new Certificate(this, `Certificate${listenerName}`, { domainName, - hostedZone: domainZone, + validation: CertificateValidation.fromDns(domainZone), }); } } @@ -611,4 +611,4 @@ interface ListenerConfig { * @default - No Route53 hosted domain zone. */ readonly domainZone?: IHostedZone; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 17693856ed8fd..5a091e0425b25 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -462,17 +462,17 @@ export = { }); // THEN - stack contains a load balancer, a service, and a certificate - expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { - ServiceToken: { - 'Fn::GetAtt': [ - 'ServiceCertificateCertificateRequestorFunctionB69CD117', - 'Arn', - ], - }, + expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { DomainName: 'api.example.com', - HostedZoneId: { - Ref: 'HostedZoneDB99F866', - }, + DomainValidationOptions: [ + { + DomainName: 'api.example.com', + HostedZoneId: { + Ref: 'HostedZoneDB99F866', + }, + }, + ], + ValidationMethod: 'DNS', })); expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer')); @@ -481,10 +481,9 @@ export = { Port: 443, Protocol: 'HTTPS', Certificates: [{ - CertificateArn: { 'Fn::GetAtt': [ - 'ServiceCertificateCertificateRequestorResource0FC297E9', - 'Arn', - ]}, + CertificateArn: { + Ref: 'ServiceCertificateA7C65FE6', + }, }], })); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json index 3f560edd9c37a..ca353bc26679b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json @@ -443,10 +443,7 @@ "Certificates": [ { "CertificateArn": { - "Fn::GetAtt": [ - "myServiceCertificateCertificateRequestorResource76DB9304", - "Arn" - ] + "Ref": "myServiceCertificate152F9DDA" } } ] @@ -463,154 +460,19 @@ } } }, - "myServiceCertificateCertificateRequestorFunctionServiceRoleB55B306C": { - "Type": "AWS::IAM::Role", + "myServiceCertificate152F9DDA": { + "Type": "AWS::CertificateManager::Certificate", "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] - ] - } - ] - } - }, - "myServiceCertificateCertificateRequestorFunctionServiceRoleDefaultPolicyFB403A3B": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "acm:RequestCertificate", - "acm:DescribeCertificate", - "acm:DeleteCertificate" - ], - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": "route53:GetChange", - "Effect": "Allow", - "Resource": "*" - }, - { - "Action": "route53:changeResourceRecordSets", - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":route53:::hostedzone/fakeId" - ] - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "myServiceCertificateCertificateRequestorFunctionServiceRoleDefaultPolicyFB403A3B", - "Roles": [ + "DomainName": "test.example.com", + "DomainValidationOptions": [ { - "Ref": "myServiceCertificateCertificateRequestorFunctionServiceRoleB55B306C" + "DomainName": "test.example.com", + "HostedZoneId": "fakeId" } - ] + ], + "ValidationMethod": "DNS" } }, - "myServiceCertificateCertificateRequestorFunctionC16CEAAF": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "S3Bucket": { - "Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3BucketFCCD3A76" - }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6" - } - ] - } - ] - } - ] - ] - } - }, - "Handler": "index.certificateRequestHandler", - "Role": { - "Fn::GetAtt": [ - "myServiceCertificateCertificateRequestorFunctionServiceRoleB55B306C", - "Arn" - ] - }, - "Runtime": "nodejs10.x", - "Timeout": 900 - }, - "DependsOn": [ - "myServiceCertificateCertificateRequestorFunctionServiceRoleDefaultPolicyFB403A3B", - "myServiceCertificateCertificateRequestorFunctionServiceRoleB55B306C" - ] - }, - "myServiceCertificateCertificateRequestorResource76DB9304": { - "Type": "AWS::CloudFormation::CustomResource", - "Properties": { - "ServiceToken": { - "Fn::GetAtt": [ - "myServiceCertificateCertificateRequestorFunctionC16CEAAF", - "Arn" - ] - }, - "DomainName": "test.example.com", - "HostedZoneId": "fakeId" - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" - }, "myServiceDNSD76FB53A": { "Type": "AWS::Route53::RecordSet", "Properties": { @@ -863,19 +725,5 @@ ] } } - }, - "Parameters": { - "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3BucketFCCD3A76": { - "Type": "String", - "Description": "S3 bucket for asset \"19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2\"" - }, - "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6": { - "Type": "String", - "Description": "S3 key for asset version \"19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2\"" - }, - "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2ArtifactHash652C125C": { - "Type": "String", - "Description": "Artifact hash for asset \"19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2\"" - } } } \ No newline at end of file From d3e9c4d3228732e7fb1289747f422ca5d9e76cf5 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 9 Jul 2020 12:04:24 +0200 Subject: [PATCH 3/9] do not deprecate DnsValidatedCertificate --- .../dns_validated_certificate_handler/lib/index.js | 1 - .../aws-certificatemanager/lib/dns-validated-certificate.ts | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js index b6cad5d0112fa..866e9405b049e 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js @@ -225,7 +225,6 @@ const deleteCertificate = async function(arn, region) { * Main handler, invoked by Lambda */ exports.certificateRequestHandler = async function(event, context) { - console.log('The `DnsValidatedCertificate` construct is deprecated. See `validation` prop in `Certificate`.') var responseData = {}; var physicalResourceId; var certificateArn; diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts index 7b66332335ddf..d6b7a18a345e3 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -8,8 +8,7 @@ import { CertificateProps, ICertificate } from './certificate'; /** * Properties to create a DNS validated certificate managed by AWS Certificate Manager * - * @deprecated use the `validation` prop with `CertificateValidation.fromDns()` - * on `Certificate`. + * @experimental */ export interface DnsValidatedCertificateProps extends CertificateProps { /** @@ -53,8 +52,7 @@ export interface DnsValidatedCertificateProps extends CertificateProps { * validated using DNS validation against the specified Route 53 hosted zone. * * @resource AWS::CertificateManager::Certificate - * @deprecated use the `validation` prop with `CertificateValidation.fromDns()` - * on `Certificate`. + * @experimental */ export class DnsValidatedCertificate extends cdk.Resource implements ICertificate { public readonly certificateArn: string; From fb792ff086f30f63a971af9f6bbb91f1a80ea4a7 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 9 Jul 2020 12:12:28 +0200 Subject: [PATCH 4/9] README for DnsValidatedCertificate --- .../@aws-cdk/aws-certificatemanager/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/@aws-cdk/aws-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index 3c5be75d0afbb..f4d90361b1307 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -66,6 +66,23 @@ When working with multiple domains, you can specify a default validation hosted [multiple domains DNS validation](test/example.dns.lit.ts) +Use the `DnsValidatedCertificate` construct for cross-region certificate creation: + +```ts +new DnsValidatedCertificate(this, 'CrossRegionCertificate', { + domainName: 'hello.example.com', + hostedZone: myHostedZone, + region: 'us-east-1', +}); +``` + +This is useful when deploying a stack in a region other than `us-east-1` with a +certificate for a CloudFront distribution. + +If cross-region is not needed, the recommended solution is to use the +`Certificate` construct which uses a native CloudFormation implementation. + + ### Importing If you want to import an existing certificate, you can do so from its ARN: From 0248018127e951c4ce9ac8f322d52da5de06d111 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 9 Jul 2020 12:18:54 +0200 Subject: [PATCH 5/9] README DNS --- .../@aws-cdk/aws-certificatemanager/README.md | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index f4d90361b1307..4979f8c49f05a 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -24,10 +24,6 @@ Because of this wait time, it's better to provision your certificates either in a separate stack from your main service, or provision them manually and import them into your CDK application. -The CDK also provides a custom resource which can be used for automatic -validation if the DNS records for the domain are managed through Route53 (see -below). - ### Email validation Email-validated certificates (the default) are validated by receiving an @@ -39,29 +35,29 @@ in the AWS Certificate Manager User Guide. ### DNS validation -DNS-validated certificates are validated by configuring appropriate DNS -records for your domain. - -See [Validate with DNS](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html) -in the AWS Certificate Manager User Guide. +If Amazon Route 53 is your DNS provider for the requested domain, the DNS record can be +created automatically: ```ts new Certificate(this, 'Certificate', { domainName: 'hello.example.com', - validation: CertificateValidation.fromDns(), + validation: CertificateValidation.fromDns(myHostedZone), // Route 53 hosted zone }); ``` -If Amazon Route 53 is your DNS provider for the requested domain, the DNS record can be -created automatically: +Otherwise DNS records must be added manually and the stack will not complete +creating until the records are added. ```ts new Certificate(this, 'Certificate', { domainName: 'hello.example.com', - validation: CertificateValidation.fromDns(myHostedZone), + validation: CertificateValidation.fromDns(), // Records must be added manually }); ``` +See also [Validate with DNS](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html) +in the AWS Certificate Manager User Guide. + When working with multiple domains, you can specify a default validation hosted zone: [multiple domains DNS validation](test/example.dns.lit.ts) From 3b1ee069c17cbf754d41b8c49211ab1faa25b465 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 9 Jul 2020 12:36:30 +0200 Subject: [PATCH 6/9] fromDnsMultiZone --- .../@aws-cdk/aws-certificatemanager/README.md | 8 ++--- .../aws-certificatemanager/lib/certificate.ts | 29 ++++++++++------ .../test/example.dns.lit.ts | 4 ++- .../test/test.certificate.ts | 34 +++++++++++++++++-- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index 4979f8c49f05a..15fbc1a8e09b4 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -20,9 +20,9 @@ After requesting a certificate, you will need to prove that you own the domain in question before the certificate will be granted. The CloudFormation deployment will wait until this verification process has been completed. -Because of this wait time, it's better to provision your certificates -either in a separate stack from your main service, or provision them -manually and import them into your CDK application. +Because of this wait time, when using manual validation methods, it's better +to provision your certificates either in a separate stack from your main +service, or provision them manually and import them into your CDK application. ### Email validation @@ -58,7 +58,7 @@ new Certificate(this, 'Certificate', { See also [Validate with DNS](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html) in the AWS Certificate Manager User Guide. -When working with multiple domains, you can specify a default validation hosted zone: +When working with multiple domains, use the `CertificateValidation.fromDnsMultiZone()`: [multiple domains DNS validation](test/example.dns.lit.ts) diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts index c284751ab9655..8217c66c942ac 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts @@ -1,6 +1,6 @@ import * as route53 from '@aws-cdk/aws-route53'; import { Construct, IResource, Resource, Token } from '@aws-cdk/core'; -import { CfnCertificate } from './certificatemanager.generated'; +import { CfnCertificate } from './certificatemanager.generated'; import { apexDomain } from './util'; /** @@ -101,17 +101,28 @@ export class CertificateValidation { /** * Validate the certifcate with DNS * - * IMPORTANT: If neither `hostedZone` nor `hostedZones` is specified, DNS records - * must be added manually and the stack will not complete creating until the - * records are added. + * IMPORTANT: If `hostedZone` is not specified, DNS records must be added + * manually and the stack will not complete creating until the records are + * added. * - * @param hostedZone the default hosted zone to use for all domains in the certificate - * @param hostedZones a map of hosted zones to use for domains in the certificate + * @param hostedZone the hosted zone where DNS records must be created */ - public static fromDns(hostedZone?: route53.IHostedZone, hostedZones?: { [domainName: string]: route53.IHostedZone }) { + public static fromDns(hostedZone?: route53.IHostedZone) { return new CertificateValidation({ method: ValidationMethod.DNS, hostedZone, + }); + } + + /** + * Validate the certifcate with DNS + * + * @param hostedZones a map of hosted zones where DNS records must be created + * for the domains in the certificate + */ + public static fromDnsMultiZone(hostedZones: { [domainName: string]: route53.IHostedZone }) { + return new CertificateValidation({ + method: ValidationMethod.DNS, hostedZones, }); } @@ -183,10 +194,8 @@ export class Certificate extends Resource implements ICertificate { } else { // Deprecated props if (props.validationMethod === ValidationMethod.DNS) { validation = CertificateValidation.fromDns(); - } else if (props.validationDomains) { - validation = CertificateValidation.fromEmail(props.validationDomains); } else { - validation = CertificateValidation.fromEmail(); + validation = CertificateValidation.fromEmail(props.validationDomains); } } diff --git a/packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts b/packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts index 3989019d5e1b5..c2cfd41ff0e15 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts @@ -17,7 +17,9 @@ class AcmStack extends Stack { const cert = new acm.Certificate(this, 'Certificate', { domainName: 'test.example.com', subjectAlternativeNames: ['cool.example.com', 'test.example.net'], - validation: acm.CertificateValidation.fromDns(exampleCom, { + validation: acm.CertificateValidation.fromDnsMultiZone({ + 'text.example.com': exampleCom, + 'cool.example.com': exampleCom, 'test.example.net': exampleNet, }), }); diff --git a/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts b/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts index e11334a018457..3b0de2e1ba1f2 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts @@ -154,7 +154,35 @@ export = { test.done(); }, - 'CertificateValidation.fromDns with hosted zones'(test: Test) { + 'CertificateValidation.fromDns with hosted zone'(test: Test) { + const stack = new Stack(); + + const exampleCom = new route53.HostedZone(stack, 'ExampleCom', { + zoneName: 'example.com', + }); + + new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + validation: CertificateValidation.fromDns(exampleCom), + }); + + expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + DomainValidationOptions: [ + { + DomainName: 'test.example.com', + HostedZoneId: { + Ref: 'ExampleCom20E1324B', + }, + }, + ], + ValidationMethod: 'DNS', + })); + + test.done(); + }, + + 'CertificateValidation.fromDnsMultiZone'(test: Test) { const stack = new Stack(); const exampleCom = new route53.HostedZone(stack, 'ExampleCom', { @@ -168,7 +196,9 @@ export = { new Certificate(stack, 'Certificate', { domainName: 'test.example.com', subjectAlternativeNames: ['cool.example.com', 'test.example.net'], - validation: CertificateValidation.fromDns(exampleCom, { + validation: CertificateValidation.fromDnsMultiZone({ + 'test.example.com': exampleCom, + 'cool.example.com': exampleCom, 'test.example.net': exampleNet, }), }); From 01b59411bb1b51420d5ca009a9c8f05f0c309d73 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 9 Jul 2020 12:40:03 +0200 Subject: [PATCH 7/9] JSDoc --- packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts index 8217c66c942ac..387955d4f1f52 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts @@ -115,7 +115,8 @@ export class CertificateValidation { } /** - * Validate the certifcate with DNS + * Validate the certifcate with automatically created DNS records in multiple + * AWS Route 53 hosted zones. * * @param hostedZones a map of hosted zones where DNS records must be created * for the domains in the certificate From a371fbd9c5645ab81cdafb0b7d0c9a173cf21cf5 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 9 Jul 2020 12:44:04 +0200 Subject: [PATCH 8/9] AWS Route 53 -> Amazon Route 53 --- packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts index 387955d4f1f52..37606b21a874e 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts @@ -116,7 +116,7 @@ export class CertificateValidation { /** * Validate the certifcate with automatically created DNS records in multiple - * AWS Route 53 hosted zones. + * Amazon Route 53 hosted zones. * * @param hostedZones a map of hosted zones where DNS records must be created * for the domains in the certificate From 538b6589720e04a318fd39567843e797d6db7758 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 9 Jul 2020 21:35:52 +0200 Subject: [PATCH 9/9] revert delete example.dns-validated-request.lit.ts --- .../test/example.dns-validated-request.lit.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/@aws-cdk/aws-certificatemanager/test/example.dns-validated-request.lit.ts diff --git a/packages/@aws-cdk/aws-certificatemanager/test/example.dns-validated-request.lit.ts b/packages/@aws-cdk/aws-certificatemanager/test/example.dns-validated-request.lit.ts new file mode 100644 index 0000000000000..3852b9ea8ac6c --- /dev/null +++ b/packages/@aws-cdk/aws-certificatemanager/test/example.dns-validated-request.lit.ts @@ -0,0 +1,26 @@ +import * as route53 from '@aws-cdk/aws-route53'; +import { App, Construct, Stack } from '@aws-cdk/core'; +import * as certmgr from '../lib'; + +class CertStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// !show + const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { + domainName: 'example.com', + privateZone: false, + }); + + const certificate = new certmgr.DnsValidatedCertificate(this, 'TestCertificate', { + domainName: 'test.example.com', + hostedZone, + }); + /// !hide + + Array.isArray(certificate); + } +} + +const app = new App(); +new CertStack(app, 'MyStack4'); +app.synth();