How to target Route53 A record to ApiGateway v2 - aws-cloudformation

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',
},
});

Related

How to prevent CDK from creating default base path mapping for new DomainName

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'
}
});

CDK API Gateway default authorizer exclude OPTIONS method

I'm creating a LambdaRestApi in CDK and I want to have both CORS enabled and add an ANY proxy using the addProxy method.
I currently have the following CDK code:
const api = new LambdaRestApi(...); // This API has CORS enabled in defaultCorsPreflightOptions
const apiProxy = api.root.addProxy({
defaultMethodOptions: {
authorizationType: AuthorizationType.COGNITO,
authorizer: new CognitoUserPoolsAuthorizer(...),
}
});
The problem I'm running into is that while a proxy is created with the ANY method, it also sets the OPTIONS method to require authentication. I tried to add an OPTIONS method to the proxy using addMethod to override the authorizer but I get an error that there's already a construct with the same name. I'm also trying to avoid having to set the anyMethod field in the proxy to be false and adding my own methods. Is there a way in the API Gateway CDK to set the default authorizer to only work for any method except the OPTIONS method?
There is also a possibility to remove the auth explicitly on the OPTIONS method, here I also remove requirement for X-API-Key in the requests
const lambdaApi = new apigateway.LambdaRestApi(...) // This API has CORS enabled in defaultCorsPreflightOptions
lambdaApi.methods
.filter((method) => method.httpMethod === "OPTIONS")
.forEach((method) => {
const methodCfn = method.node.defaultChild as apigateway.CfnMethod;
methodCfn.authorizationType = apigateway.AuthorizationType.NONE;
methodCfn.authorizerId = undefined;
methodCfn.authorizationScopes = undefined;
methodCfn.apiKeyRequired = false;
});
I ran into the same issue when building a RestApi using the aws cdk. Here is a workaround where you can build the api piece by piece.
Declare the api construct without the defaultCorsPreflightOptions property, otherwise you will not be able to override Authorization on the OPTIONS method.
import * as apigateway from '#aws-cdk/aws-apigateway';
import * as lambda from '#aws-cdk/aws-lambda';
const restAPI = new apigateway.RestApi(this, "sample-api");
Add your resources and methods. In this case, I want to add an ANY method with a custom authorizer on the "data" resource. You can do this with any other supported authorization mechanism. The proxy handler is a lambda function created with NodeJs.
const dataProxyLambdaFunction = new lambda.Function(this, "data-handler", {
code: lambda.Code.fromBucket(S3CODEBUCKET, "latest/js_data.zip"),
handler: "index.handler",
runtime: lambda.Runtime.NODEJS_14_X
});
const dataProxy = restAPI.root.addResource("data")
.addResource("{proxy+}");
dataProxy.addMethod("ANY", new apigateway.LambdaIntegration(dataProxyLambdaFunction , {
allowTestInvoke: true,
}), { authorizationType: apigateway.AuthorizationType.CUSTOM, authorizer: customLambdaRequestAuthorizer });
Now, you can add an OPTIONS method to this proxy, without authorization. I used a standardCorsMockIntegration and optionsMethodResponse object to reuse with other methods in my api.
const ALLOWED_HEADERS = ['Content-Type', 'X-Amz-Date', 'X-Amz-Security-Token', 'Authorization', 'X-Api-Key', 'X-Requested-With', 'Accept', 'Access-Control-Allow-Methods', 'Access-Control-Allow-Origin', 'Access-Control-Allow-Headers'];
const standardCorsMockIntegration = new apigateway.MockIntegration({
integrationResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': `'${ALLOWED_HEADERS.join(",")}'`,
'method.response.header.Access-Control-Allow-Origin': "'*'",
'method.response.header.Access-Control-Allow-Credentials': "'false'",
'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
},
}],
passthroughBehavior: apigateway.PassthroughBehavior.NEVER,
requestTemplates: {
"application/json": "{\"statusCode\": 200}"
}
});
const optionsMethodResponse = {
statusCode: '200',
responseModels: {
'application/json': apigateway.Model.EMPTY_MODEL
},
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': true,
'method.response.header.Access-Control-Allow-Methods': true,
'method.response.header.Access-Control-Allow-Credentials': true,
'method.response.header.Access-Control-Allow-Origin': true,
}
};
dataProxy.addMethod("OPTIONS", standardCorsMockIntegration, {
authorizationType: apigateway.AuthorizationType.NONE,
methodResponses: [
optionsMethodResponse
]
});
When you deploy the API, you can verify using the API Gateway console that your methods have been setup correctly. The proxy's ANY method has authorization enabled while the OPTIONS method does not.
Reference to the GitHub issue that helped: GitHub Issue 'apigateway: add explicit support for CORS'

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
});

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"