AWS API Gateway, default base mappings with CDK - amazon-web-services

I'm setting up an environment with AWS CDK but I'm having trouble with API Gateway and base mappings for custom domains.
I got an API that should have two stages: "internal" and "external".
Whenever I create a new RestApi and either specify domainName as a prop to the construcor, or use the addDomainName method afterwards. This will always create a default Base Mapping, which I do not want. I want to add my own mappings like this:
apiGateway.domainName.addBasePathMapping(apiGateway, { basePath: 'internal', stage: internalStage });
apiGateway.domainName.addBasePathMapping(apiGateway, { basePath: 'external', stage: externalStage });
The problem if I do the above is that the default mapping have already been created and since it will be created with an empty basePath, I can't add any other mappings to the same API.
I've checked the source code and there does not seem to be a way to pass mappings when you add a domain, they are always created automatically.
Is there a way to change the default mappings, or to get pass this problem in another way?
Example that would be nice:
apiGateway.domainName.basePathMappings[0] = { ... }
My code right now:
const apiGateway = new apigw.RestApi(this, 'RestApi', {
deploy: false,
domainName: {
domainName: 'sub.example.com',
certificate,
endpointType: apigw.EndpointType.REGIONAL,
securityPolicy: apigw.SecurityPolicy.TLS_1_2,
},
});
const deployment = new apigw.Deployment(this, 'Deployment', { api: apiGateway });
const internalStage = new apigw.Stage(this, 'InternalStage', {
stageName: 'internal',
deployment,
});
apiGateway.domainName.addBasePathMapping(apiGateway, { basePath: 'internal', stage: internalStage });
const externalStage = new apigw.Stage(this, 'ExternalStage', {
stageName: 'external',
deployment,
});
apiGateway.domainName.addBasePathMapping(apiGateway, { basePath: 'external', stage: externalStage });
The generated syntax when i run Synth, will show 3 different AWS::ApiGateway::BasePathMapping.
One for internal, one for external (with basePath set correctly) and one is the default created one with no basePath (which I want gone).

The moment we add a domainName either by passing to RestApi or by calling .addDomainName , cdk is adding a base path mapping /.
I was able to work around by using cfn resources for DomainName and Base path mapping.
const cfnInternalDomain = new apigw.CfnDomainName(this, "internal-domain", {
domainName: internalDomainName,
regionalCertificateArn: myCert.certificateArn,
endpointConfiguration: { types: [apigw.EndpointType.REGIONAL] },
});
const intBasePath = new apigw.CfnBasePathMapping(
this,
"internal-base-path",
{
basePath: "intPath",
domainName: cfnInternalDomain.ref,
restApiId: myRestApi.restApiId,
stage: internalStage.stageName,
}
);
This is full code.
const myRestApi = new apigw.RestApi(this, "rest-api", {
deploy: false,
});
myRestApi.root.addMethod("ANY", new apigw.MockIntegration());
const deployment = new apigw.Deployment(this, "api-deployment", {
api: myRestApi,
retainDeployments: false,
});
const internalStage = new apigw.Stage(this, "internal-stage", {
stageName: "internal",
deployment,
});
const internalDomainName = "internal.mytest.domain.com";
const cfnInternalDomain = new apigw.CfnDomainName(this, "internal-domain", {
domainName: internalDomainName,
regionalCertificateArn: myCert.certificateArn,
endpointConfiguration: { types: [apigw.EndpointType.REGIONAL] },
});
const intBasePath = new apigw.CfnBasePathMapping(
this,
"internal-base-path",
{
basePath: "intPath",
domainName: cfnInternalDomain.ref,
restApiId: myRestApi.restApiId,
stage: internalStage.stageName,
}
);

Related

How to add multiple base path mappings from different projects into the same AWS API Gateway?

We have separate AWS CDK projects for different APIs. We want to use same subdomain with different base path mappings for the same API Gateway resource. For example; lets say we have two APIs which are tenantApi and invoiceApi mapping to test.example.com/tenant and test.example.com/invoice. This is doable from one repository with creating one RestApi and defining multiple base path mappings to it. However, I couldn't find a way to achieve this doing it from different repositories since I need to create only one ARecord for the subdomain. My thought was creating ARecord inside a repository where we manage shared resources and importing that record from the repositories we will use the same Api Gateway.
Here is the simple aws cdk code about how I am creating an Api Gateway. As you can see, we have to pass RestApi instance into route53.RecordTarget.fromAlias so I am not really sure if we can create a ARecord before creating an Api Gateway.
export class ApiGatewayStack extends Stack {
constructor(scope: Construct, id: string, props: StackEnvProps) {
super(scope, id, props);
const tenantApi = new apigateway.RestApi(this, 'tenantApi', {
domainName: {
domainName: props.context['domainName'],
certificate: acm.Certificate.fromCertificateArn(this, 'certificateArn', props.context['certificateArn']),
basePath: 'tenant'
},
deploy: true,
deployOptions: {
stageName: 'prod',
},
defaultCorsPreflightOptions: {
allowMethods: apigateway.Cors.ALL_METHODS,
allowOrigins: apigateway.Cors.ALL_ORIGINS,
}
});
const zone = route53.HostedZone.fromLookup(this, 'Zone', { domainName: 'example.com' });
// create an alias for mapping
new route53.ARecord(this, 'domainAliasRecord', {
zone: zone,
recordName: "test",
target: route53.RecordTarget.fromAlias(new ApiGateway(tenantApi)),
});
const methodOptions: apigateway.MethodOptions = {
methodResponses: [
{
statusCode: '200',
responseParameters: {
'method.response.header.Content-Type': true,
},
},
{
statusCode: '400',
responseParameters: {
'method.response.header.Content-Type': true,
},
},
],
};
const postPaymentsLambda = new NodejsFunction(this, 'postTenantLambda', {
entry: './lambda/rest/tenant-api/post-tenant-api.ts',
handler: 'handler',
memorySize: 512,
runtime: lambda.Runtime.NODEJS_14_X,
});
// tenant/v1
const tenantV1 = tenantApi.root.addResource('v1');
tenantV1.addMethod('POST', new apigateway.LambdaIntegration(postPaymentsLambda), methodOptions);
}
}
I appreciate for any help. Thanks!
I had to first create a domainName then create a ARecord with targeting that domainName which can be imported from different APIs I want to attach.
// create domain name for api gateway
const domainName = new apigateway.DomainName(this, 'domainName', {
domainName: `test.${props.context['domainName']}`, // replace with props.type later
certificate: acm.Certificate.fromCertificateArn(this, 'certificateArn', props.context['certificateArn']),
endpointType: apigateway.EndpointType.REGIONAL,
securityPolicy: apigateway.SecurityPolicy.TLS_1_2,
});
const zone = route53.HostedZone.fromLookup(this, 'hostedZone', {
domainName: props.context['domainName']
});
// create an alias for mapping
new route53.ARecord(this, 'apiGatewayDomainAliasRecord', {
zone: zone,
recordName: 'test',
target: route53.RecordTarget.fromAlias(new r53target.ApiGatewayDomain(domainName)),
});
new CfnOutput(this, 'apiGatewayDomainNameAliasTarget', {
value: domainName.domainNameAliasDomainName,
description: 'domainNameAliasTarget attribute used when importing domain name',
exportName: 'apiGatewayDomainNameAliasTarget'
});
Later on, I will import this domainName to create a BasePathMapping. There are three attributes used when importing a domainName;
domainName: the domainName we created before.
domainNameAliasHostedZoneId: Hosted ZoneId where the domain is defined.
domainNameAliasTarget: AWS documentation doesn't clearly state out about what it is. Basically, it is the domainNameAliasDomainName value of the domainName we created at the very first place.
const tenantApi = new apigateway.RestApi(this, 'tenantApi', {
deployOptions: {
stageName: 'dev',
},
deploy: true,
defaultCorsPreflightOptions: {
allowMethods: apigateway.Cors.ALL_METHODS,
allowOrigins: apigateway.Cors.ALL_ORIGINS,
}
});
const domainName = apigateway.DomainName.fromDomainNameAttributes(this, 'domainName', {
domainName: `test.${props.context['domainName']}`,
domainNameAliasHostedZoneId: props.context['hostedZoneId'],
domainNameAliasTarget: Fn.importValue(props.context['domainNameAliasTarget']),
});
const nodeBasePathMapping = new apigateway.BasePathMapping(this, 'nodeBasePathMapping', {
basePath: 'node',
domainName,
restApi: tenantApi,
});

Get dnsName and hostedZoneId from HttpApi in the same stack

Using aws cdk, I am creating an apigatewayv2.HttpApi backed by lambda functions.
I would like to setup a custom domain (apiCustomDomain) name for this created API, however I can't seem to find a way to retrieve the dnsName and hostedZoneId from the created API to create an ARecord.
import * as apigateway2 from '#aws-cdk/aws-apigatewayv2';
const apiCustomDomain = new apigateway2.DomainName(this, 'api-custom-domain', {
domainName: subDomainName,
certificate: certificate
});
const api = new apigateway2.HttpApi(this, 'my-api', {
apiName: 'my-api',
defaultDomainMapping: {
domainName: apiCustomDomain
},
createDefaultStage: true,
disableExecuteApiEndpoint: false,
defaultAuthorizer: new HttpIamAuthorizer()
});
new route53.ARecord(this, 'a-api-record', {
zone: hostedZone,
recordName: subDomainName,
target: route53.RecordTarget.fromAlias({
bind(record: IRecordSet, zone?: IHostedZone): AliasRecordTargetConfig {
return {
dnsName: api.?????, // what to put here
hostedZoneId: api.????? // what to put here
}
}
})
});
Now in the v1 of apigateway, it was straightforward, we would get those values from the domainName property of the api, something like:
dnsName = api.domainName!.domainNameAliasDomainName
hostedZoneId = api.domainName!.domainNameAliasHostedZoneId
But I can't find the same for the apigatewayv2 library.
Pass the existing Hosted Zone retrieved using fromLookup and a ApiGatewayv2DomainProperties target from the Route53 targets module to the ARecord.
const zone = route53.HostedZone.fromLookup(this, 'HostedZone', {
domainName: domain,
});
new route53.ARecord(this, 'AliasRecord', {
recordName: subdomain,
zone,
target: route53.RecordTarget.fromAlias(
new targets.ApiGatewayv2DomainProperties(
apiCustomDomain.regionalDomainName,
apiCustomDomain.regionalHostedZoneId
)
),
});

route53 returns forbidden for custom domain with API Gateway

I'm using AWS CDK to create an APIGateway. I want to attach a custom domain to my api so I can use api.findtechjobs.io. In the console, I can see I have a custom domain attached, however I always get a 403 response when using my custom domain.
Below is the following AWS CDK Stack I am using to create my API Gateway attached with a single lambda function.
AWS CDK deploys well, however, when I attempt to make a POST request to https://api.findtechjobs.io/search AWS returns a 403 Forbidden response. I don't have a VPC, WAF, or an API key for this endpoint.
I am very uncertain why my custom domain is returning a 403 response. I have been reading a lot of documentation, and used answers from other questions and I still can't figure out what I am doing wrong.
How can I associate api.findtechjobs.io to my API Gateway well using AWS CDK?
export class HostingStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) {
super(scope, id, props)
const zonefindtechjobsio = route53.HostedZone.fromLookup(this, 'findtechjobs.io', {
domainName: 'findtechjobs.io'
});
const certificate = new acm.Certificate(this, 'APICertificate', {
domainName: 'findtechjobs.io',
subjectAlternativeNames: ['api.findtechjobs.io'],
validation: acm.CertificateValidation.fromDns(zonefindtechjobsio),
});
const api = this.buildAPI(certificate)
new route53.ARecord( this, "AliasRecord api.findtechjobs.io", {
zone: zonefindtechjobsio,
recordName: `api`,
target: route53.RecordTarget.fromAlias(new route53targets.ApiGateway(api)),
});
}
private buildAPI(certificate: acm.Certificate) {
// API
const api = new apigateway.RestApi(this, "techjobapi", {
domainName: {
domainName: 'findtechjobs.io',
certificate: certificate
},
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS, // TODO limit this when you go to prod
},
deploy: true,
deployOptions: {
stageName: 'dev',
},
endpointTypes: [apigateway.EndpointType.REGIONAL]
});
const searchResource = api.root.addResource("search", {
defaultMethodOptions: {
operationName: "Search",
},
});
searchResource.addMethod(
"POST",
new apigateway.LambdaIntegration(new lambda.Function(this, "SearchLambda", {
runtime: lambda.Runtime.GO_1_X,
handler: "main",
code: lambda.Code.fromAsset(path.resolve("..", "search", "main.zip")),
environment: {
DB_NAME: "...",
DB_CONNECTION:"...",
},
})),
{
operationName: "search",
}
);
return api;
}
}
Same problem. After some struggle. I found out that the problem may lay in the DNS. Cause my domain was transferred from another registrar. The name server is not changed. After I change them to AWS dns it worked. But I can't 100% sure.
And I found out that the default API gateway domain(d-lb4byzxxx.execute-api.ap-east-1.amazonaws.com ) is always in 403 forbidden state.

Request mapping and wildcards in #aws-cdk/aws-apigatewayv2

I'm trying to add a HttpRoute in a HttpApi with the following features:
a wildcard after the initial path: i'd like to proxy everything after "/foo" (such as "/foo", "/foo/bar" etc) to an ecs service (via HttpServiceDiscoveryIntegration, already set)
a request mapping so that the path is correctly handled (I'm using stages for the Api)
Right now I've the following code:
new HttpRoute(scope, 'Route', {
httpApi,
routeKey: HttpRouteKey.with('/foo'),
integration: new HttpServiceDiscoveryIntegration({
service: service.cloudMapService!,
vpcLink: VpcLink.fromVpcLinkAttributes(scope, 'VpcLink', {
vpc,
vpcLinkId: 'aaa',
}),
}),
});
I can't find the right place to put the wildcard (if I put the wildcard inside the HttpRouteKey.with('/foo*') it rises an error)
For what concerns the request mapping, I'd like to obtain what follows:
Thanks!!!
Found out CDK doesn't support what I wanted, so I used the Cfn classes:
const integration = new CfnIntegration(scope, 'Integration', {
apiId: httpApi.apiId,
integrationType: HttpIntegrationType.HTTP_PROXY,
integrationUri: service.cloudMapService!.serviceArn,
integrationMethod: HttpMethod.ANY,
connectionId: vpcLink.vpcLinkId,
connectionType: HttpConnectionType.VPC_LINK,
payloadFormatVersion: PayloadFormatVersion.VERSION_1_0.version,
requestParameters: {
'overwrite:path': '$request.path',
},
});
new CfnRoute(scope, 'BeckyRoute', {
apiId: httpApi.apiId,
routeKey: `ANY /foo`,
target: `integrations/${integration.ref}`,
});
new CfnRoute(scope, 'BeckyProxyRoute', {
apiId: httpApi.apiId,
routeKey: `ANY /foo/{proxy+}`,
target: `integrations/${integration.ref}`,
});

Creating Cognito User Pool With Custom Domain name from AWS CDK

I'm trying to creating Cognito user pool with a custom domain name through AWS CDK. I manage to get everyting working untill to the point where I needed to create an A record in the Rout53 hosted zone. I searched through all the documents but coudn't find a way to do that. Following is my code. Any help would be much appriciated.
const cfnUserPool = new CfnUserPool(this, 'MyCognitoUserPool', {
userPoolName: 'MyCognitoUserPool',
adminCreateUserConfig: {
allowAdminCreateUserOnly: false
},
policies: {
passwordPolicy: {
minimumLength: 8,
requireLowercase: true,
requireNumbers: true,
requireSymbols: true,
requireUppercase: true,
temporaryPasswordValidityDays: 30
}
},
usernameAttributes: [
UserPoolAttribute.EMAIL
],
schema: [
{
attributeDataType: 'String',
name: UserPoolAttribute.EMAIL,
mutable: true,
required: true
},
{
attributeDataType: 'String',
name: UserPoolAttribute.FAMILY_NAME,
mutable: false,
required: true
},
{
attributeDataType: 'String',
name: UserPoolAttribute.GIVEN_NAME,
mutable: false,
required: true
}
]
});
const cognitoAppDomain = new CfnUserPoolDomain(this, "PigletAuthDomainName", {
domain: authDomainName,
userPoolId: cfnUserPool.ref,
customDomainConfig: {
certificateArn: 'ACM Certificate arn'
}
});
/*
TODO: Create an A record from the created cnfUserPoolDomain
*/
Everything works up untill to this point. Now the question is how to create an A record using the CfnUserPoolDomain
Any help is much appriciated.
Update May 2020
The UserPoolDomain construct has been extended and a UserPoolDomainTarget was added to provide this functionality.
Now, all you need to do is the following:
const userPoolDomain = new cognito.UserPoolDomain(this, 'UserPoolDomain', {
userPool,
customDomain: {
domainName: authDomainName,
certificate,
},
});
new route53.ARecord(this, 'UserPoolCloudFrontAliasRecord', {
zone: hostedZone,
recordName: authDomainName,
target: route53.RecordTarget.fromAlias(new route53_targets.UserPoolDomainTarget(userPoolDomain)),
});
I had the same Problem, It looks like CloudFormation does not have a return parameter for the CfnUserPoolDomain AliasTarget. Which means the cdk can not provide this parameter either.
I ended up implementing it using the AWS SDK (npm install aws-sdk) and getting the value using the APIs:
Update: The better solution is to use the AwsCustomResource. You can see a detailed example in aws/aws-cdk (#6787):
const userPoolDomainDescription = new customResources.AwsCustomResource(this, 'user-pool-domain-description', {
onCreate: {
physicalResourceId: 'user-pool-domain-description',
service: 'CognitoIdentityServiceProvider',
action: 'describeUserPoolDomain',
parameters: {
Domain: userPoolDomain.domain
}
}
});
const dnsName = userPoolDomainDescription.getData('DomainDescription.CloudFrontDistribution').toString();
// Route53 alias record for the UserPoolDomain CloudFront distribution
new route53.ARecord(this, 'UserPoolDomainAliasRecord', {
recordName: userPoolDomain.domain,
target: route53.RecordTarget.fromAlias({
bind: _record => ({
hostedZoneId: 'Z2FDTNDATAQYW2', // CloudFront Zone ID
dnsName: dnsName,
}),
}),
zone,
})
Here's how to get around it. Assuming you have a stack.yaml that you deploy with a CI tool, say through bash:
THE_STACK_NAME="my-cognito-stack"
THE_DOMAIN_NAME="auth.yourveryowndomain.org"
# get the alias target
# notice that it will be empty upon first launch (chicken and the egg problem)
ALIAS_TARGET=$(aws cognito-idp describe-user-pool-domain --domain ${THE_DOMAIN_NAME} | grep CloudFrontDistribution | cut -d \" -f4)
# create/update the deployment CloudFormation stack
# notice the AliasTarget parameter (which can be empty, it's okay!)
aws cloudformation deploy --stack-name ${THE_STACK_NAME} --template-file stack.yaml --parameter-overrides AliasTarget=${ALIAS_TARGET} DomainName=${THE_DOMAIN_NAME}
The stack.yaml minimal version (remember to fill the UserPool config):
---
AWSTemplateFormatVersion: 2010-09-09
Parameters:
DomainName:
Type: String
Default: auth.yourveryowndomain.org
Description: The domain name to use to serve this project.
ZoneName:
Type: String
Default: yourveryowndomain.org
Description: The hosted zone name coming along with the DomainName used.
AliasTarget: # no default value, can be empty
Type: String
Description: The UserPoolDomain alias target.
Conditions: # here's "the trick"
HasAliasTarget: !Not [!Equals ['', !Ref AliasTarget]]
Resources:
Certificate:
Type: "AWS::CertificateManager::Certificate"
Properties:
DomainName: !Ref ZoneName
DomainValidationOptions:
- DomainName: !Ref ZoneName
ValidationDomain: !Ref ZoneName
SubjectAlternativeNames:
- !Ref DomainName
UserPool:
Type: AWS::Cognito::UserPool
Properties:
[... fill that with your configuration! ...]
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
UserPoolId: !Ref UserPool
Domain: !Ref DomainName
CustomDomainConfig:
CertificateArn: !Ref Certificate
DnsRecord: # if AliasTarget parameter is empty, well we just can't do that one!
Condition: HasAliasTarget # and here's how we don't do it when we can't
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Sub "${ZoneName}."
AliasTarget:
DNSName: !Ref AliasTarget
EvaluateTargetHealth: false
# HostedZoneId value for CloudFront is always this one
# see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html
HostedZoneId: Z2FDTNDATAQYW2
Name: !Ref DomainName
Type: A
Be aware CloudFormation conditions are not "a trick" at all: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html. We simply use it as a trick along with the "first launch won't do it all" to get around our scenario.
Kinda weird, but only for the first run! Launch it again: everything is fine.
PS: can't wait to avoid all that by simply having the CloudFrontDistribution alias target directly in the AWS::Cognito::UserPoolDomain return values!!