AWS migrate ApplicationLoadBalancer from CFT to CDK without replacement - amazon-web-services

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

Related

Best way to accomplish _single_ instance container CI/CD on AWS?

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.

How can I edit Nodegroup in AWS EKS from AWS CDK

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.

AWS CDK ApplicationLoadBalancedFargateService in private (isolated) subnet

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.

Cloudformation Elasticbeanstalk specify target group for shared load balancer

I have two Cloudformation templates
one which creates a VPC, ALB and any other shared resources etc.
one which creates an elastic beanstalk environment and relevant listener rules to direct traffic to this environment using the imported shared load balancer (call this template Environment)
The problem I'm facing is the Environment template creates a AWS::ElasticBeanstalk::Environment which subsequently creates a new CFN stack which contains things such as the ASG, and Target Group (or process as it is known to elastic beanstalk). These resources are not outputs of the AWS owned CFN template used to create the environment.
When setting
- Namespace: aws:elasticbeanstalk:environment
OptionName: LoadBalancerIsShared
Value: true
In the optionsettings for my elastic beanstalk environment, a load balancer is not created which is fine. I then try to attach a listener rule to my load balancer listener.
ListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Priority: 1
ListenerArn:
Fn::ImportValue: !Sub '${NetworkStackName}-HTTPS-Listener'
Actions:
- Type: forward
TargetGroupArn: WHAT_GOES_HERE
Conditions:
- Field: host-header
HostHeaderConfig:
Values:
- mywebsite.com
DependsOn:
- Environment
The problem here is that I don't have access as far as I can tell to the ARN of the target group created by the elastic beanstalk environment resource. If I create a target group then it's not linked to elastic beanstalk and no instances are present.
I found the this page which states
The resources that Elastic Beanstalk creates for your environment have names. You can use these names to get information about the resources with a function, or modify properties on the resources to customize their behavior.
But because they're in a different stack (of which i don't know the name in advance), not ouputs of the template, I have no idea how to get hold of them.
--
Edit:
Marcin pointed me in the direction of a custom resource in their answer. I have expanded on it slightly and got it working. The implementation is slightly different in a couple of ways
it's in Node instead of Python
the api call describe_environment_resources in the example provided returns a list of resources, but seemingly not all of them. In my implementation I grab the auto scaling group, and use the Physical Resource ID to look up the other resources in the stack to which it belongs using the Cloudformation API.
const AWS = require('aws-sdk');
const cfnResponse = require('cfn-response');
const eb = new AWS.ElasticBeanstalk();
const cfn = new AWS.CloudFormation();
exports.handler = (event, context) => {
if (event['RequestType'] !== 'Create') {
console.log(event[RequestType], 'is not Create');
return cfnResponse.send(event, context, cfnResponse.SUCCESS, {
Message: `${event['RequestType']} completed.`,
});
}
eb.describeEnvironmentResources(
{ EnvironmentName: event['ResourceProperties']['EBEnvName'] },
function (err, { EnvironmentResources }) {
if (err) {
console.log('Exception', e);
return cfnResponse.send(event, context, cfnResponse.FAILED, {});
}
const PhysicalResourceId = EnvironmentResources['AutoScalingGroups'].find(
(group) => group.Name
)['Name'];
const { StackResources } = cfn.describeStackResources(
{ PhysicalResourceId },
function (err, { StackResources }) {
if (err) {
console.log('Exception', e);
return cfnResponse.send(event, context, cfnResponse.FAILED, {});
}
const TargetGroup = StackResources.find(
(resource) =>
resource.LogicalResourceId === 'AWSEBV2LoadBalancerTargetGroup'
);
cfnResponse.send(event, context, cfnResponse.SUCCESS, {
TargetGroupArn: TargetGroup.PhysicalResourceId,
});
}
);
}
);
};
The Cloudformation templates
LambdaBasicExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess
- arn:aws:iam::aws:policy/AWSElasticBeanstalkReadOnly
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
GetEBLBTargetGroupLambda:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Description: 'Get ARN of EB Load balancer'
Timeout: 30
Role: !GetAtt 'LambdaBasicExecutionRole.Arn'
Runtime: nodejs12.x
Code:
ZipFile: |
... code ...
ListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Priority: 1
ListenerArn:
Fn::ImportValue: !Sub '${NetworkStackName}-HTTPS-Listener'
Actions:
- Type: forward
TargetGroupArn:
Fn::GetAtt: ['GetEBLBTargetGroupResource', 'TargetGroupArn']
Conditions:
- Field: host-header
HostHeaderConfig:
Values:
- mydomain.com
Things I learned while doing this which hopefully help others
using async handlers in Node is difficult with the default cfn-response library which is not async and results in the Cloudformation creation (and deletion) process hanging for many hours before rolling back.
the cfn-response library is included automatically by cloudformation if you use ZipFile. The code is available on the AWS Docs if you were so inclined to include it manually (you could also wrap it in a promise then and use async lambda handlers). There are also packages on npm to achieve the same effect.
Node 14.x couldn't run, Cloudformation threw up an error. I didn't make note of what it was, unfortunately.
The policy AWSElasticBeanstalkFullAccess used in the example provided no longer exists and has been replaced with AdministratorAccess-AWSElasticBeanstalk.
My example above needs less permissive policies attached but I've not yet addressed that in my testing. It'd be better if it could only read the specific elastic beanstalk environment etc.
I don't have access as far as I can tell to the ARN of the target group created by the elastic beanstalk environment resource
That's true. The way to overcome this is through custom resource. In fact I developed fully working, very similar resource for one of my previous answers, thus you can have a look at it and adopt to your templates. The resource returns ARN of the EB load balancer, but you could modify it to get the ARN of EB's target group instead.

How to create Elastic IP association with an EC2 instance using AWS CDK?

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.