Amazon Graviton Three Ways

2024-01-16
This post cover image
Voice provided by Amazon Polly

The Frugal Architect, was introduced by Dr. Werner Vogels during re:Invent 2023. It consists of several laws for building cost efficient architectures in the cloud. This is where the Graviton (ARM based) CPU from AWS comes in. Graviton delivers the best cost vs performance, most workloads can utilize Graviton with very small modifications. Not all, but many workloads can. Switching to Graviton is often an easy way to save on cost without sacrificing performance. I have helped migrate several workloads to Graviton over the last couple of years, on average the cost has been decreased with 20-25%.

In this post we'll dive into Graviton and I'll show how run Graviton for three different workloads. We'll look at a AWS Lambda based workload written in Python, one written in Golang, and finally a container based Java Spring Boot application running in Fargate. I'll show how to move the existing workloads from X86 to Graviton. I will build and deploy everything from a Macbook with Apple silicone, but I will show the techniques for building and deploying from anywhere, including CI/CD tools like GitHub Actions.

Introduction

The transition to Amazon Graviton for the three workloads represents a strategic move to exploit Graviton's strengths, such as lower power consumption, better price-performance ratio, and enhanced processing capabilities. For developers and enterprises, understanding how to optimize their applications for Graviton is crucial, as it could lead to substantial performance gains and cost savings. This introduction to Graviton, tailored to these three specific AWS Cloud workloads, paves the way for a more in-depth exploration of the practicalities and benefits of this migration.

Prerequisites

Before we start there are a couple of things that must be installed. Make sure you have Python, Golang, Java, Docker, AWS CLI with AWS SAM CLI installed.

Architecture Overview

First of all, let us do an overview of the architecture for the three different workloads, what components that are involved and how the workloads are deployed. We'll create a very basic Hello World API, yes yes I know it's boring, for all three workloads. The Lambda based will utilize API Gateway and the Container based Java workload will use an Application Load Balancer.

Image showing architecture overview.

Python based Lambda function

First of all, let's build and deploy the API using SAM CLI and run them on the standard X86 based CPU.

Deploy and run Python Lambda on X86

Creating an API backed by a Lambda function using SAM is straight forward, we deploy a template with an HttpApi and a function integration.


AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Create a HTTP API with Graviton based Lambda function in Python

Parameters:
Application:
Type: String
Description: Name of the Application owning this Stack Resources

Globals:
Function:
Timeout: 5
Runtime: python3.12

Resources:
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowMethods:
- GET
AllowOrigins:
- "*"
AllowHeaders:
- "*"
Tags:
Application: !Ref Application

HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: hello-world.handler
Events:
HelloGet:
Type: HttpApi
Properties:
Path: /hello
Method: get
ApiId: !Ref HttpApi

The SAM config file I use looks like this, it configures the stack name and region, and sets the template parameters.

version: 0.1
default:
global:
parameters:
stack_name: http-api-lambda-python-graviton-tutorial
region: eu-north-1
resolve_s3: true
confirm_changeset: false
fail_on_empty_changeset: false
capabilities: CAPABILITY_NAMED_IAM
deploy:
parameters:
parameter_overrides:
- Application=graviton-tutorial

The Python code is really simple and just return a status 200 and a message.

def handler(event, context):
return {"statusCode": 200, "body": "Hello World!"}

To deploy the template we use SAM CLI command


sam deploy --config-env default

From the stack output we can grab the API endpoint and test that we get a proper response back, we need to append /hello to the endpoint since that is the path we set in the integration.

Image showing python based API response.

If we then head over to the AWS Console we can verify that the function is running on X86 based CPU.

Image showing the function running on X86 in the console.

Migrate Python Lambda to Graviton

The migration to Graviton for Python is as simple as flipping a switch, or in our case specify a different architecture. So for the Lambda function we just specify arm64 as the architecture.


HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
+ Architectures:
+ - arm64
CodeUri: src/
Handler: hello-world.handler
Events:
HelloGet:
Type: HttpApi
Properties:
Path: /hello
Method: get
ApiId: !Ref HttpApi

Then we just deploy once again using SAM.


sam deploy --config-env default

After the deployment we can once again head over to the Console and check the function architecture. We can clearly see that we have switched to arm64 (Graviton).

Image showing the function running on Arm in the console.

With that, we have migrated our function to Graviton by flipping a switch.

Golang based Lambda function

Running a Golang Lambda function is a bit different, first of all there is no managed runtime for Golang that we can use with Graviton. We need to use the Amazon Linux based provided.al2 runtime.

Deploy and run Golang Lambda on X86

When running on the provided.al2 runtime our handler need to be named bootstrap, which we also declare in our template. We also need to specify our build method in the meta-data section of the function. I will be using Make file when building. The Make file is not that complex for this function. We create a section to build it and add the command for building with an amd64 architecture.

build-HelloWorldFunction:
GOOS=linux GOARCH=amd64 go build -o bootstrap main.go
cp ./bootstrap $(ARTIFACTS_DIR)/.

The template is similar to the python variant, with the modifications to add the build method.

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Create a HTTP API with Graviton based Lambda function in Golang

Parameters:
Application:
Type: String
Description: Name of the Application owning this Stack Resources

Globals:
Function:
Timeout: 5
MemorySize: 512

Resources:
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowMethods:
- GET
AllowOrigins:
- "*"
AllowHeaders:
- "*"
Tags:
Application: !Ref Application

HelloWorldFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
CodeUri: hello-world/
Handler: bootstrap
Runtime: provided.al2
Events:
HelloGet:
Type: HttpApi
Properties:
Path: /hello
Method: get
ApiId: !Ref HttpApi

Outputs:
ApiEndpoint:
Description: HTTP API endpoint URL
Value: !Sub https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com

The SAM config file I use looks like this, it configures the stack name and region, and sets the template parameters.


version: 0.1
default:
global:
parameters:
stack_name: http-api-lambda-golang-graviton-tutorial
region: eu-north-1
resolve_s3: true
confirm_changeset: false
fail_on_empty_changeset: false
capabilities: CAPABILITY_NAMED_IAM
deploy:
parameters:
parameter_overrides:
- Application=graviton-tutorial

Now when building this function we can't just run the sam deploy command, like we did with Python. We also need to run the actual build phase in this case. So we end up with a build followed by a deploy command.


sam build

sam deploy --config-env default

When the stack has been deployed, it does take a few seconds longer to build and deploy Golang than it did with Python. Once again we grab the API endpoint, from the stack output, and test that we get a proper response back, we need to append /hello to the endpoint since that is the path we set in the integration.

Image showing python based API response.

If we then head over to the AWS Console we can verify that the function is running on X86 based CPU. We can also see that our handler is bootstrap and that we run on the custom Amazon Linux runtime.

Image showing the function running on X86 in the console.

Migrate Golang Lambda to Graviton

The migration to Graviton for Golang is not as simple as it was for Python, there are a few extra steps, but it's still fairly straight forward.

First of all we need to update our Make file so we build for arm based CPU instead of the amd64 we did before, we set the GOARCH to arm64.

build-HelloWorldFunction:
GOOS=linux GOARCH=arm64 go build -o bootstrap main.go
cp ./bootstrap $(ARTIFACTS_DIR)/.

We also update the template and set the Architectures to arm64.


HelloWorldFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
+ Architectures:
+ - arm64
CodeUri: hello-world/
Handler: bootstrap
Runtime: provided.al2
Events:
HelloGet:
Type: HttpApi
Properties:
Path: /hello
Method: get
ApiId: !Ref HttpApi

Now, let's build and deploy using SAM once again.


sam build

sam deploy --config-env default

After the deployment we can once again head over to the Console and check the function architecture. We can clearly see that we have switched to arm64 (Graviton) and that we still on the Amazon Linux 2 Runtime.

Image showing the function running on Arm in the console.

With that, we have migrated our function to Graviton. A few extra steps, making sure we build for arm, but still a straight forward process.

Java based container

With the two Lambda based workloads migrated, let's start looking at a Java based container workload running in Fargate. Before we start we need to deploy required resources as VPC, ECR repository, and more.

To setup the needed infrastructure and service running in ECS follow my blog post: Run a java service serverless with ECS and Fargate. I will assume you have a ECS cluster setup according to that guide.

Navigate to the ECS Cluster, in the services tab click on the service name, select Tasks tab and click the task ID. Confirm that you have a configuration that looks like this.

Image showing the ECS task runnig x86.

To start migrating this Java based service to Graviton, we first of all need to build the Docker image for ARM.

./gradlew clean build -Pversion=1234

aws ecr get-login-password --region eu-north-1 | docker login -u AWS --password-stdin ACCOUNT.dkr.ecr.eu-north-1.amazonaws.com
IMAGE="ACCOUNT.dkr.ecr.eu-north-1.amazonaws.com/graviton-tutorial-hello:latest"
docker buildx build --platform linux/arm64 --push -t $IMAGE .

We need to set the platform to linux/arm64 to build for Graviton. After pushing the new image to ECR we have to update the TaskDefinition and change CpuArchitecture to ARM64.

...
ServiceTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub ${Application}-${Service}-task
RequiresCompatibilities:
- FARGATE
NetworkMode: awsvpc
ExecutionRoleArn:
Fn::ImportValue: !Sub ${ServiceInfraStackName}:cluster-role
TaskRoleArn: !Sub ${Application}-${Service}-task-role
+ RuntimePlatform:
+ CpuArchitecture: ARM64
Cpu: 512
Memory: 1024
ContainerDefinitions:
- Name: !Ref Service
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Application}-${Service}:${ServiceTag}
Cpu: 512
Memory: 1024
Environment:
- Name: NAME
Value: !Ref Service
PortMappings:
- ContainerPort: 8080
Protocol: tcp
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Sub /ecs/${Application}/${Service}
awslogs-region: !Sub ${AWS::Region}
awslogs-stream-prefix: !Ref Service
Tags:
- Key: Name
Value: !Sub ${Application}-${Service}-task
...

After that change and a redeploy we should now end up with a new Task running in our service with this configuration.

Image showing the ECS task running ARM.

And that is basically it, we have migrated our service to Graviton. Now, not all Java services are this easy but most that I have worked with actually are.

Cost saving

As mentioned in the beginning I have migrated several workloads to Graviton and the data I have collected show that you can save between 20 - 25% on cost. If we look at our basic example running 0.5vCPU and 1Gb of memory and running this container around the clock the cost would look something like this in the eu-north-1 (Stockholm) Region.

X86 Cost

vCPU cost is $0.04048 per hour, memory cost is $0.004445 per GB per hour. Calculating with 720 hours per months.

vCPU: 0.04048 * 0.5 * 720 = ~$16 memory: 0.004445 * 1 * 720 = ~$3.5 total: ~19.5

Graviton Cost

vCPU cost is $0.03238 per hour, memory cost is $0.00356 per GB per hour. Calculating with 720 hours per months.

vCPU: 0.03238 * 0.5 * 720 = ~$11.7 memory: 0.00356 * 1 * 720 = ~$2.6 total: ~14.5

This is a cost saving of ~$5 or 25%!

Graviton also has a lower carbon footprint, so not only is this good for your cost, it's also good for the environment!

Final Words

In this post I showed how to migrate three different workloads from X86 to Graviton based compute. Now, not all workloads are this easy. However, after doing several migrations myself I can say that most that I have worked on actually are. You can also check out the repo aws-graviton-getting-started from AWS for more tips and trix.

Don't forget to follow me on LinkedIn and X for more content, and read rest of my Blogs