Containerize and run
In the first lab you are going to build the application locally. We are leveraging Amazon Corretto a no-cost, multi-platform, production-ready distribution of the Open Java Development Kit (OpenJDK). Corretto comes with long-term support that will include performance enhancements and security fixes. Amazon runs Corretto internally on thousands of production services and Corretto is certified as compatible with the Java SE standard.
Building the application locally
- Navigate to the application folder and build the application via Maven:
1
2
cd ~/environment/unicorn-store-spring
mvn clean package && mv target/store-spring-1.0.0-exec.jar store-spring.jar
Running the application locally
To run the Java Application we need to get a valid database connection-url and password. The database url is stored in AWS Systems Manager Parameter Store and the database password in AWS Secrets Manager . The values will be provided to the application as environment variables.
- Set the environment variables from Secrets Manager and Parameter Store:
1
2
export SPRING_DATASOURCE_URL=$(aws ssm get-parameter --name databaseJDBCConnectionString | jq --raw-output '.Parameter.Value')
export SPRING_DATASOURCE_PASSWORD=$(aws secretsmanager get-secret-value --secret-id unicornstore-db-secret | jq --raw-output '.SecretString' | jq -r .password)
- Start Java application locally (listening on port 8080):
1
java -jar -Dserver.port=8080 store-spring.jar
- Open another terminal window and test the application via curl:
1
2
3
4
5
6
7
export SVC_URL=localhost:8080
curl --location --request POST $SVC_URL'/unicorns' --header 'Content-Type: application/json' --data-raw '{
"name": "'"Something-$(date +%s)"'",
"age": "20",
"type": "Animal",
"size": "Very big"
}' | jq
Don’t close the terminal window with test command, we will use it for the following tests.
- You should see the following output:
- Switch back to the initial terminal and stop the application with
Ctrl+C
.
Containerizing the application
The local build can be a good starting point, but in the real world another development machine might miss some of the required dependencies to run the application. In order to solve the “Works on my machine” problem we can use containers.
A container is a standardized unit of software development that holds everything that your software application requires to run. This includes relevant code, runtime, system tools, and system libraries.
Containers are created from a read-only template that’s called an image. Images are typically built from a Dockerfile. A Dockerfile is a plaintext file that specifies all of the components that are included in the container. After they’re built, these images are stored in a registry such as Amazon ECR where they can be downloaded from.
- Copy a simple Dockerfile to the current application folder:
1
2
cd ~/environment/unicorn-store-spring
cp dockerfiles/Dockerfile_00_initial Dockerfile
- Inspect the Dockerfile. You will see that we just copy our previously created jar file into the container. We are adding a dedicated user and group (1000) to use the least privilege identity (read more about this in our Well-Architected Container Build Lens ).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /unicorn-store-spring/Dockerfile
FROM public.ecr.aws/docker/library/maven:3.9-amazoncorretto-17-al2023 as builder
RUN yum install -y shadow-utils
COPY store-spring.jar store-spring.jar
RUN groupadd --system spring -g 1000
RUN adduser spring -u 1000 -g 1000
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["java","-jar","-Dserver.port=8080","/store-spring.jar"]
- Build a container image:
1
docker buildx build --load -t unicorn-store-spring:latest .
- List previously built container image:
1
docker images
- Run the container instance locally:
1
2
3
4
5
docker run \
-e SPRING_DATASOURCE_URL=$SPRING_DATASOURCE_URL \
-e SPRING_DATASOURCE_PASSWORD=$SPRING_DATASOURCE_PASSWORD \
-p 8080:8080 \
unicorn-store-spring:latest
- Open the terminal window with the test command and run it again:
1
2
3
4
5
6
7
export SVC_URL=localhost:8080
curl --location --request POST $SVC_URL'/unicorns' --header 'Content-Type: application/json' --data-raw '{
"name": "'"Something-$(date +%s)"'",
"age": "20",
"type": "Animal",
"size": "Very big"
}' | jq
- Switch back to the initial terminal and stop the application with
Ctrl+C
.
Building the application in the container
Instead of building the Java application locally and copying the final binaries to the container we can also run the build process inside the container. With this technique your development or build machines do not have to worry about installing the required build tools anymore. The dependencies for building your application are now embedded in the image.
- Copy the new Dockerfile to the current folder
1
2
cd ~/environment/unicorn-store-spring
cp dockerfiles/Dockerfile_01_original Dockerfile
- Start the build for the container image. While it is building, you can move to the next step and inspect the Dockerfile.
1
docker buildx build --load -t unicorn-store-spring:latest .
- Inspect the Dockerfile. You can now see that we are running the Maven command inside the container:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# /unicorn-store-spring/Dockerfile
FROM public.ecr.aws/docker/library/maven:3.9-amazoncorretto-17-al2023 as builder
RUN yum install -y shadow-utils
COPY ./pom.xml ./pom.xml
RUN mvn dependency:go-offline -f ./pom.xml
COPY src ./src/
RUN mvn clean package && mv target/store-spring-1.0.0-exec.jar store-spring.jar
RUN rm -rf ~/.m2/repository
RUN groupadd --system spring -g 1000
RUN adduser spring -u 1000 -g 1000
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["java","-jar","-Dserver.port=8080","/store-spring.jar"]
- List the container images:
1
docker images
- Run a container instance:
1
2
3
4
5
docker run \
-e SPRING_DATASOURCE_URL=$SPRING_DATASOURCE_URL \
-e SPRING_DATASOURCE_PASSWORD=$SPRING_DATASOURCE_PASSWORD \
-p 8080:8080 \
unicorn-store-spring:latest
- Open the terminal window with the test command and run it again:
1
2
3
4
5
6
7
export SVC_URL=localhost:8080
curl --location --request POST $SVC_URL'/unicorns' --header 'Content-Type: application/json' --data-raw '{
"name": "'"Something-$(date +%s)"'",
"age": "20",
"type": "Animal",
"size": "Very big"
}' | jq
- Switch back to the initial terminal and stop the application with
Ctrl+C
.
Optimizing the Dockerfile
We are now embedding additional build tools such as Maven, an image size will naturally increase. However, Maven is only needed during build-time and not for running the final JAR. You can therefore leverage a multi-stage build to reduce the image size by separating the build from the runtime stage.
- Check size of the initial image, it is 995 MB
1
docker images
Image size and application startup times might be different in your case
REPOSITORY TAG IMAGE ID CREATED SIZE
unicorn-store-spring latest 836da356dc0e About a minute ago 995MB
- Copy the prepared Dockerfile:
1
2
cd ~/environment/unicorn-store-spring
cp dockerfiles/Dockerfile_02_multistage Dockerfile
- Start the build for the container image. While it is building, you can move to the next step and inspect the Dockerfile.
1
docker buildx build --load -t unicorn-store-spring:latest .
- Inspect the Dockerfile.
As you can see in line 11 – we are starting with a fresh Amazon Corretto Image. On line 14 we are copying the artifact from the initial build stage to the fresh image.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# /unicorn-store-spring/Dockerfile
FROM public.ecr.aws/docker/library/maven:3.9-amazoncorretto-17-al2023 as builder
COPY ./pom.xml ./pom.xml
RUN mvn dependency:go-offline -f ./pom.xml
COPY src ./src/
RUN mvn clean package && mv target/store-spring-1.0.0-exec.jar store-spring.jar
RUN rm -rf ~/.m2/repository
FROM public.ecr.aws/docker/library/amazoncorretto:17.0.9-al2023
RUN yum install -y shadow-utils
COPY --from=builder store-spring.jar store-spring.jar
RUN groupadd --system spring -g 1000
RUN adduser spring -u 1000 -g 1000
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["java","-jar","-Dserver.port=8080","/store-spring.jar"]
- Check size of the image, it is 660 MB now.
1
docker images
Now we can see that the size of our image is less than in the previous build:
REPOSITORY TAG IMAGE ID CREATED SIZE
unicorn-store-spring latest ea42046620d4 29 seconds ago 660MB
With multi-stage build we achieved about 30% reduction of container image size.
We will continue to optimize the image in the following modules.
Pushing a container image to Amazon Elastic Container Registry (ECR)
Before we move to deploying containers in different environments, you will push container image to Amazon Elastic Container Registry (ECR) .
The Amazon ECR repository with the name unicorn-store-spring
was already created for you during the workshop setup. It is empty yet, but you can explore it and make yourself familiar with Amazon ECR in the AWS console:
- To be able to push images to the repository we need to login to the repository:
1
2
3
export ECR_URI=$(aws ecr describe-repositories --repository-names unicorn-store-spring | jq --raw-output '.repositories[0].repositoryUri')
echo $ECR_URI
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_URI
- Tag the local container image:
1
2
3
4
5
IMAGE_TAG=i$(date +%Y%m%d%H%M%S)
echo $IMAGE_TAG
docker tag unicorn-store-spring:latest $ECR_URI:$IMAGE_TAG
docker tag unicorn-store-spring:latest $ECR_URI:latest
docker images
- Push the image to Amazon ECR:
1
2
docker push $ECR_URI:$IMAGE_TAG
docker push $ECR_URI:latest
- Go to Amazon ECR in the AWS Console and verify that the image is uploaded:
Image size in Amazon ECR is smaller than locally due to compression
If you change application source code, you can run the set of commands below to build and push a new container image to ECR:
1
2
3
4
5
6
7
cd ~/environment/unicorn-store-spring
docker buildx build --load -t unicorn-store-spring:latest .
IMAGE_TAG=i$(date +%Y%m%d%H%M%S)
docker tag unicorn-store-spring:latest $ECR_URI:$IMAGE_TAG
docker tag unicorn-store-spring:latest $ECR_URI:latest
docker push $ECR_URI:$IMAGE_TAG
docker push $ECR_URI:latest
Section finished
You successfully containerized Java application and optimized the build behavior and image size. Finally, you pushed the container image to Amazon ECR. With the container image in the AWS Cloud you can now proceed with deploying your containers.