diff --git a/java/application-load-balancer/.gitignore b/java/application-load-balancer/.gitignore new file mode 100644 index 0000000000..1db21f1629 --- /dev/null +++ b/java/application-load-balancer/.gitignore @@ -0,0 +1,13 @@ +.classpath.txt +target +.classpath +.project +.idea +.settings +.vscode +*.iml + +# CDK asset staging directory +.cdk.staging +cdk.out + diff --git a/java/application-load-balancer/README.md b/java/application-load-balancer/README.md new file mode 100644 index 0000000000..6153214c76 --- /dev/null +++ b/java/application-load-balancer/README.md @@ -0,0 +1,54 @@ +# Application Load Balancer + + + + +--- + +![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge) + +> **This is a stable example. It should successfully build out of the box** +> +> This example is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build. + +--- + + +This example creates an AutoScalingGroup (containing 2 AWS Graviton2 Micro-T4G EC2 machines running the Amazon Linux 2023 AMI), and an ApplicationLoadBalancer inside a shared VPC. +It hooks up an open listener from the Load Balancer to the Scaling Group to indicate how many targets to balance between. + +For more info on using Auto Scaling with Load Balancing see the AWS guide [here](https://docs.aws.amazon.com/autoscaling/ec2/userguide/autoscaling-load-balancer.html). + +## Build + +To build this example, you need to be in this example's root directory. +Then, run the following: + +```bash + npm install -g aws-cdk + npm install + cdk synth +``` + +This will install the necessary CDK, then this example's dependencies, and then build the CloudFormation template. +The resulting CloudFormation template will be in the `cdk.out` directory. + +## Deploy + +Run `cdk deploy`. +This will deploy / redeploy the stack to AWS. +After a successful deployment, the URL of the ALB created will be available in the output of the terminal console. +At this point, the ALB URL can be used for testing. +Copy the ALB URL and paste it in the address bar of a browser. +The ALB will route the received requests between the 2 EC2 instances created initially by the ASG. +You can observe that the requests made reach different EC2 instances. +This can be observed based on the content of the web page displayed in the browser (the hello message on the web page contains the host which is different for each EC2 instance). + +## Useful commands + + * `mvn package` compile and run tests + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation diff --git a/java/application-load-balancer/cdk.json b/java/application-load-balancer/cdk.json new file mode 100644 index 0000000000..1ce7118872 --- /dev/null +++ b/java/application-load-balancer/cdk.json @@ -0,0 +1,76 @@ +{ + "app": "mvn -e -q compile exec:java", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTimeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true + } +} diff --git a/java/application-load-balancer/pom.xml b/java/application-load-balancer/pom.xml new file mode 100644 index 0000000000..d66f161978 --- /dev/null +++ b/java/application-load-balancer/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.myorg + application-load-balancer + 0.1 + + + UTF-8 + 2.171.1 + [10.0.0,11.0.0) + 5.7.1 + 22 + 22 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.myorg.ApplicationLoadBalancerApp + + + + + + + + software.amazon.awscdk + aws-cdk-lib + ${cdk.version} + + + software.constructs + constructs + ${constructs.version} + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + diff --git a/java/application-load-balancer/src/main/java/com/myorg/ApplicationLoadBalancerApp.java b/java/application-load-balancer/src/main/java/com/myorg/ApplicationLoadBalancerApp.java new file mode 100644 index 0000000000..9bbfd41634 --- /dev/null +++ b/java/application-load-balancer/src/main/java/com/myorg/ApplicationLoadBalancerApp.java @@ -0,0 +1,13 @@ +package com.myorg; + +import software.amazon.awscdk.App; +import software.amazon.awscdk.StackProps; + +public class ApplicationLoadBalancerApp { + public static void main(final String[] args) { + var app = new App(); + var stackProps = StackProps.builder().build(); + new ApplicationLoadBalancerStack(app, "ApplicationLoadBalancerStack", stackProps); + app.synth(); + } +} diff --git a/java/application-load-balancer/src/main/java/com/myorg/ApplicationLoadBalancerStack.java b/java/application-load-balancer/src/main/java/com/myorg/ApplicationLoadBalancerStack.java new file mode 100644 index 0000000000..fa75aaa1c5 --- /dev/null +++ b/java/application-load-balancer/src/main/java/com/myorg/ApplicationLoadBalancerStack.java @@ -0,0 +1,79 @@ +package com.myorg; + +import software.amazon.awscdk.CfnOutput; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.autoscaling.AutoScalingGroup; +import software.amazon.awscdk.services.autoscaling.RequestCountScalingProps; +import software.amazon.awscdk.services.ec2.*; +import software.amazon.awscdk.services.elasticloadbalancingv2.AddApplicationTargetsProps; +import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationListenerProps; +import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationLoadBalancer; +import software.constructs.Construct; + +import java.util.List; + +public class ApplicationLoadBalancerStack extends Stack { + + // This user data string is required to deploy a nginx web server to each EC2 instance created. + // A simple web page that displays the host name of the EC2 instance is also deployed to nginx. + // This is useful to check if the ALB created sends requests to different EC2 instances by observing the hostname displayed. + private static final String USER_DATA_CONTENT_NGINX = """ + #!/bin/bash + dnf -y upgrade + dnf -y install nginx + systemctl start nginx + systemctl enable nginx + rm -rfv /usr/share/nginx/html/ && mkdir /usr/share/nginx/html/ + echo "

Hello world from $(hostname -f)

" > /usr/share/nginx/html/index.html + """; + + public ApplicationLoadBalancerStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + var vpc = Vpc.Builder.create(this, "VPC") + .restrictDefaultSecurityGroup(false) + .build(); + var amazonLinuxImage = AmazonLinuxImage.Builder.create() + .generation(AmazonLinuxGeneration.AMAZON_LINUX_2023) + .cpuType(AmazonLinuxCpuType.ARM_64) + .userData(UserData.custom(USER_DATA_CONTENT_NGINX)) + .build(); + var autoScalingGroup = AutoScalingGroup.Builder.create(this, "ASG") + .vpc(vpc) + .instanceType(InstanceType.of(InstanceClass.BURSTABLE4_GRAVITON, InstanceSize.MICRO)) + .machineImage(amazonLinuxImage) + // Change the desired capacity (default value of 1) so that 2 EC2 instances are created initially instead of one. + // This way the ALB will route requests between both instances right away making it easier to test load balancing functionality. + .desiredCapacity(2) + .maxCapacity(3) + .minCapacity(1) + .build(); + var applicationLoadBalancer = ApplicationLoadBalancer.Builder.create(this, "LB") + .vpc(vpc) + .loadBalancerName("LB") + .internetFacing(Boolean.TRUE) + .build(); + var applicationListenerProps = ApplicationListenerProps.builder() + .port(80) + .loadBalancer(applicationLoadBalancer) + .build(); + var applicationListener = applicationLoadBalancer.addListener("Listener", applicationListenerProps); + var addApplicationTargetsProps = AddApplicationTargetsProps.builder() + .port(80) + .targets(List.of(autoScalingGroup)) + .build(); + applicationListener.addTargets("Target", addApplicationTargetsProps); + applicationListener.getConnections().allowDefaultPortFromAnyIpv4("Open to the world"); + var requestCountScalingProps = RequestCountScalingProps.builder() + .targetRequestsPerMinute(60) + .build(); + // This is an example on how to create a dynamic target tracking scaling policy. + // This is based on the ALB request count per target, but it is more difficult to test. + // The dynamic scaling policy should show up though in the AWS Management Console for the ASG created. + autoScalingGroup.scaleOnRequestCount("AModestLoad", requestCountScalingProps); + CfnOutput.Builder.create(this, "ApplicationLoadBalancerURL") + .value(applicationListener.getLoadBalancer().getLoadBalancerDnsName()) + .description("The DNS of the application load balancer.") + .build(); + } +} diff --git a/java/application-load-balancer/src/test/java/com/myorg/ApplicationLoadBalancerTest.java b/java/application-load-balancer/src/test/java/com/myorg/ApplicationLoadBalancerTest.java new file mode 100644 index 0000000000..46ccbb9ccf --- /dev/null +++ b/java/application-load-balancer/src/test/java/com/myorg/ApplicationLoadBalancerTest.java @@ -0,0 +1,142 @@ +package com.myorg; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.App; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.assertions.Template; + +public class ApplicationLoadBalancerTest { + + private static Template template; + + @BeforeAll + public static void setUp() { + var app = new App(); + var stackProps = StackProps.builder().build(); + var stack = new ApplicationLoadBalancerStack(app, "ApplicationLoadBalancerStack", stackProps); + template = Template.fromStack(stack); + } + + @Test + @DisplayName("Test if number of VPCs in the stack is equal to the expected value.") + public void testVPCsCount() { + template.resourceCountIs("AWS::EC2::VPC", 1); + } + + @Test + @DisplayName("Test if number of subnets in the stack is equal to the expected value.") + public void testSubnetsCount() { + template.resourceCountIs("AWS::EC2::Subnet", 4); + } + + @Test + @DisplayName("Test if number of route tables in the stack is equal to the expected value.") + public void testRouteTablesCount() { + template.resourceCountIs("AWS::EC2::RouteTable", 4); + } + + @Test + @DisplayName("Test if number of subnet route table associations in the stack is equal to the expected value.") + public void testSubnetRouteTableAssociationsCount() { + template.resourceCountIs("AWS::EC2::SubnetRouteTableAssociation", 4); + } + + @Test + @DisplayName("Test if number of routes in the stack is equal to the expected value.") + public void testRoutesCount() { + template.resourceCountIs("AWS::EC2::Route", 4); + } + + @Test + @DisplayName("Test if number of EIPs in the stack is equal to the expected value.") + public void testEIPsCount() { + template.resourceCountIs("AWS::EC2::EIP", 2); + } + + @Test + @DisplayName("Test if number of NAT gateways in the stack is equal to the expected value.") + public void testNatGatewaysCount() { + template.resourceCountIs("AWS::EC2::NatGateway", 2); + } + + @Test + @DisplayName("Test if number of Internet gateways in the stack is equal to the expected value.") + public void testInternetGatewaysCount() { + template.resourceCountIs("AWS::EC2::InternetGateway", 1); + } + + @Test + @DisplayName("Test if number of VPC gateway attachments in the stack is equal to the expected value.") + public void testVPCGatewayAttachmentsCount() { + template.resourceCountIs("AWS::EC2::VPCGatewayAttachment", 1); + } + + @Test + @DisplayName("Test if number of security groups in the stack is equal to the expected value.") + public void testSecurityGroupsCount() { + template.resourceCountIs("AWS::EC2::SecurityGroup", 2); + } + + @Test + @DisplayName("Test if number of security group ingress resources in the stack is equal to the expected value.") + public void testSecurityGroupIngressResourcesCount() { + template.resourceCountIs("AWS::EC2::SecurityGroupIngress", 1); + } + + @Test + @DisplayName("Test if number of roles in the stack is equal to the expected value.") + public void testRolesCount() { + template.resourceCountIs("AWS::IAM::Role", 1); + } + + @Test + @DisplayName("Test if number of IAM instance profiles in the stack is equal to the expected value.") + public void testIAMInstanceProfilesCount() { + template.resourceCountIs("AWS::IAM::InstanceProfile", 1); + } + + @Test + @DisplayName("Test if number of launch configurations in the stack is equal to the expected value.") + public void testLaunchConfigurationsCount() { + template.resourceCountIs("AWS::AutoScaling::LaunchConfiguration", 1); + } + + @Test + @DisplayName("Test if number of auto scaling groups in the stack is equal to the expected value.") + public void testAutoScalingGroupsCount() { + template.resourceCountIs("AWS::AutoScaling::AutoScalingGroup", 1); + } + + @Test + @DisplayName("Test if number of scaling policies in the stack is equal to the expected value.") + public void testScalingPoliciesCount() { + template.resourceCountIs("AWS::AutoScaling::ScalingPolicy", 1); + } + + @Test + @DisplayName("Test if number of load balancers in the stack is equal to the expected value.") + public void testLoadBalancersCount() { + template.resourceCountIs("AWS::ElasticLoadBalancingV2::LoadBalancer", 1); + } + + @Test + @DisplayName("Test if number of security group egress resources in the stack is equal to the expected value.") + public void testSecurityGroupEgressResourcesCount() { + template.resourceCountIs("AWS::EC2::SecurityGroupEgress", 1); + } + + @Test + @DisplayName("Test if number of load balancer listeners in the stack is equal to the expected value.") + public void testLoadBalancerListenersCount() { + template.resourceCountIs("AWS::ElasticLoadBalancingV2::Listener", 1); + } + + @Test + @DisplayName("Test if number of load balancer target groups in the stack is equal to the expected value.") + public void testLoadBalancerTargetGroupsCount() { + template.resourceCountIs("AWS::ElasticLoadBalancingV2::TargetGroup", 1); + } + +}