Problem
I need create a public API in AWS which will be integrated with multiple services: including multiple fargate services, EC2 services, and Lambda, etc. You can think it as twitter or facebook public API. Something like this: https://aws.amazon.com/blogs/compute/architecting-multiple-microservices-behind-a-single-domain-with-amazon-api-gateway/
Now I'm working on integrating one of its endpoint to an "ApplicationLoadBalancedFargateService".
I only see HTTPIntegration and LambdaIntegration there, not "AlbIntegration".
Therefore, I picked HTTPIntegration which requires VPCLLink.
Then I find that the "targets" attribute in ApiGateway.VpcLink only support INetworkLoadBalancer type. But my fargate service is created with EcsPatterns.ApplicationLoadBalancedFargateService.
So the error I see at line targets: [PandaServiceLoadBalancer] is :
Type 'IApplicationLoadBalancer' is not assignable to type 'INetworkLoadBalancer'.
Types of property 'addListener' are incompatible.
What change should I make here to make it work?
Should I change my fargate service to EcsPatterns.NetworkLoadBalancedFargateService to make it easier?
Details
I have a fargate service defined as this:
this.fargateService =
new EcsPatterns.ApplicationLoadBalancedFargateService(
this,
`${props.stageName}-${this.serviceName}-ID`,
{
cluster: PandaServiceFargetCluster, // Required
//loadBalancerName: "PandaServiceLoadBalancer",
serviceName: "PandaService",
desiredCount: 1, // Increase it for better scalability.
taskDefinition: this.createFargateTask(props),
publicLoadBalancer: true, // Default is false
healthCheckGracePeriod: CDK.Duration.minutes(1),
circuitBreaker: {
rollback: true,
},
}
);
Now in my public API gateway, I want to integrate it with the fargate service.
import * as CDK from "aws-cdk-lib";
import * as CertificateManager from "aws-cdk-lib/aws-certificatemanager";
import * as Route53 from "aws-cdk-lib/aws-route53";
import * as ApiGateway from "aws-cdk-lib/aws-apigateway";
import * as ELBv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Construct } from "constructs";
import { StageInfo } from "../config/stage-config";
import * as EC2 from "aws-cdk-lib/aws-ec2";
...
...
...
this.vpcLink = new ApiGateway.VpcLink(this, `${this.constructIdPrefix}VpcLinkCreation`, {
vpcLinkName: "PandaServiceVpcLink"
targets: [PandaServiceLoadBalancer]
});
this.pandaApi = new ApiGateway.RestApi(
this,
`${this.constructIdPrefix}-pandaApi`,
{
description: "The centralized API for panda.com",
domainName: {
domainName: props.stageInfo.domainName,
certificate: domainCertificate,
//mappingKey: props.pipelineStageInfo.stageName
},
defaultCorsPreflightOptions: {
allowOrigins: ApiGateway.Cors.ALL_ORIGINS,
allowMethods: [...ApiGateway.Cors.DEFAULT_HEADERS],
},
}
);
const productsResource = this.pandaApi.root.addResource("products");
productsResource.addMethod(
"GET",
new ApiGateway.HttpIntegration(
`${props.stageInfo.PandaServiceLoadBalancerDns}/products`,
{
httpMethod: "GET",
options: {
connectionType: ApiGateway.ConnectionType.VPC_LINK,
vpcLink: this.vpcLink,
},
}
)
);
Related
I'm trying to deploy a fairly basic Nodejs CRUD API to AWS using AWS-CDK. The service runs in a docker container and I'm deploying it to an ECS Fargate cluster behind an ALB. I also have a domain in Route53 that I'm trying to use.
The problem I'm having is I can't seem to access the ALB through the domain. I can access the ALB directly using its default AWS DNS (XXXXX.us-west-2.elb.amazonaws.com/) over HTTP, but I get 504 timeouts when I attempt to access it through the domain.
I'm pretty new to AWS and CDK, so I'm sure I'm missing something obvious here. Any advice or recommended resources/examples would be much appreciated. Here's my CDK code:
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as Cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as CloudfrontOrigins from "aws-cdk-lib/aws-cloudfront-origins";
import * as Route53 from "aws-cdk-lib/aws-route53";
import * as Route53Targets from "aws-cdk-lib/aws-route53-targets";
import * as ACM from "aws-cdk-lib/aws-certificatemanager";
import * as EC2 from "aws-cdk-lib/aws-ec2";
import * as ECS from "aws-cdk-lib/aws-ecs";
import * as EcsPatterns from "aws-cdk-lib/aws-ecs-patterns";
interface Props extends StackProps {
domainName: string;
dockerDir: string;
}
export class AppStack extends Stack {
constructor(scope: Construct, id: string, { domainName, dockerDir, ...rest }: Props) {
super(scope, id, rest);
const hostedZone = Route53.HostedZone.fromLookup(this, `${id}_Zone`, {
domainName,
});
const vpc = new EC2.Vpc(this, `${id}_Vpc`, { maxAzs: 2 });
const cluster = new ECS.Cluster(this, `${id}_Ec2Cluster`, { vpc });
cluster.addCapacity(`${id}_DefaultAutoScalingGroup`, {
instanceType: EC2.InstanceType.of(
EC2.InstanceClass.T3,
EC2.InstanceSize.MICRO
),
minCapacity: 1,
maxCapacity: 3,
});
const certificate = new ACM.DnsValidatedCertificate(
this,
`${id}_SiteCertificate`,
{
domainName,
hostedZone,
region: "us-east-1",
}
);
const fargateService = new EcsPatterns.ApplicationLoadBalancedFargateService(
this,
`${id}_FargateLoadBalancedService`,
{
cluster,
desiredCount: 1,
publicLoadBalancer: true,
taskImageOptions: {
image: ECS.ContainerImage.fromAsset(dockerDir),
containerPort: 8000,
environment: {
PORT: '8000',
},
},
}
);
const distribution = new Cloudfront.Distribution(
this,
`${id}_SiteDistribution`,
{
certificate,
domainNames: [domainName],
minimumProtocolVersion: Cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
defaultBehavior: {
origin: new CloudfrontOrigins.HttpOrigin(
fargateService.loadBalancer.loadBalancerDnsName
),
compress: false,
cachePolicy: Cloudfront.CachePolicy.CACHING_DISABLED,
allowedMethods: Cloudfront.AllowedMethods.ALLOW_ALL,
},
}
);
new Route53.ARecord(this, `${id}_SiteAliasRecord`, {
recordName: domainName,
target: Route53.RecordTarget.fromAlias(
new Route53Targets.CloudFrontTarget(distribution)
),
zone: hostedZone,
});
}
}
And this class gets created in my bin/infra.ts file:
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import * as path from "path";
import { AppStack } from "../lib/AppStack";
const appId = `MyApp`;
const app = new cdk.App();
new AppStack(app, `${appId}Stack`, {
dockerDir: path.resolve(__dirname, "..", "api"), // contains the Dockerfile
domainName: 'mydomain.com',
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
And here's the Dockerfile in case it's useful.
FROM node:16-alpine as builder
ENV NODE_ENV build
USER node
WORKDIR /home/node
COPY package*.json ./
RUN npm i
COPY --chown=node:node . .
RUN npm run build \
&& npm prune --production
# ---
FROM node:16-alpine
ENV PORT 8000
ENV NODE_ENV production
# Add curl for healthcheck
RUN apk --no-cache add curl
USER node
WORKDIR /home/node
COPY --from=builder --chown=node:node /home/node/package*.json ./
COPY --from=builder --chown=node:node /home/node/node_modules/ ./node_modules/
COPY --from=builder --chown=node:node /home/node/dist/ ./dist/
EXPOSE 8000
CMD ["node", "dist/main.js"]
HEALTHCHECK CMD curl -f http://localhost:8000/api/healthcheck || exit 1
Why am I getting 504 errors when I access my service through my domain? Or where can I look to get a better idea of what I'm missing?
CloudFront talks HTTPS (port 443) to its origins by default. An ALB (regardless of whether created explicitly or implicitly by the ApplicationLoadBalancedFargateService construct) listens on HTTP (port 80) by default unless explicitly set up for HTTPS.
As your ALB is not configured to listen on HTTPS, CloudFront attempts to talk HTTPS to an ALB that only listens on HTTP.
To fix that, set origin.protocolPolicy to OriginProtocolPolicy.HTTP_ONLY, which instructs CloudFront to talk HTTP to your ALB. Please note that the below code uses CDK v2 and also, I've used LoadBalancerV2Origin over HttpOrigin, although both should work the same.
const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', {
vpc,
internetFacing: true,
});
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: {
new origins.LoadBalancerV2Origin(loadBalancer, {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
})
},
});
I have a CDK project that creates a CodePipeline which deploys an application on ECS. I had it all previously working, but the VPC was using a NAT gateway, which ended up being too expensive. So now I am trying to recreate the project without requiring a NAT gateway. I am almost there, but I have now run into issues when the ECS service is trying to start tasks. All tasks fail to start with the following error:
ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve secret from asm: service call has been retried 5 time(s): failed to fetch secret
At this point I've kind of lost track of the different things I have tried, but I will post the relevant bits here as well as some of my attempts.
const repository = ECR.Repository.fromRepositoryAttributes(
this,
"ecr-repository",
{
repositoryArn: props.repository.arn,
repositoryName: props.repository.name,
}
);
// vpc
const vpc = new EC2.Vpc(this, this.resourceName(props, "vpc"), {
maxAzs: 2,
natGateways: 0,
enableDnsSupport: true,
});
const vpcSecurityGroup = new SecurityGroup(this, "vpc-security-group", {
vpc: vpc,
allowAllOutbound: true,
});
// tried this to allow the task to access secrets manager
const vpcEndpoint = new EC2.InterfaceVpcEndpoint(this, "secrets-manager-task-vpc-endpoint", {
vpc: vpc,
service: EC2.InterfaceVpcEndpointAwsService.SSM,
});
const secrets = SecretsManager.Secret.fromSecretCompleteArn(
this,
"secrets",
props.secrets.arn
);
const cluster = new ECS.Cluster(this, this.resourceName(props, "cluster"), {
vpc: vpc,
clusterName: `api-cluster`,
});
const ecsService = new EcsPatterns.ApplicationLoadBalancedFargateService(
this,
"ecs-service",
{
taskSubnets: {
subnetType: SubnetType.PUBLIC,
},
securityGroups: [vpcSecurityGroup],
serviceName: "api-service",
cluster: cluster,
cpu: 256,
desiredCount: props.scaling.desiredCount,
taskImageOptions: {
image: ECS.ContainerImage.fromEcrRepository(
repository,
this.ecrTagNameParameter.stringValue
),
secrets: getApplicationSecrets(secrets), // returns
logDriver: LogDriver.awsLogs({
streamPrefix: "api",
logGroup: new LogGroup(this, "ecs-task-log-group", {
logGroupName: `${props.environment}-api`,
}),
logRetention: RetentionDays.TWO_MONTHS,
}),
},
memoryLimitMiB: 512,
publicLoadBalancer: true,
domainZone: this.hostedZone,
certificate: this.certificate,
redirectHTTP: true,
}
);
const scalableTarget = ecsService.service.autoScaleTaskCount({
minCapacity: props.scaling.desiredCount,
maxCapacity: props.scaling.maxCount,
});
scalableTarget.scaleOnCpuUtilization("cpu-scaling", {
targetUtilizationPercent: props.scaling.cpuPercentage,
});
scalableTarget.scaleOnMemoryUtilization("memory-scaling", {
targetUtilizationPercent: props.scaling.memoryPercentage,
});
secrets.grantRead(ecsService.taskDefinition.taskRole);
repository.grantPull(ecsService.taskDefinition.taskRole);
I read somewhere that it probably has something to do with Fargate version 1.4.0 vs 1.3.0, but I'm not sure what I need to change to allow the tasks to access what they need to run.
You need to create an interface endpoints for Secrets Manager, ECR (two types of endpoints), CloudWatch, as well as a gateway endpoint for S3.
Refer to the documentation on the topic.
Here's an example in Python, it'd work the same in TS:
vpc.add_interface_endpoint(
"secretsmanager_endpoint",
service=ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
)
vpc.add_interface_endpoint(
"ecr_docker_endpoint",
service=ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
)
vpc.add_interface_endpoint(
"ecr_endpoint",
service=ec2.InterfaceVpcEndpointAwsService.ECR,
)
vpc.add_interface_endpoint(
"cloudwatch_logs_endpoint",
service=ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
)
vpc.add_gateway_endpoint(
"s3_endpoint",
service=ec2.GatewayVpcEndpointAwsService.S3
)
Keep in mind that interface endpoints cost money as well, and may not be cheaper than a NAT.
I have a flask app which I'm trying to deploy into a landing zone, using CDK (Typescript, v2.5.0) as a Fargate instance.
The landing zone an existing VPC which I need to use, with Isolated and Private subnets.
I've tried every combination I can think of to get the load balancer (tried both application- and network-balanced) to use the Isolated subnets, but nothing has worked.
The error I get from cdk synth is
deploy/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.ts:401
throw new Error(`There are no '${subnetType}' subnet groups in this VPC. Available types: ${availableTypes}`);
^
*Error: There are no 'Public' subnet groups in this VPC. Available types: Isolated*
Here's my code:
import * as cdk from "#aws-cdk/core";
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecsp from "aws-cdk-lib/aws-ecs-patterns";
export class DeployStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const defaultnonprodVPC = "existing-vpc";
const defaultVPC = ec2.Vpc.fromLookup(this,
"defaultVPC",
{
isDefault: false,
vpcId: defaultnonprodVPC,
tags: { "aws-cdk:subnet-type": "isolated" }
}
);
const knownIsolatedSubnets = defaultVPC.isolatedSubnets;
const monitoringSubnets = defaultVPC.selectSubnets(
{
subnetType: ec2.SubnetType.PRIVATE_ISOLATED
}
);
const networkBalancedFargateService = new ecsp.NetworkLoadBalancedFargateService(this,
"ConnectorMonitorService", {
memoryLimitMiB: 512,
desiredCount: 1,
cpu: 256,
taskImageOptions: {
image: ecs.ContainerImage.fromAsset("../src")
},
taskSubnets:
{
subnetType: ec2.SubnetType.PRIVATE_ISOLATED
},
vpc: defaultVPC
});
}
}
Changing the taskSubnets to any of
{ subnets: { knownIsolatedSubnets } }
or
subnetGroupName: "subnet-existing-subnet-name"
or
monitoringSubnets
makes no difference to cdk synth. Setting assignPublicIp: false doesn't change things either.
What am I doing wrong, or missing?
NetworkLoadBalancedFargateService has a property publicLoadBalancer which is true by default. This makes the Load Balancer internet-facing, which is not correct in your case. You need to set it to false so that private or isolated subnets work.
Documentation for NetworkLoadBalancedFargateService
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 have defined a CDK app stack using TypeScript (sensitive information rendomized in the code below):
import * as cdk from "#aws-cdk/core";
import * as ec2 from "#aws-cdk/aws-ec2";
import * as ecs from "#aws-cdk/aws-ecs";
import * as ecr from "#aws-cdk/aws-ecr";
import * as ecr_assets from "#aws-cdk/aws-ecr-assets";
import * as ecs_patterns from "#aws-cdk/aws-ecs-patterns";
import * as sm from "#aws-cdk/aws-secretsmanager";
export class CdkAppStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create a Docker image and upload it to the Amazon Elastic Container Registry (ECR)
const dockerImage = new ecr_assets.DockerImageAsset(this, "ApiDockerImage", {
directory: "/home/ec2-user/environment/node-test"
});
// Create a new VPC and NAT Gateway
const vpc = new ec2.Vpc(this, "ApiVpc", {
maxAzs: 3 // Default is all AZs in region
});
// Create a new Amazon Elastic Container Service (ECS) cluster
const cluster = new ecs.Cluster(this, "ApiCluster", {
vpc: vpc
});
// Create a load-balanced Fargate service and make it public
new ecs_patterns.ApplicationLoadBalancedFargateService(this, "ApiFargateService", {
cluster: cluster, // Required
cpu: 512, // Default is 256
desiredCount: 2, // Default is 1
taskImageOptions: {
image: ecs.ContainerImage.fromDockerImageAsset(dockerImage),
containerPort: 8080,
enableLogging: true,
secrets: sm.Secret.fromSecretCompleteArn(this, "ImportedSecret", "arn:aws:secretsmanager:ap-south-1:762589711820:secret:/api/production/FrOibp")
},
memoryLimitMiB: 2048, // Default is 512
publicLoadBalancer: true // Default is false
});
}
}
Deployment with cdk deploy is successful if I remove the secrets key from taskImageOptions but with the secrets there, I get this error when trying to deploy:
ec2-user:~/environment/cdk-app (master) $ cdk deploy
тип Unable to compile TypeScript:
lib/cdk-app-stack.ts:42:9 - error TS2322: Type 'ISecret' is not assignable to type '{ [key: string]: Secret; }'.
Index signature is missing in type 'ISecret'.
42 secrets: secret
~~~~~~~
Subprocess exited with error 1
I'm doing something wrong here in trying to use secrets from Secrets Manager. What is the right way to reference secrets in a ApplicationLoadBalancedFargateService?
There are two issues here:
secrets is of type index signature. you should therefore name your secret (this is the environment variable that will be exposed in your container)
an ecs.Secret is expected (you can create it from an sm.Secret)
here is a working version:
new ecs_patterns.ApplicationLoadBalancedFargateService(this, "ApiFargateService", {
cluster: cluster, // Required
cpu: 512, // Default is 256
desiredCount: 2, // Default is 1
taskImageOptions: {
image: ecs.ContainerImage.fromDockerImageAsset(dockerImage),
containerPort: 8080,
enableLogging: true,
secrets: {
"MY_SECRET": ecs.Secret.fromSecretsManager( sm.Secret.fromSecretCompleteArn(this, "ImportedSecret", "arn:aws:secretsmanager:ap-south-1:762589711820:secret:/api/production/FrOibp"))
}
},
memoryLimitMiB: 2048, // Default is 512
publicLoadBalancer: true // Default is false
});