I have an S3 bucket named foo.example.com which is configured for static website hosting, with the endpoint http://foo.example.com.s3-website-us-east-1.amazonaws.com. I'm trying to create an A record in a hosted zone of the same name to point to this S3 bucket.
package main
import (
"log"
"strconv"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
)
func main () {
sess, _ := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
})
// Create a Route53 service client
route53Svc := route53.New(sess)
// Set the name of the hosted zone
hostedZoneName := "foo.example.com"
// Set the maximum number of hosted zones to return
maxItems := int64(10)
// Call the ListHostedZonesByName action
result, err := route53Svc.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{
DNSName: aws.String(hostedZoneName + "."),
MaxItems: aws.String(strconv.FormatInt(maxItems, 10)),
})
if err != nil {
log.Fatalf("Error listing hosted zones: %s", err)
}
// Iterate through the list of hosted zones
for _, hz := range result.HostedZones {
// Check if the hosted zone name matches the specified name
if *hz.Name == hostedZoneName + "." {
// Get the hosted zone ID
hostedZoneID := aws.StringValue(hz.Id)
val := "http://s3-website-us-east-1.amazonaws.com"
// Create an A record for the bucket in the desired hosted zone
_, err = route53Svc.ChangeResourceRecordSets(&route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(hostedZoneID),
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{
{
Action: aws.String("CREATE"),
ResourceRecordSet: &route53.ResourceRecordSet{
Type: aws.String("A"),
Name: aws.String(hostedZoneName),
ResourceRecords: []*route53.ResourceRecord{
{
Value: aws.String(val),
},
},
TTL: aws.Int64(300),
},
},
},
},
})
if err != nil {
log.Fatalf("%s",err)
}
break
}
}
}
However I get the following error:
> InvalidChangeBatch: [Invalid Resource Record: 'FATAL problem:
> ARRDATAIllegalIPv4Address (Value is not a valid IPv4 address)
> encountered with 'http://s3-website-us-east-1.amazonaws.com'']
How do I properly reference the S3 endpoint in the hosted zone?
You want to create an Alias Record, which allows you to target a DNS name. Plain-old A records only accept IPv4 addresses.
Action: aws.String("CREATE"),
ResourceRecordSet: &route53.ResourceRecordSet{
AliasTarget: &route53.AliasTarget{
DNSName: aws.String("foo.example.com.s3-website-us-east-1.amazonaws.com"),
EvaluateTargetHealth: aws.Bool(true),
HostedZoneId: aws.String("Z3AADJGX6KTTL2"),
},
Name: aws.String("foo.example.com"),
Type: aws.String("A"),
...
See the examples in the V1 Go SDK repo.
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.
For the purposes of this question, assume that I already have an example.org Hosted Zone in Route53 (my actual zone is, of course, different)
With the following CDK app:
export class MyExampleStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const backend = new Function(this, 'backendLambda', {
code: new AssetCode("lambda/"),
handler: "index.handler",
runtime: Runtime.PYTHON_3_8
});
apiDomainName = 'api.test.example.org'
const api = new LambdaRestApi(this, 'api', {
handler: backend,
proxy: true,
deploy: true,
domainName: {
domainName: apiDomainName,
certificate: new Certificate(this, 'apiCertificate', {
domainName: apiDomainName
})
}
});
}
}
, when I run cdk deploy, part of the output reads:
Outputs:
MyExampleStack.apiEndpoint0F54D2EA = https://<alphanumericId>.execute-api.us-east-1.amazonaws.com/prod/
, and, indeed, when I curl that url, I see the response I would expect from my Lambda code. I would expect curling api.test.example.org to give the same result - however, instead it gives curl: (6) Could not resolve host: api.test.example.org.
Based on this documentation, I tried:
export class MyExampleStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const rootDomain = 'example.org'
const zone = HostedZone.fromLookup(this, 'baseZone', {
domainName: rootDomain
});
const backend = new Function(...);
const api = new LambdaRestApi(...);
new ARecord(this, 'apiDNS', {
zone: zone,
recordName: 'api.test',
target: RecordTarget.fromAlias(new ApiGateway(api))
});
}
}
which did give a Route53 entry:
$ aws route53 list-hosted-zones
{
"HostedZones": [
{
"Id": "/hostedzone/ZO3B2N6W70PDD",
"Name": "example.org.",
"CallerReference": "598D71AB-4A98-EC5A-A170-D51CB243A2EA",
"Config": {
"PrivateZone": false
},
"ResourceRecordSetCount": 8
}
]
}
$ aws route53 list-resource-record-sets --hosted-zone-id /hostedzone/ZO3B2N6W70PDD --query 'ResourceRecordSets[?Name==`api.test.example.org.`]'
[
{
"Name": "api.test.example.org.",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z1UJRXOUMOOFQ9",
"DNSName": "<alphanumericId2>.execute-api.us-east-1.amazonaws.com.",
"EvaluateTargetHealth": false
}
}
]
But this still doesn't work:
curl api.test.example.org still gives "Could not resolve host"
curl <alphanumericId2>.execute-api.us-east-1.amazonaws.com gives curl: (7) Failed to connect to <alphanumericId2>.execute-api.us-east-1.amazonaws.com port 80: Connection refused
curl https://<alphanumericId2>..execute-api.us-east-1.amazonaws.com gives {"message":"Forbidden"}
curl https://<alphanumericId>.[...] (i.e. the output from cdk deploy) still gives the expected response from the Lambda
How can I define a custom name in Route53 to route to my Lambda-backed APIGateway API?
Overall code LambdaRestApi with Route53 A Record, will create
Custom domain pointing to a particular stage prod i.e api.test.example.org domain to stage `prod'(example)
Route 53 A record for api.test.example.org pointing to Api Gateway hosted zone.
These are two combinations that will work
https://api.test.example.org will work pointing directly to stage prod.
CDK Output https://abcdef1234.execute-api.us-east-1.amazonaws.com/prod/ will work as stage is appended to it.
These are few combinations that will not work
Two other tests you did with http://
With no protocol, defaults to http, will not work, as we api gateway by default gives TLS 1.0 (ssl-https) and no http listener.
One other attempt you did with https:// without a stage name at the end, will return 403 forbidden, as stage name is missing.
Here is full CDK code.
import * as cdk from "#aws-cdk/core";
import * as apigw from "#aws-cdk/aws-apigateway";
import * as acm from "#aws-cdk/aws-certificatemanager";
import * as route53 from "#aws-cdk/aws-route53";
import * as route53Targets from "#aws-cdk/aws-route53-targets";
import * as lambda from "#aws-cdk/aws-lambda";
export class HelloCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.buildLambdaApiGateway();
}
buildLambdaApiGateway() {
const rootDomain = "example.org";
const zone = route53.HostedZone.fromLookup(this, "baseZone", {
domainName: rootDomain,
});
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: `api-test.${rootDomain}`,
certificate: acm.Certificate.fromCertificateArn(
this,
"my-cert",
"arn:aws:acm:us-east-1:111112222333:certificate/abcd6805-1234-4159-ac38-761acdc700ef"
),
endpointType: apigw.EndpointType.REGIONAL,
},
});
new route53.ARecord(this, "apiDNS", {
zone: zone,
recordName: "api-test",
target: route53.RecordTarget.fromAlias(
new route53Targets.ApiGateway(restApi)
),
});
}
}
To create an Elastic Beanstalk Application and Environment I have the following code:
// this: the class instance extending Construct
const application = new CfnApplication(this, 'Application', {
applicationName: 'some-name'
});
const environment = new CfnEnvironment(this, 'Environment', {
environmentName: 'production',
applicationName: application.applicationName,
platformArn: 'arn::of::plaform',
solutionStackName: 'a-valid-stack-name'
});
Creating an alias record in Route53 requires a target implementing IAliasRecordTarget
const record = new AliasRecord(this, 'ARecord', {
recordName: 'a-record',
target: ?
zone: zone
});
How can I use the environment as target? Looking for classes implementing IAliasRecordTarget in the aws-cdk repo does not yield many candidates beside cloudfront distribution and base load balancer
For those who are looking for a solution in case of a single instance environment:
Set the cnamePrefix in your EBS Environment to a value you like (eg. 'my-app'). This results in a url you can use later as part of the dnsName to create an A-record;
Create an AliasRecordTarget:
const record: IAliasRecordTarget = {
bind: (): AliasRecordTargetConfig => ({
dnsName: `${cnamePrefix}.${this.region}.elasticbeanstalk.com`,
hostedZoneId: 'Z2NYPWQ7DFZAZH' // Lookup ID or create a mapper: https://www.rubydoc.info/gems/roadworker/Aws/Route53
})
};
Create the A-record:
// Route53 alias record for the EBS app
new ARecord(this, 'ebs-alias-record', {
recordName: `my-app.mydomain.com.`,
target: RecordTarget.fromAlias(record),
zone: hostedZone
})
** Edit **
To get the value of the hostedZone variable you can lookup your zone by using:
HostedZone.fromLookup(this, 'zone-lookup', {domainName: 'my-app.mydomain.com'});
The target prop expects an object with a bind() function returning dnsName, evaluateTargetHealth and hostedZoneId (see AWS::Route53::RecordSet AliasTarget and the implementation of AliasRecord).
You can do the following:
const record = new AliasRecord(this, 'ARecord', {
recordName: 'a-record',
target: {
bind: (): AliasRecordTargetProps => ({
dnsName: environment.attrEndpointUrl,
hostedZoneId: 'Z14LCN19Q5QHIC' // for us-east-2
})
},
zone: zone
});
See AWS Elastic Beanstalk endpoints and quotas for a list of hosted zone IDs if using another region or Elastic Load Balancing endpoints and quotas when the environment is load-balanced.
UPDATE 2018-05-28: asAliasRecordTarget is now bind in aws-cdk version 0.32.0
Here's a workaround that allows you to forward requests to the EBS environment URL (not the load balancer).
import { ARecord, RecordTarget, HostedZone, AliasRecordTargetConfig } from '#aws-cdk/aws-route53';
// Environment URL for my EBS app.
const EBS_ENV_URL = 'mysampleenvironment.eba-8mmp67ym.us-east-1.elasticbeanstalk.com';
// Id of the hosted zone in the region your EBS environment is located.
const EBS_ENV_HOSTED_ZONE_ID = 'Z117KPS5GTRQ2G';
const aliasRecord = new ARecord(stack, 'AliasRecord', {
recordName: DOMAIN,
target: RecordTarget.fromAlias({
bind: (): AliasRecordTargetConfig => ({
dnsName: EBS_ENV_URL,
hostedZoneId: EBS_ENV_HOSTED_ZONE_ID
})
}),
zone: HostedZone.fromLookup(stack, 'WebsiteHostedZone', {
domainName: DOMAIN
})
});
This workaround is basically implementing a custom IAliasRecordTarget.
Hardcode the EBS app’s environment URL into the dnsName prop.
Visit https://docs.aws.amazon.com/general/latest/gr/elasticbeanstalk.html and find the hosted zone id that matches the region of the EBS app’s environment. This value will need to be hardcoded in the hostedZoneId prop.
In addition to the solution and comment posted by #jogold,
using the HostedZoneProvider, to retreive your own hosted zone and
using the zone id of the Elastic Beanstalk Hosted zone as target
const zone = new HostedZoneProvider(this, {
domainName: props.domainName
}).findAndImport(this, 'a-hosted-zone');
const ebsRegionHostedZoneId = 'Z117KPS5GTRQ2G' // us-east-1
const record = new AliasRecord(this, 'ARecord', {
recordName: 'a-record',
target: {
asAliasRecordTarget: (): AliasRecordTargetProps => ({
dnsName: environment.environmentEndpointUrl,
// the id of the hosted zone in your region
hostedZoneId: ebsRegionHostedZoneId
})
},
// your hosted zone
zone: zone
});