Introduction#
This GitHub show a simple architecture with load balancer, autoscaling group, and a webserver with userdata
- Create a VPC
- Create a application load balancer
- Create an autoscaling group (asg) 2-2-2
- Add userData to run a web
- Terminate an EC2 and see (asg) launch a new EC2
Network Stack#
export class VpcStack extends Stack {public readonly vpc: aws_ec2.Vpcconstructor(scope: Construct, id: string, props: VpcProps) {super(scope, id, props)this.vpc = new aws_ec2.Vpc(this, 'VpcAlbDemo', {vpcName: 'VpcAlbDemo',cidr: props.cidr,subnetConfiguration: [{name: 'Public',cidrMask: 24,subnetType: aws_ec2.SubnetType.PUBLIC},{name: 'PrivateWithNat',cidrMask: 24,subnetType: aws_ec2.SubnetType.PRIVATE_WITH_NAT},{name: 'PrivateWoNat',cidrMask: 24,subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED}]})}}
look up for an existed vpc
interface ImportedVpcProps extends StackProps {vpcId: stringvpcName: string}export class ImportedVpcStack extends Stack {public readonly vpc: aws_ec2.IVpcconstructor(scope: Construct, id: string, props: ImportedVpcProps) {super(scope, id, props)this.vpc = aws_ec2.Vpc.fromLookup(this, 'LookupExistedVpc', {vpcId: props.vpcId,vpcName: props.vpcName})}}
Application Load Balancer#
security group for alb
const albSecurityGroup = new aws_ec2.SecurityGroup(this, 'SGForWeb', {securityGroupName: 'SGForWeb',vpc: vpc})albSecurityGroup.addIngressRule(aws_ec2.Peer.anyIpv4(),aws_ec2.Port.tcp(80),'Allow port 80 web')
application load balancer
const alb = new aws_elasticloadbalancingv2.ApplicationLoadBalancer(this,'AlbWebDemo',{vpc: vpc,loadBalancerName: 'AlbWebDemo',vpcSubnets: {subnetType: aws_ec2.SubnetType.PUBLIC},internetFacing: true,deletionProtection: false,securityGroup: albSecurityGroup})
add listener port 80
const listener = alb.addListener('AlbListener', {port: 80})
Auto Scaling Group#
security group for auto scaling group
const asgSecurityGroup = new aws_ec2.SecurityGroup(this, 'SGForASG', {securityGroupName: 'SGForASG',vpc: props.vpc})asgSecurityGroup.addIngressRule(aws_ec2.Peer.securityGroupId(albSecurityGroup.securityGroupId),aws_ec2.Port.tcp(80))
auto scaling group
const asg = new aws_autoscaling.AutoScalingGroup(this, 'AsgDemo', {autoScalingGroupName: 'AsgWebDemo',vpc: vpc,instanceType: aws_ec2.InstanceType.of(aws_ec2.InstanceClass.T2,aws_ec2.InstanceSize.SMALL),machineImage: aws_ec2.MachineImage.latestAmazonLinux2023({cachedInContext: true}),minCapacity: 2,maxCapacity: 2,vpcSubnets: {subnets: vpc.privateSubnets},role: role,securityGroup: asgSecurityGroup})
asg user data - download and run webserver
asg.addUserData(fs.readFileSync('./lib/script/user-data.sh', 'utf8'))
ALB Listener#
an implicit target group created
listener.addTargets('Target', {port: 80,targets: [asg],healthCheck: {path: '/',port: '80',protocol: aws_elasticloadbalancingv2.Protocol.HTTP,healthyThresholdCount: 5,unhealthyThresholdCount: 2,timeout: Duration.seconds(10)}})
HTTPS
- create ACM certificate
- add listener https on alb
- create a record on route53
const listenerHTTPS = alb.addListener('AlbListenerHTTPS', {port: 443,open: true,protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTPS,certificates: [ListenerCertificate.fromArn(props.acmCertArn ? props.acmCertArn : '')]})listenerHTTPS.addTargets('TargetHTTPS', {port: 80,targets: [asg],healthCheck: {path: '/',port: '80',protocol: aws_elasticloadbalancingv2.Protocol.HTTP,healthyThresholdCount: 5,unhealthyThresholdCount: 2,timeout: Duration.seconds(10)}})
Route53#
Script to update route53 record
import osimport boto3# change to entest accountos.system("set-aws-account.sh entest ap-southeast-1")# route53 clientclient = boto3.client('route53')# update load balancer dnsresponse = client.change_resource_record_sets(ChangeBatch={'Changes': [{'Action': 'UPSERT','ResourceRecordSet': {'Name': 'image-vng.entest.io','ResourceRecords': [{'Value': $ALB_ENDPOINT,},],'TTL': 300,'Type': 'CNAME',},},],'Comment': 'Web Server',},HostedZoneId=$HOSTED_ZONE_ID,)print(response)# change back to demo accountos.system("set-aws-account.sh demo us-east-1")
Scaling Policy#
target tracking - on cpu usage
asg.scaleOnCpuUtilization('KeepSparseCPU', {targetUtilizationPercent: 50})
target tracking - on number of request per instance
asg.scaleOnRequestCount('AvgReqeustPerInstance', {targetRequestsPerMinute: 1000})
step scale - based on custom metric
const metric = new aws_cloudwatch.Metric({metricName: 'CPUUtilization',namespace: 'AWS/EC2',statistic: 'Average',period: Duration.minutes(1),dimensionsMap: {AutoScalingGroupName: asg.autoScalingGroupName}})
scale on custom metric with custom step
asg.scaleOnMetric('MyMetric', {metric: metric,scalingSteps: [{upper: 1,change: -1},{lower: 10,change: +1},{lower: 60,change: +3}],adjustmentType: aws_autoscaling.AdjustmentType.CHANGE_IN_CAPACITY})
Bedrock#
Let update the instance role so it can call Bedorck
role.addToPolicy(new aws_iam.PolicyStatement({effect: Effect.ALLOW,resources: ['arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2','arn:aws:bedrock:us-east-1::foundation-model/stability.stable-diffusion-xl-v1'],actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream']}))// allow push and pull ecrrole.addManagedPolicy(aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'))
And user-data-bedrock here
user-data-bedrock.sh
#!/bin/bash# export account idexport ACCOUNT_ID=111222333444# export regionexport REGION=us-east-1# install dockeryes | dnf install docker# start dockersystemctl start docker# kill running containers# docker kill $(docker ps -q)# delete all existing images# yes | docker system prune -a# auth ecraws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com# pull and rundocker pull $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/next-bedrock:latest# run docker imagedocker run -d -p 80:3000 $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/next-bedrock:latest# debug# sudo docker exec -it sad_hellman /bin/bash# sudo docker exec -it sad_hellman /bin/sh# sudo docker run -d -p 3000:3000 $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/next-bedrock:latest
Load Test#
Option 1. manually terminal EC2 instances Option 2. send concurrent requests
import timeimport requestsfrom concurrent.futures import ThreadPoolExecutorURL = "http://$ALB_URL"NO_CONCUR_REQUEST = 1000COUNT = 1def send_request():resp = requests.get(URL)# print(resp)def test_concurrent():with ThreadPoolExecutor(max_workers=NO_CONCUR_REQUEST) as executor:for k in range(1, NO_CONCUR_REQUEST):executor.submit(send_request)while True:print(f"{NO_CONCUR_REQUEST} requests {COUNT}")test_concurrent()time.sleep(1)COUNT += 1
User Data#
user-data-1
#!/bin/bashcd ~wget https://github.com/cdk-entest/alb-asg-demo/archive/refs/heads/main.zipunzip main.zipcd alb-asg-demo-maincd webpython3 -m pip install -r requirements.txtpython3 -m app
user-data-2
#!/bin/bashcd ~wget https://github.com/cdk-entest/eks-cdk-web/archive/refs/heads/master.zipunzip master.zipcd eks-cdk-web-master/webapppython3 -m ensurepip --upgradepython3 -m pip install -r requirements.txtpython3 -m app
user-data-3
#!/bin/bash# # kill -9 $(lsof -t -i:8080)cd ~# download vim configurationwget -O ~/.vimrc https://raw.githubusercontent.com/cdk-entest/basic-vim/main/.vimrc# download web appwget https://github.com/cdk-entest/flask-tailwind-polly/archive/refs/heads/master.zipunzip master.zipcd flask-tailwind-polly-master# install pippython3 -m ensurepip --upgrade# install dependenciespython3 -m pip install -r requirements.txtcd app# export bucket name for polly appexport BUCKET_NAME="vpb-polly-demo-10072023"# export region for polly appexport REGION="ap-southeast-1"python3 -m app