I'm trying to create a cdk stack containing an ApplicationLoadBalancedFargateService (docs). I want it placed in my VPC which exclusively contains private subnets.
When I try to deploy my stack I get an error message saying:
Error: There are no 'Public' subnet groups in this VPC. Available types: Isolated
Which well... in theory is correct, but why does it break my deployment?
Here an extract of my code my code:
// Get main VPC and subnet to use
const mainVpc = ec2.Vpc.fromLookup(this, 'MainVpc', {
vpcName: this.VPC_NAME
});
// Fargate configuration
const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this,
'CdkDocsFargateService', {
serviceName: 'docs-fargate-service',
memoryLimitMiB: 512,
desiredCount: 1,
cpu: 256,
vpc: mainVpc,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry(this.IMAGE_NAME),
containerPort: 80,
},
});
I was able to achieve the desired outcome manually from the management console. What am I doing wrong when using CDK?
I solved the problem by setting publicLoadBalancer: false in the properties of the ApplicationLoadBalancedFargateService.
Related
My goal is to have an AWS code commit repo, that on push to main branch will run a code pipeline CI/CD process to deploy a single node app to AWS.
I went through the setup to get this working with fargate via CDK using ApplicationLoadBalancedFargateService, but ultimately ran into issues because the ALB requires two availability zones, and I don't want to run two instance of my app (I'm not concerned with high availability, and in this case it's a chat bot that I don't want "logged on" twice).
Does anyone have any recommendations here? Perhaps EBS is the service I want? (I've gone down that path pre-container, but maybe I should revisit?)
I also read about the code deploy agent and EC2, but that seems more of a manual process, where I'm hoping to be able to automated the creation of all resources with CDK.
resolution: I believe this is a case of me not understanding fargate well enough, shoutout #Victor Smirnov for helping break down everything for me.
There is in fact only a single task registered when my CDK stack builds.
I think the issue I ran into was I'd use the CDK codepipeline ECS deploy action, which would start deploying a second task before deregistering the first (which I think is just a fargate "feature" to avoid downtime, ie blue/green deploy). I mistakenly expected only a single container to be running at a given time, but that's just not how Services work.
I think Victor had a good point about the health checks as well. It took me a few tries to get all the ports lined up, and when they were misaligned and health checks were failing I'd see the "old failed task" getting "deregistered" alongside the "new task that hadn't failed yet" which made me think I had two concurrent tasks running.
Below is an example of the ApplicationLoadBalancedFargateService pattern used to create the Fargate service with one running task. I deployed the stack when I wrote the answer to the question.
The Application load balancer has three availability zones because my VPC has three public subnets. It means that the loader balancer itself has the IP addresses in three different zones.
The load balancer has only one target. There is no requirement that the load balancer should have a target in each zone.
I put everything in the public VPC zone because I do not have NAT. You might want to have your Fargate tasks in the private zone for better security.
I added the health check path with default values because, most likely, you will want to define a custom URL for your service. We can omit the default definition.
import { App, RemovalPolicy, Stack } from 'aws-cdk-lib'
import { Certificate, CertificateValidation } from 'aws-cdk-lib/aws-certificatemanager'
import { Vpc } from 'aws-cdk-lib/aws-ec2'
import { Cluster, ContainerImage, LogDriver } from 'aws-cdk-lib/aws-ecs'
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'
import { ApplicationProtocol } from 'aws-cdk-lib/aws-elasticloadbalancingv2'
import { LogGroup } from 'aws-cdk-lib/aws-logs'
import { HostedZone } from 'aws-cdk-lib/aws-route53'
import { env } from 'process'
function createStack (scope, id, props) {
const stack = new Stack(scope, id, props)
const logGroup = new LogGroup(stack, 'LogGroup', { logGroupName: 'so-service', removalPolicy: RemovalPolicy.DESTROY })
const vpc = Vpc.fromLookup(stack, 'Vpc', { vpcName: 'BlogVpc' })
const domainZone = HostedZone.fromLookup(stack, 'ZonePublic', { domainName: 'victorsmirnov.blog' })
const domainName = 'service.victorsmirnov.blog'
const certificate = new Certificate(stack, 'SslCertificate', {
domainName,
validation: CertificateValidation.fromDns(domainZone)
})
const cluster = new Cluster(stack, 'Cluster', {
clusterName: 'so-cluster',
containerInsights: true,
enableFargateCapacityProviders: true,
vpc,
})
const service = new ApplicationLoadBalancedFargateService(stack, id, {
assignPublicIp: true,
certificate,
cluster,
cpu: 256,
desiredCount: 1,
domainName,
domainZone,
memoryLimitMiB: 512,
openListener: true,
protocol: ApplicationProtocol.HTTPS,
publicLoadBalancer: true,
redirectHTTP: true,
targetProtocol: ApplicationProtocol.HTTP,
taskImageOptions: {
containerName: 'nginx',
containerPort: 80,
enableLogging: true,
family: 'so-service',
image: ContainerImage.fromRegistry('nginx'),
logDriver: LogDriver.awsLogs({ streamPrefix: 'nginx', logGroup })
}
})
service.targetGroup.configureHealthCheck({
path: '/',
})
return stack
}
const app = new App()
createStack(app, 'SingleInstanceAlbService', {
env: { account: env.CDK_DEFAULT_ACCOUNT, region: env.CDK_DEFAULT_REGION }
})
Content for the cdk.json and package.json for completeness.
{
"app": "node question.js",
"context": {
"#aws-cdk/core:newStyleStackSynthesis": true
}
}
{
"name": "alb-single-instance",
"version": "0.1.0",
"dependencies": {
"aws-cdk-lib": "^2.37.1",
"cdk": "^2.37.1",
"constructs": "^10.1.76"
},
"devDependencies": {
"rimraf": "^3.0.2",
"snazzy": "^9.0.0",
"standard": "^17.0.0"
},
"scripts": {
"cdk": "cdk",
"clean": "rimraf cdk.out dist",
"format": "standard --fix --verbose | snazzy",
"test": "standard --verbose | snazzy"
},
"type": "module"
}
This should be enough to have a fully functional setup where everything is configured automatically using the CDK.
Maybe you do not need the load balancer because there is no need to balance traffic for only one task. You can set up the Service discovery for your service and use the DNS name for your task without a load balancer. This should save money if you want.
Your application can still be in one AZ. The fact that ALB requires two AZs is only related to ALB itself. So you do not have to create any extra instance of your application in other AZ if you don't want. Though it could be a good idea for high-availability.
I use AWS CDK to create eks cluster then use addNodegroupCapacity to add nodegroup.
const myNodeGroup = cluster.addNodegroupCapacity('my-node-group', {
nodegroupName: 'my-node-group',
instanceTypes: [
new ec2.InstanceType('t3a.small'),
],
minSize: 1,
desiredSize: 1,
maxSize: 1,
diskSize: 10,
capacityType: eks.CapacityType.SPOT,
amiType: eks.NodegroupAmiType.AL2_X86_64,
subnets: { subnetType: ec2.SubnetType.PUBLIC },
})
I want to change subnet to
subnets: { availabilityZones: ['ap-southeast-1a'] }
When I made change in CDK I got an error
Resource handler returned message: "NodeGroup already exists with name my-node-group and cluster name (Service: Eks, Status Code: 409, Request ID: {Request ID})" (RequestToken: {RequestToken}, HandlerErrorCode: AlreadyExists)
How can I edit this nodegroup from AWS CDK or I have to delete and recreate it?
Changing the subnet is a replacement operation, which means the NodeGroup will be destroyed and another created. However, your explicit nodegroupName is interfering with this CloudFormation process. For this reason it's best practice to use generated resource names, not physical names.
Delete the resource manually. Remove the nodegroupName prop to avoid this problem in the future.
I am using AWS CDK to create a CloudFormation Stack with a RDS Aurora Cluster Database, VPC, Subnet, RouteTable and Security Group resources. And another Stack with a couple of Lambdas, API Gateway, IAM Roles and Policies and many other resources.
The CDK deployment works fine and I can see both stack created in CloudFormation with all the resources. But I had issues trying to connect with the RDS Database so I added a CfnOutput to check the connection string and realised that the RDS port was not resolved from it's original number-encoded token, while the hostname is resolved properly? So, I'm wondering why this is happening...
This is how I'm setting the CfnOutput:
new CfnOutput(this, "mysql-messaging-connstring", {
value: connectionString,
description: "Mysql connection string",
exportName: `${prefix}-mysqlconnstring`
});
The RDS Aurora Database Cluster is created in a method called createDatabaseCluster:
const cluster = new rds.DatabaseCluster(scope, 'Database', {
engine: rds.DatabaseClusterEngine.auroraMysql({ version: rds.AuroraMysqlEngineVersion.VER_5_7_12 }),
credentials: dbCredsSecret,
instanceProps: {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL),
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
vpc: vpc,
publiclyAccessible: true,
securityGroups: [ clusterSG ]
},
instances: 1,
instanceIdentifierBase: dbInstanceName,
});
This createDatabaseCluster method returns the connection string:
return `server=${cluster.instanceEndpoints[0].hostname};user=${username};password=${password};port=${cluster.instanceEndpoints[0].port};database=${database};`;
In this connection string, the DB credentials are retrieved from a secret in AWS Secrets Manager and stored in username and password variables to be used in the return statement.
The actual observed value of the CfnOutput is as follow:
As a workaround, I can just specify the port to be used but I want to understand what's the reason why this number-encoded token is not being resolved.
I'm currently migrating an AWS stack defined in Cloudformation (CFT) to CDK. The goal is not to trigger a replacement of viral resources, but I'm stuck with my Application Load Balancer.
In the old CFT stack the ALB is defined as:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
without the "Type" Property set which allows the following values: application | gateway | network
Anyways the resulting Resource in AWS Console has the Type set to "application".
In CDK I create the ALB like:
new ApplicationLoadBalancer(this, 'alb', {
vpc,
internetFacing: true,
vpcSubnets: {
subnets: vpc.publicSubnets,
},
securityGroup: this.securityGroup,
});
unfortunately this triggers a replacement because "Type": "application" is now set explicitly.
Is there any way around this? My next guess would be to try an Cfn Construct...
The most convenient solution I found was to just delete the property that is set implicitly in the L2 Construct.
const alb = new ApplicationLoadBalancer(this, 'alb', {
vpc,
internetFacing: true,
vpcSubnets: {
subnets: vpc.publicSubnets,
},
securityGroup: mySg
});
// First cast Construct to its underlying Cfn Construct
// Then delete property
(alb.node.defaultChild as CfnLoadBalancer).addDeletionOverride('Properties.Type');
More information can be found here: AWS Documentation: Escape hatches
In AWS CDK, I have an EC2 instance and Elastic IP created as follows:
// EC2 Instance
let ec2Instance = new ec2.Instance(this, "EC2Instance", {
instanceType: ec2.InstanceType.of(InstanceClass.T2, InstanceSize.MICRO),
vpc: vpc,
securityGroup: securityGroupEc2,
keyName: Config.keyPairName,
machineImage: new ec2.GenericLinuxImage({"eu-west-1": Config.ec2ImageAmi}),
blockDevices: [{deviceName: "/dev/sda1", volume: ec2.BlockDeviceVolume.ebs(30)}]
});
// Elastic IP
let eip = new ec2.CfnEIP(this, "Ip");
I have difficulties to understand how I can declare an association between these, as I can not perceive using AWS CDK documentation how to declare that. Seems that I need AWS::EC2::EIPAssociation.EIP: string to supply and I am missing as to how to get it from the eip object.
It wasn't explained very well, but the solution is:
// EC2 Instance <> EIP
let ec2Assoc = new ec2.CfnEIPAssociation(this, "Ec2Association", {
eip: eip.ref,
instanceId: ec2Instance.instanceId
});
For Cfn* CDK resources, I find that the CloudFormation docs are much more informative than the CDK API.
The CloudFormation docs for AWS::EC2::EIP show that there is an instanceId property that can be used when creating the EIP, which I believe should avoid the need to create a CfnEIPAssociation separately.