How to prevent CDK from creating default base path mapping for new DomainName - aws-api-gateway

When you create a CustomDomain while creating a RestApi OR you .addDomainName after creating the RestApi in CDK, a default base path mapping is created for the specified stage to the root of the domain.
I don't want that to be created. I want to create my own base path mapping. When I add one via domain.addBasePathMapping(), I end up with both a mapping to the root and a mapping to the specified base path. Like so:
api: example.com / stage: dev / path: (none) // don't want this
one.
api: example.com / stage: dev / path: the-base-path //want this
one.
Is there a way to either change the default base path mapping OR prevent it from being created?
Code reproduces the issue:
const apiSpec = <openapi spec loaded here>
const zone = route53.HostedZone.fromLookup(this, 'theZone', {
domainName: 'example.com'
});
//Get the existing certificate
const acmCertificate = acm.Certificate.fromCertificateArn(this, 'Certificate', CERTIFICATE_ARN);
const apiDomainName = 'example.com';
const theApi = new apigateway.SpecRestApi(this, `the-example-api`, {
deploy: true,
restApiName: 'ApiNameHere',
deployOptions: {
stageName: 'dev',
},
endpointTypes: [ apigateway.EndpointType.REGIONAL ],
apiDefinition: apigateway.ApiDefinition.fromInline(apiSpec),
endpointExportName: `endpointExportName`,
domainName: {
domainName: apiDomainName,
certificate: acmCertificate,
securityPolicy: apigateway.SecurityPolicy.TLS_1_2
}
});
const domain = theApi.domainName
domain.addBasePathMapping(theApi, {basePath: 'the-base-path', stage: theApi.deploymentStage});
//Create alias record to route to apis
const aRecord = new route53.ARecord(this, 'alias-record', {
recordName: apiDomainName,
zone,
target: route53.RecordTarget.fromAlias(new targets.ApiGateway(theApi))
});

The addDomainName method on BaseRestApi always uses the restApi object as the mapping parameter as seen here: https://github.com/aws/aws-cdk/blob/b3a7d5ba67bec09e422c0c843d7dee4653fe9aec/packages/%40aws-cdk/aws-apigateway/lib/restapi.ts#L346-L355
If you don't want that, don't provide the domainName parameter when creating the api and instead create it separate like this:
const domain = new apigateway.DomainName(theApi, 'CustomDomain', {
domainName: apiDomainName,
certificate: acmCertificate,
securityPolicy: apigateway.SecurityPolicy.TLS_1_2
});
domain.addBasePathMapping(theApi, {basePath: 'the-base-path', stage: theApi.deploymentStage});

Looks like this was a bug that someone had reported in this github issue. It has since been fixed. Now, when you create an api, you can add basePath as an option to the DomainNameOptions, preventing it from creating two separate mappings. E.g.
const theApi = new apigateway.SpecRestApi(this, `the-example-api`, {
...
...
domainName: {
domainName: apiDomainName,
certificate: acmCertificate,
securityPolicy: apigateway.SecurityPolicy.TLS_1_2,
basePath: 'the-base-path'
}
});

Related

assumed-role is not authorized to perform: route53:ListHostZonesByDomain; Adding a Route53 Policy to a CodePipeline CodeBuildAction's Assumed Rule

My goal is to create a website at subdomain.mydomain.com pointing to a CloudFront CDN distributing a Lambda running Express that's rendering an S3 website. I'm using AWS CDK to do this.
I have an error that says
[Error at /plants-domain] User: arn:aws:sts::413025517373:assumed-role/plants-pipeline-BuildCDKRole0DCEDB8F-1BHVX6Z6H5X0H/AWSCodeBuild-39a582bf-8b89-447e-a6b4-b7f7f13c9db1 is not authorized to perform: route53:ListHostedZonesByName
It means:
[Error at /plants-domain] - error in the stack called plants-domain
User: arn:aws:sts::1234567890:assumed-role/plants-pipeline-BuildCDKRole0DCEDB8F-1BHVX6Z6H5X0H/AWSCodeBuild-39a582bf-8b89-447e-a6b4-b7f7f13c9db is the ARN of the Assumed Role associated with my object in the plants-pipeline executing route53.HostedZone.fromLookup() (but which object is it??)
is not authorized to perform: route53:ListHostedZonesByName the Assumed Role needs additional Route53 permissions
I believe this policy will permit the object in question to lookup the Hosted Zone:
const listHostZonesByNamePolicy = new IAM.PolicyStatement({
actions: ['route53:ListHostedZonesByName'],
resources: ['*'],
effect: IAM.Effect.ALLOW,
});
The code using Route53.HostedZone.fromLookup() is in the first stack domain.ts. My other stack consumes the domain.ts template using CodePipelineAction.CloudFormationCreateUpdateStackAction (see below)
domain.ts
// The addition of this zone lookup broke CDK
const zone = route53.HostedZone.fromLookup(this, 'baseZone', {
domainName: 'domain.com',
});
// Distribution I'd like to point my subdomain.domain.com to
const distribution = new CloudFront.CloudFrontWebDistribution(this, 'website-cdn', {
// more stuff goes here
});
// Create the subdomain aRecord pointing to my distribution
const aRecord = new route53.ARecord(this, 'aliasRecord', {
zone: zone,
recordName: 'subdomain',
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
});
pipeline.ts
const pipeline = new CodePipeline.Pipeline(this, 'Pipeline', {
pipelineName: props.name,
restartExecutionOnUpdate: false,
});
// My solution to the missing AssumedRole synth error: Create a Role, add the missing Policy to it (and the Pipeline, just in case)
const buildRole = new IAM.Role(this, 'BuildRole', {
assumedBy: new IAM.ServicePrincipal('codebuild.amazonaws.com'),
path: '/',
});
const listHostZonesByNamePolicy = new IAM.PolicyStatement({
actions: ['route53:ListHostedZonesByName'],
resources: ['*'],
effect: IAM.Effect.ALLOW,
});
buildRole.addToPrincipalPolicy(listHostZonesByNamePolicy);
pipeline.addStage({
// This is the action that fails, when it calls `cdk synth`
stageName: 'Build',
actions: [
new CodePipelineAction.CodeBuildAction({
actionName: 'CDK',
project: new CodeBuild.PipelineProject(this, 'BuildCDK', {
projectName: 'CDK',
buildSpec: CodeBuild.BuildSpec.fromSourceFilename('./aws/buildspecs/cdk.yml'),
role: buildRole, // this didn't work
}),
input: outputSources,
outputs: [outputCDK],
runOrder: 10,
role: buildRole, // this didn't work
}),
new CodePipelineAction.CodeBuildAction({
actionName: 'Assets',
// other stuff
}),
new CodePipelineAction.CodeBuildAction({
actionName: 'Render',
// other stuff
}),
]
})
pipeline.addStage({
stageName: 'Deploy',
actions: [
// This is the action calling the compiled domain stack template
new CodePipelineAction.CloudFormationCreateUpdateStackAction({
actionName: 'Domain',
templatePath: outputCDK.atPath(`${props.name}-domain.template.json`),
stackName: `${props.name}-domain`,
adminPermissions: true,
runOrder: 50,
role: buildRole, // this didn't work
}),
// other actions
]
});
With the above configuration, unfortunately, I still receive the same error:
[Error at /plants-domain] User: arn:aws:sts::413025517373:assumed-role/plants-pipeline-BuildCDKRole0DCEDB8F-1BHVX6Z6H5X0H/AWSCodeBuild-957b18fb-909d-4e22-94f0-9aa6281ddb2d is not authorized to perform: route53:ListHostedZonesByName
With the Assumed Role ARN, is it possible to track down the object missing permissions? Is there another way to solve my IAM/AssumedUser role problem?
Here is the answer from the official doco: https://docs.aws.amazon.com/cdk/api/latest/docs/pipelines-readme.html#context-lookups
TLDR:
pipeline by default cannot do lookups -> 2 options:
synth on dev machine (make sure a dev has permissions)
add policy for lookups
new CodePipeline(this, 'Pipeline', {
synth: new CodeBuildStep('Synth', {
input: // ...input...
commands: [
// Commands to load cdk.context.json from somewhere here
'...',
'npm ci',
'npm run build',
'npx cdk synth',
// Commands to store cdk.context.json back here
'...',
],
rolePolicyStatements: [
new iam.PolicyStatement({
actions: ['sts:AssumeRole'],
resources: ['*'],
conditions: {
StringEquals: {
'iam:ResourceTag/aws-cdk:bootstrap-role': 'lookup',
},
},
}),
],
}),
});
Based on the error, the pipeline role (and it would work at the stage or action...)
By default a new role is being created for the pipeline:
role?
Type: IRole (optional, default: a new IAM role will be created.)
The IAM role to be assumed by this Pipeline.
Instead, when you are constructing your pipeline add the buildRole there:
const pipeline = new CodePipeline.Pipeline(this, 'Pipeline', {
pipelineName: props.name,
restartExecutionOnUpdate: false,
role: buildRole
});
Based on your pipeline you never assigned the role to the relevant stage action according to the docs:
pipeline.addStage({
stageName: 'Deploy',
actions: [
// This is the action calling the compiled domain stack template
new CodePipelineAction.CloudFormationCreateUpdateStackAction({
...
role: buildRole, // this didn't work
}),
// other actions
]
});
Should be:
pipeline.addStage({
stageName: 'Deploy',
actions: [
// This is the action calling the compiled domain stack template
new CodePipelineAction.CloudFormationCreateUpdateStackAction({
....
deploymentRole: buildRole
}),
]
});
Why is it deploymentRole instead of just role, no one knows.

Circular dependency of defining APIGateway, ARecord, and Certificate

In CDK, I would like to define:
An APIGateway (backed by a Lambda)
An ARecord to resolve to the APIGateway
A Certificate associated with the ARecord
The code to do this would look something like:
let apig = new LambdaRestApi(this, 'api', {
handler: <some Lambda Function>,
proxy: true,
deploy: true,
domainName: {
domainName: aRecord.domainName,
certificate: cert
}
})
let aRecord = new ARecord(this, 'apiDNS', {
zone: zone,
recordName: props.recordName,
target: RecordTarget.fromAlias(new ApiGateway(apig))
})
let cert = new Certificate(this, 'cert', {
domainName: aRecord.domainName,
validation: CertificateValidation.fromDns(zone)
});
However, this appears to be impossible because of circular dependencies:
The APIGateway needs to have a domain name set at construction-time (there's an .addDomainName method, but even when that is called, I still get an error API does not define a default domain name when trying to call RecordTarget.fromAlias(...)), which requires that both the Certificate and the ARecord exist.
The ARecord needs to have a target, which requires that the ApiGateway exists.
The Certificate needs to have a DomainName set, which requires that the ARecord exists
I guess I could manually determine what the ARecord's domainName would be, and set that as the domainName for the Certificate and the APIGateway - but that feels like I'm doing something wrong. Is there a way to "lazily" set the APIGateway's domainName to "the domainName of the ARecord (that hasn't been created yet)?", or some other way to achieve this setup with explicit (rather than implicit) associations?
As already mentioned in ticket, it is not possible for CDK or CloudFormation to create resource which depends on each other.
Here is blog on Circular Dependency
Resource A is dependent on Resource B, and Resource B is dependent on
Resource A. When AWS CloudFormation assesses that this type of
condition exists, you will get a circular dependency error because AWS
CloudFormation is unable to clearly determine which resource should be
created first.
In this case, we can create a ACM Certificate for subDomain and then pass the domain and certificate to LambdaRestApi.
Here is the modified CDK.
const rootDomain = "mydomain.com";
const subDomain = "api-test";
const zone = route53.HostedZone.fromLookup(this, "baseZone", {
domainName: rootDomain,
});
let cert = new acm.Certificate(this, "cert", {
domainName: `${subDomain}.${rootDomain}`,
validation: acm.CertificateValidation.fromDns(zone),
});
const backend = new lambda.Function(this, "MyLayeredLambda", {
code: new lambda.InlineCode("foo"),
handler: "index.handler",
runtime: lambda.Runtime.NODEJS_10_X,
});
const restApi = new apigw.LambdaRestApi(this, "myapi", {
handler: backend,
domainName: {
domainName: `${subDomain}.${rootDomain}`,
certificate: cert,
endpointType: apigw.EndpointType.REGIONAL,
},
});
new route53.ARecord(this, "apiDNS", {
zone: zone,
recordName: `${subDomain}`,
target: route53.RecordTarget.fromAlias(
new route53Targets.ApiGateway(restApi)
),
});
}

How can I refer to the generated domain name of `elasticsearch.CfnDomain` in AWS CDK?

I created a CfnDomain in AWS CDK and I was trying to get the generated domain name to create an alarm.
const es = new elasticsearch.CfnDomain(this, id, esProps);
new cloudwatch.CfnAlarm(this, "test", {
...
dimensions: [
{
name: "DomainName",
value: es.domainName,
},
],
});
But it seems that the domainName attribute is actually the argument that I pass in (I passed none so it will be autogenerated), so it's actually undefined and can't be used.
Is there any way that I can specify it such that it will wait for the elasticsearch cluster to be created so that I can obtain the generated domain name, or is there any other way to created an alarm for the metrics of the cluster?
You use CfnDomain.ref as the domain value for your dimension. Sample alarm creation for red cluster status:
const domain: CfnDomain = ...;
const elasticDimension = {
"DomainName": domain.ref,
};
const metricRed = new Metric({
namespace: "AWS/ES",
metricName: "ClusterStatus.red",
statistic: "maximum",
period: Duration.minutes(1),
dimensions: elasticDimension
});
const redAlarm = metricRed.createAlarm(construct, "esRedAlarm", {
alarmName: "esRedAlarm",
evaluationPeriods: 1,
threshold: 1
});

How to target Route53 A record to ApiGateway v2

There's an alias for ApiGateway 1, but it's interface doesn't conform to V2:
Here's domainName:
const domainName = new apigw2.DomainName(config.scope, config.id + 'DomainName', {
domainName: config.domainName,
certificate: config.certificate,
});
It doesn't look like aws-route53-targets packages supports apigatewayv2 yet. In the meantime you can probably wrap the v2 object in the v1 interface like this:
new route53.ARecord(config.scope, config.id + "AliasRecord", {
recordName: config.domainName,
target: route53.RecordTarget.fromAlias(
new route53targets.ApiGatewayDomain({
...domainName,
domainNameAliasDomainName: domainName.regionalDomainName,
domainNameAliasHostedZoneId: domainName.regionalHostedZoneId
})
),
zone: config.hostedZone
});
The documentation for Custom Domain is the starting point. However, it does not generate any record in Route53 by default.
To get this sorted for ApiGatewayV2:
Follow the steps as in the mentioned documentation and set up your API Gateway.
You need to use #aws-cdk/aws-route53 and #aws-cdk/aws-route53-targets and the example that mentions API Gateway V2.
So that:
// From the API Gateway setup (step 1)
const apiProdDomain = new DomainName(this, '...', {...})
...
new r53.ARecord(this, 'YourDomainAliasRecord', {
zone: yourDomainHostedZone,
recordName: yourDomainPrefix, // i.e 'api' for api.xxx.com
target: r53.RecordTarget.fromAlias(new ApiGatewayv2DomainProperties(apiProdDomain.regionalDomainName, apiProdDomain.regionalHostedZoneId)
})
That's it.
You could try it the way it's described in the documentation for Custom Domain
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}/foo goes to prodApi $default stage
defaultDomainMapping: {
domainName: dn,
mappingKey: 'foo',
},
});

ApiGateway: Only one base path mapping is allowed if the base path is empty

Creating an API Gateway I get this error:
api-mapping (apimappingXXXXXXX) Only one base path mapping is allowed
if the base path is empty. (Service: AmazonApiGateway; Status Code:
409; Error Code: ConflictException;
where my code is:
// External API Gateway
const externalApi = new apigateway.RestApi(this, 'external-api-gw',
{
apiKeySourceType: apigateway.ApiKeySourceType.AUTHORIZER,
restApiName: 'external-api',
deploy: false,
endpointConfiguration: {
...
},
policy: new iam.PolicyDocument({
statements: [
..
],
}),
]
})
}
);
const domainName = externalApi.addDomainName('domain-name', {
domainName: props.apigatewayRecordName + '.' + props.hostedZone,
certificate: existingCertificate,
endpointType: apigateway.EndpointType.REGIONAL,
});
const myApiGateway = new route53targets.ApiGateway(externalApi);
// deployment
const apiDeployment = new apigateway.Deployment(this, 'deployment', {
api: externalApi
});
// stage
const apiStage = new apigateway.Stage(this, 'stage', {
stageName: 'api',
accessLogDestination: new apigateway.LogGroupLogDestination(logGroup),
accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields(),
loggingLevel: apigateway.MethodLoggingLevel.INFO,
dataTraceEnabled: true,
deployment: apiDeployment
});
externalApi.deploymentStage = apiStage;
domainName.addBasePathMapping(externalApi, { basePath: 'api', stage: apiStage} );
It seems that an empty base path mapping is created automatically and the second one cannot be added.
Any suggestions, please?
As mentioned in comments, Below you can find the working code snippet:
Create CfnDomainName:
// import relevant data from ssm
const certificateArn = ssm.StringParameter.valueFromLookup(this, 'CertificateArn');
const domainNameURL = ssm.StringParameter.valueFromLookup(this, 'ApiCustomDomainUrl');
const certificate = cert.Certificate.fromCertificateArn(this, 'Certificate', certificateArn);
// create DomainName
const domainName = new apigateway.DomainName(this, 'DomainName', {
domainName: domainNameURL,
certificate: certificate ,
endpointType: apigateway.EndpointType.REGIONAL,
});
Add base path mapping:
// create the api
const api = new apigateway.RestApi(this, id, {
restApiName: 'API GW ' + id,
deployOptions: {
stageName: 'dev',
},
endpointTypes: [apigateway.EndpointType.REGIONAL]
});
// add new base path to domain name
new apigateway.BasePathMapping(this, 'my-mapping', {
domainName: domainName,
restApi: api,
basePath: 'my-mapping'
});
// add new base path to domain name
new apigateway.BasePathMapping(this, 'my-mapping-two', {
domainName: domainName,
restApi: api,
basePath: 'my-mapping-two'
});
More about BasePathMapping , DomainName.
Not sure if something changed over the versions, but this does not work with 1.57 of the CDK.
Will generate: "Error: API does not define a default name"