CircleCI’s ARM resource classes let you run your CI jobs on ARM processors, unlocking faster builds and lower costs for ARM-native workloads.
Here’s a quick look at how it works in a config.yml:
version: 2.1
jobs:
build_and_test:
docker:
- image: cimg/node:16.14.0
resource_class: arm.medium # <-- This is the key
steps:
- checkout
- run:
name: Build
command: npm install && npm run build
- run:
name: Test
command: npm test
deploy:
docker:
- image: cimg/node:16.14.0
resource_class: medium
steps:
- checkout
- run:
name: Deploy to production
command: echo "Deploying..."
workflows:
version: 2
build_and_deploy:
jobs:
- build_and_test
- deploy:
requires:
- build_and_test
filters:
branches:
only: main
The resource_class: arm.medium line tells CircleCI to provision an ARM-based runner for this specific job. You can choose from arm.medium, arm.large, and arm.xlarge, each offering different CPU and memory configurations.
The Problem: X86 Lock-in and Its Costs
For a long time, CI/CD pipelines were almost exclusively run on x86 (Intel/AMD) architecture. This meant that if your application or its dependencies were ARM-native – like many IoT devices, mobile applications (especially Android), or modern server workloads running on AWS Graviton or Apple Silicon – you were still compiling, testing, and deploying on potentially slower, more expensive x86 hardware. This often involved cross-compilation, which can be complex and might not always catch architecture-specific bugs.
How ARM Resource Classes Solve It
CircleCI’s ARM resource classes directly address this by providing native ARM execution environments. Instead of emulating or cross-compiling, your code runs on actual ARM processors. This leads to:
- Faster Builds: ARM processors can be significantly faster for ARM-native workloads.
- Cost Savings: ARM instances are often more cost-effective per unit of performance, especially for compute-intensive tasks.
- Architecture-Specific Testing: You can run tests on the same architecture your application will eventually run on, catching potential issues that cross-compilation might miss.
- Simplified Toolchains: You can often use standard ARM toolchains without complex cross-compilation setups.
The Mental Model: Resource Classes as a Fleet
Think of CircleCI’s resource classes like a fleet of different types of vehicles you can rent for your jobs.
medium: A standard sedan. Good for general tasks.large: A small truck. More power, more cargo space (CPU/RAM).xlarge: A large truck. For the heaviest loads.
And now, you have two distinct fleets:
- x86 Fleet: The traditional fleet you’ve always used.
- ARM Fleet: A new fleet of vehicles specifically designed for ARM.
When you specify resource_class: medium (without arm.), you get an x86 sedan. When you specify resource_class: arm.medium, you get an ARM sedan. You can mix and match these fleets within the same config.yml file, using the right vehicle for the right job.
Configuration Deep Dive
Choosing the Right resource_class:
The choice depends on your job’s needs:
arm.medium: 2 vCPU, 4 GB RAMarm.large: 4 vCPU, 8 GB RAMarm.xlarge: 8 vCPU, 16 GB RAM
You’ll select one based on the typical resource consumption of your build and test steps. It’s often a good idea to start with arm.medium and scale up if your builds are consistently hitting resource limits.
Docker Images:
The Docker images you use should ideally be ARM-compatible. Many official images (like those from cimg, node, python, golang) now provide multi-architecture support, meaning docker pull cimg/node:16.14.0 will automatically pull the correct ARM variant when running on an ARM runner. If you’re using a custom image, ensure it’s built for ARM64 (aarch64).
Example with a Different Language (Go):
version: 2.1
jobs:
build_arm_binary:
docker:
- image: cimg/go:1.18.3 # cimg/go images are multi-arch
resource_class: arm.large
steps:
- checkout
- run:
name: Build Go binary for ARM
command: GOOS=linux GOARCH=arm64 go build -o myapp_arm .
- persist_to_workspace:
root: .
paths:
- myapp_arm
workflows:
version: 2
build_workflow:
jobs:
- build_arm_binary
In this Go example, GOOS=linux GOARCH=arm64 explicitly tells the Go compiler to produce an ARM64 binary. Running this on an arm.large resource class ensures the compilation itself happens on an ARM host, not emulated.
The "Gotcha": Caching and Dependencies
When you switch to ARM runners, your Docker image cache and any dependency caches (like npm modules or pip packages) are built and stored separately from their x86 counterparts. If you have existing caches configured for your jobs, you’ll need to be mindful that switching to an arm. resource class will likely result in a cache miss initially, requiring a fresh download and build of dependencies. This is generally a good thing, as it ensures your ARM dependencies are correctly installed.
However, if you also run x86 builds for other parts of your system, you might want to use different cache keys or paths for your ARM builds to prevent accidental cross-contamination. For instance, you might append _arm to your cache directory name.
This is why you might encounter a "no cache found" warning or a longer initial build time when first switching to ARM resource classes.
The next logical step after optimizing your builds is to consider optimizing your deployment strategy for ARM-based environments.