In this blog post I will show you how you can easily deploy containers to AWS ECS. You can check out the code used in this blog post on GitHub.
ECS is a container plattform on AWS that can run containers either using standard EC2 Instances or as a managed service using Fargate. Compared to something like Kubernetes you can get up and running very quickly on ECS.
Building the Spring Boot application
To start of we do need to build a container that can serve HTTP traffic. For this example I use a small Spring Boot application, but you can use every other HTTP backend as well.
Our application will serve some simple hello world text on the HOST:PORT/hello
path:
@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
@RestController
static class HelloController {
@GetMapping("/hello")
public String getHello() {
return "Hello World!";
}
}
}
Now lets build this application using maven with mvn package
. The built artifact will be available under the target/
directory.
Creating the container image
Before we start with the CDK part lets also create a Dockerfile
that describes how our spring application will
be packaged into a container.
This dockerfile uses the standard jdk16 base image based on alpine linux and simply copies our .jar
artifact into the
container.
FROM openjdk:16-jdk-alpine
ARG JAR_FILE=target/*.jar
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
If everything is correct you can test the container build using Docker docker build . -t helloapp
and
run the container with docker run helloapp -p 8080:8080
.
Building the infrastructure with CDK
Now lets move over to the CDK part. Since I like to separate infrastructure code from application code I create the CDK project in a different directory.
Usually it looks something like this:
hello/ # Repository root
hello/infra # CDK project containing our infrastructure
hello/service # Our actual service
Once in the directory you can bootstrap the CDK project with the cli using cdk init app --language java
. In this blog
post I am using Java as well for the infrastructure part since I want to keep the programming language similar if possible. Though most CDK
projects do use TypeScript or Python.
Adding the needed dependencies
CDK separates its constructs in different libraries. For our example we need to add ec2
, ecs
and ecs-patterns
. In the pom.xml
we simply
add the following dependencies.
<dependency>
<groupId>software.amazon.awscdk</groupId>
<artifactId>ec2</artifactId>
<version>${cdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awscdk</groupId>
<artifactId>ecs</artifactId>
<version>${cdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awscdk</groupId>
<artifactId>ecs-patterns</artifactId>
<version>${cdk.version}</version>
</dependency>
Now to build our stack we set up a new VPC and ECS Cluster. The sweet part happens under the ApplicationLoadBalancedFargateService. It is a 2nd level construct by AWS that lets us avoid a lot of cumbersome configuration compared to the 1st level CloudFormation constructs.
In this expample we create a single running container in our private subnet that gets exposed on the internet with an ApplicationLoadBalancer.
public HelloInfraStack(final Construct parent, final String id, final StackProps props) {
super(parent, id, props);
// ECS needs a VPC to run inside
Vpc vpc = Vpc.Builder.create(this, "HelloVpc")
.maxAzs(3)
.build();
// Our ECS cluster. Can be fine tuned further
Cluster cluster = Cluster.Builder.create(this, "HelloCluster")
.vpc(vpc).build();
// The actual container definition.
// Here we define the container resources and image
ApplicationLoadBalancedFargateService fargateService = ApplicationLoadBalancedFargateService.Builder.create(this, "HelloFargateService")
.cluster(cluster)
.cpu(256)
.desiredCount(1)
.taskImageOptions(
// This lets CDK build a container during deployment. We can simply point to a directory with a dockerfile inside.
ApplicationLoadBalancedTaskImageOptions.builder()
.image(ContainerImage.fromAsset("../hello-service"))
.containerPort(8080)
.build())
.memoryLimitMiB(512)
.openListener(true)
// We want to expose our cotainer using an ALB
.publicLoadBalancer(true)
.build();
// Configure the ALB health checks
fargateService.getTargetGroup().configureHealthCheck(HealthCheck.builder()
.healthyHttpCodes("200")
.path("/hello")
.build());
}
Now to deploy our infastructure we go to the commandline and execute cdk deploy
.
After some waiting we should see the stack outputs on the commandline. The stack output does contain the uri to our ApplicationLoadBalancer. For my stack i got the following output:
Outputs:
HelloInfraStack.HelloFargateServiceLoadBalancerDNS98570353 = Hello-Hello-YMYEY4EYNOWB-460997296.eu-central-1.elb.amazonaws.com
HelloInfraStack.HelloFargateServiceServiceURL3049AAE5 = http://Hello-Hello-YMYEY4EYNOWB-460997296.eu-central-1.elb.amazonaws.com
So when I try to send a HTTP request to the endpoint I should get the hello world text served by our container.
❯ curl -v Hello-Hello-YMYEY4EYNOWB-460997296.eu-central-1.elb.amazonaws.com/hello
* Trying 52.28.236.87...
* TCP_NODELAY set
* Connected to Hello-Hello-YMYEY4EYNOWB-460997296.eu-central-1.elb.amazonaws.com (52.28.236.87) port 80 (#0)
> GET /hello HTTP/1.1
> Host: Hello-Hello-YMYEY4EYNOWB-460997296.eu-central-1.elb.amazonaws.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200
< Date: Sun, 05 Sep 2021 09:44:54 GMT
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 12
< Connection: keep-alive
<
* Connection #0 to host Hello-Hello-YMYEY4EYNOWB-460997296.eu-central-1.elb.amazonaws.com left intact
Hello World!* Closing connection 0
So it works!
As you probably noticed, this endpoint is only availabe over HTTP without encryption. We will add TLS encryption in a later step.
Cheers
Timur