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
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,
});
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
)
),
});
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.
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}`,
});
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!!