Migrate Your Legacy Monolith to Graviton5: A 3-Step Plan to 25% Performance Gains
Migrate your monolith to Graviton5 for up to 25% performance gains. This 3-step plan details moving from x86 to ARM64, building images, and phased rollout.
Your monolithic application is bleeding money on x86 compute costs, and you know it. Moving to ARM64 on AWS Graviton is the obvious answer for a quick 20-30% performance-per-dollar boost, but the migration path seems fraught with hidden dependencies and build pipeline hell. It’s not as hard as you think if you have a clear plan.
TL;DR: This post provides a 3-step, production-ready plan for migrating legacy containerized applications to AWS Graviton5. We’ll show you how to audit for architecture-specific code, build a single multi-arch container image with Docker Buildx, and execute a safe, phased rollout. Stop overpaying for x86; start migrating this week.
What you’ll walk away with:
- A checklist to audit your codebase for ARM64 compatibility issues.
- The exact
docker buildxcommand to replace your brittle, architecture-specific build scripts. - A safe deployment strategy using Kubernetes node selectors to roll out Graviton5 with zero downtime.
How Do You Find Architecture-Specific Dependencies?
First, you must identify any code that explicitly targets the x86_64 (or amd64) architecture. This includes pre-compiled binary blobs checked into your repository, dependencies installed from package managers that pull architecture-specific versions, and ancient base images in your Dockerfile. The goal is to find anything that won’t run natively on ARM64.
The most common culprits are native libraries for performance-critical tasks like image processing or cryptography. Start by searching your codebase for files that are known to be architecture-specific. A simple find command can often uncover compiled binaries you forgot about.
1
2
# Search for ELF 64-bit LSB executables, x86-64
find . -type f -exec file {} + | grep "ELF 64-bit LSB" | grep "x86-64"
Next, scrutinize your Dockerfile and any package manager manifests (package.json, requirements.txt, pom.xml). Look for dependencies that are known to have C-extensions or other native components. Check their documentation for ARM64 support; most modern, maintained libraries have it, but legacy versions often don’t. The final step is your base image. If you’re still using something ancient like ubuntu:16.04, now is the time to upgrade to a modern release that has robust multi-arch support.
Here’s your audit checklist:
-
Scan for checked-in binaries using
fileandgrep. - Review package manager dependencies for native extensions.
- Check the upstream documentation for ARM64/aarch64 support for each native dependency.
-
Upgrade your Docker base image to a recent LTS version (e.g.,
ubuntu:22.04oralpine:3.19).
Don’t boil the ocean. Focus on your direct dependencies and base image first; transitive dependencies are often handled automatically by modern package managers.
What’s the Right Way to Build a Multi-Arch Docker Image?
The correct way to build a container image for both x86 and ARM is to produce a single multi-architecture image (often called a “fat manifest”). This is a special type of image manifest that points to other, architecture-specific image layers. When a container runtime like Docker or containerd pulls the tag, it automatically selects the manifest that matches its own architecture, making the process transparent to the end-user.
The old way of handling this was a nightmare of separate Dockerfiles and complex tagging schemes.
The Painful “Before”: Manual Builds & Tagging
In the past, you might have maintained
Dockerfile.amd64andDockerfile.arm64. The build process involved two separatedocker buildcommands and a manualdocker manifestpush to stitch them together.It was brittle, error-prone, and doubled the cognitive load for maintaining your build logic. Any change had to be carefully replicated in both files. This approach does not scale.
Today, the solution is docker buildx. It’s a Docker CLI plugin that lets you build for multiple platforms with a single command from a single Dockerfile. Since Docker Desktop 4.19 (part of Docker Engine 24.0), Buildx is the default build engine, but on older systems you may need to install it.
First, ensure you have a buildx builder instance capable of cross-compilation. The default docker driver can’t do this, so you need to create and use a docker-container driver.
1
2
3
4
# Create and switch to a new builder instance
docker buildx create --name multiarch-builder --use
# Bootstrap the builder
docker buildx inspect --bootstrap
Now, you can build and push your multi-platform image in one command. Notice the --platform flag, which is the key to this whole process. We’re telling buildx to build for both linux/amd64 (standard x86) and linux/arm64 (Graviton) and push the resulting multi-arch manifest to the registry.
1
docker buildx build --platform linux/amd64,linux/arm64 -t your-repo/your-app:latest --push .
This single command replaces the entire legacy script. The --push flag is critical; buildx must push the result because the image layers for the foreign architecture (the one not matching your build machine) can’t be stored in your local Docker daemon.
This diagram shows how the resulting manifest list works. Your Kubernetes nodes, whether x86 or Graviton, pull the same tag (your-app:latest) and the runtime selects the correct image layers automatically.
flowchart TD
ImageManifest["your-app:latest<br/>(Manifest List)"]
ImageManifest -->|"references"| AMD64["Image for linux/amd64"]
ImageManifest -->|"references"| ARM64["Image for linux/arm64"]
subgraph "Container Registry"
direction LR
ImageManifest
AMD64
ARM64
end
KubeletAMD["Kubelet (x86_64 Node)"] -- "docker pull" --> ImageManifest
KubeletARM["Kubelet (Graviton Node)"] -- "docker pull" --> ImageManifest
KubeletAMD -- "selects" --> AMD64
KubeletARM -- "selects" --> ARM64
Use
docker buildxfor all new projects. There is no longer any valid reason to maintain separate Dockerfiles per architecture.
How Should You Roll Out ARM64 Workloads Safely?
A phased rollout using Kubernetes node labels and selectors is the safest way to introduce Graviton instances. This lets you schedule workloads onto the new ARM64 nodes gradually, monitor their performance and correctness, and roll back easily if you encounter issues. Don’t attempt a big-bang migration where you replace all your nodes at once.
Most cloud providers automatically label nodes with their architecture. For AWS, EKS nodes will have the label kubernetes.io/arch=arm64. You can use a nodeSelector in your Deployment or StatefulSet manifest to explicitly target these nodes.
Here’s the “before” and “after” for a typical Deployment manifest.
1
2
3
4
5
6
7
8
9
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -16,3 +16,6 @@
containers:
- name: my-monolith
image: your-repo/your-app:latest
+ spec:
+ nodeSelector:
+ kubernetes.io/arch: arm64
To execute the rollout:
- Add Graviton Nodes: Provision a new node group in your cluster using Graviton5-based instances. Your existing x86 workloads will ignore them.
-
Deploy a Canary: Deploy a copy of your application with the
nodeSelectorpointing toarm64. Direct a small amount of traffic to this canary deployment and monitor key metrics: CPU utilization, latency, and error rates. The performance gains we explored when debunking performance myths should be visible here. -
Gradual Migration: Once you’re confident in the canary, you can update your main application’s Deployment. A safe way is to add the
nodeSelectorand gradually scale up the new Graviton node pool while scaling down the old x86 one. Kubernetes will handle the rolling update gracefully.
Full Example: Canary Deployment with Graviton Node Selector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# my-app-canary-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-monolith-graviton-canary
labels:
app: my-monolith
track: canary # Differentiate from stable deployment
spec:
replicas: 1
selector:
matchLabels:
app: my-monolith
track: canary
template:
metadata:
labels:
app: my-monolith
track: canary
spec:
containers:
- name: my-monolith
image: your-repo/your-app:latest # The multi-arch image
ports:
- containerPort: 8080
# This is the key part for the rollout
nodeSelector:
kubernetes.io/arch: arm64
# Optional: Use tolerations if you've tainted your Graviton nodes
# tolerations:
# - key: "arch"
# operator: "Equal"
# value: "arm64"
# effect: "NoSchedule"
You would then configure your service mesh or ingress controller to route a percentage of traffic to pods with the track: canary label.
This methodical approach de-risks the entire migration, turning it from a scary infrastructure overhaul into a controlled, reversible operational task.
Use a canary deployment with a
nodeSelectorto validate performance and correctness on a small blast radius before committing to a full migration.
Bottom Line
Migrating to Graviton5 isn’t an academic exercise; it’s a direct path to cutting your cloud bill and improving performance. Stop maintaining separate, brittle build pipelines. Use docker buildx to create a single multi-arch image and use Kubernetes node selectors for a safe, phased rollout. The tools are mature and the path is clear.
Next up in the graviton5-perf-tuning series, we’ll dive into compiler flags and language-specific optimizations you can apply to squeeze even more performance out of your newly-migrated services.
FAQ
Will I have to rewrite my application to run on Graviton5?
Unlikely. If your application is written in an interpreted language like Python, Ruby, or Node.js, and all its dependencies are pure-language, it will likely run without any changes. The main effort is recompiling native code (C/C++, Go, Rust) and updating dependencies with native extensions.
What is the actual performance gain from Graviton5?
AWS claims up to 40% better price-performance. In practice, CPU-bound monolithic applications often see a 20-25% raw performance improvement at a ~20% lower cost, leading to significant savings. Your mileage may vary based on the specific workload.
Can I run a mixed-architecture cluster with both x86 and ARM64 nodes?
Yes, this is the recommended approach for migration. Kubernetes handles mixed-architecture clusters seamlessly, as long as your container images are multi-arch. The scheduler will place pods on appropriate nodes based on architecture.
What if a critical dependency doesn’t support ARM64?
This is the main blocker. First, check for newer versions of the library, as support may have been added recently. If not, you must either find an alternative library, contribute ARM64 support upstream, or keep that specific service on x86 for the time being.
Does docker buildx work with my CI/CD provider?
Yes. Most modern CI/CD platforms like GitHub Actions, GitLab CI, and CircleCI have native support for setting up and using docker buildx. They typically use QEMU to emulate the foreign architecture, making cross-compilation straightforward in your pipeline.
Part of the series: graviton5-perf-tuning
- Is Graviton5 Really Faster for Your Workloads? Debunking Performance Myths
- Migrate Your Legacy Monolith to Graviton5: A 3-Step Plan to 25% Performance Gains (you are here)
Further Reading
- https://aws.amazon.com/ec2/graviton/getting-started/
- https://docs.docker.com/build/building/multi-platform/
🚀 Ready to get hands-on? Spin up an interactive AI or Kubernetes Sandbox at Aicademy Labs for free.
