CI/CD โ CircleCI
CircleCI offers cloud-native continuous integration with Docker-first architecture, orbs, and powerful caching. This guide covers configuration and migration patterns based on production deployments for Samsung's microservices fleet.
CircleCI Architecture
CircleCI operates on a cloud-native architecture with two execution models:
| Aspect | Cloud | Self-Hosted (Server) |
|---|---|---|
| Infrastructure | Fully managed by CircleCI | Runs in your data center or cloud |
| Scaling | Automatic, pay-per-use | You manage compute resources |
| Execution environments | Docker, Linux VM, macOS, Windows, GPU, ARM | Docker, Linux VM, macOS (via runners) |
| Security | Shared infrastructure, isolated jobs | Full control over build environment |
| Pricing | Free tier + usage-based credits | License + infrastructure cost |
| Setup time | Minutes (connect GitHub) | Days (install and configure) |
CircleCI Cloud is recommended for most teams. Self-hosted Server is appropriate for organizations with strict data residency requirements or specialized hardware needs.
.circleci/config.yml Structure
CircleCI configuration uses a hierarchical YAML structure:
| Component | Description |
|---|---|
version | Config schema version (always use 2.1) |
orbs | Reusable packages (AWS, Kubernetes, Slack, etc.) |
executors | Reusable execution environments |
commands | Reusable command sequences |
jobs | Units of work composed of steps |
workflows | Orchestration of jobs with dependencies |
Complete Config Example: Node.js with Docker
# .circleci/config.yml
version: 2.1
# โโ Orbs: Reusable packages โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
orbs:
aws-ecr: circleci/aws-ecr@9.0
aws-eks: circleci/aws-eks@2.1
slack: circleci/slack@4.12
node: circleci/node@5.2
sonarcloud: sonarsource/sonarcloud@2.0
# โโ Executors: Reusable environments โโโโโโโโโโโโโโโโโโโโโ
executors:
node-executor:
docker:
- image: cimg/node:20.10
resource_class: medium
working_directory: ~/project
docker-executor:
docker:
- image: cimg/base:stable
resource_class: medium
# โโ Commands: Reusable step sequences โโโโโโโโโโโโโโโโโโโโ
commands:
setup-node:
steps:
- node/install:
node-version: '20.10'
install-yarn: false
- restore_cache:
keys:
- node-deps-v1-{{ checksum "package-lock.json" }}
- node-deps-v1-
- run:
name: Install dependencies
command: npm ci
- save_cache:
key: node-deps-v1-{{ checksum "package-lock.json" }}
paths:
- ~/.npm
notify-slack:
parameters:
event:
type: enum
enum: [pass, fail]
default: pass
template:
type: string
default: basic_success_1
steps:
- slack/notify:
event: << parameters.event >>
template: << parameters.template >>
channel: C1234567890
# โโ Jobs: Units of work โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
jobs:
lint-and-unit-test:
executor: node-executor
steps:
- checkout
- setup-node
- run:
name: Run ESLint
command: npm run lint
- run:
name: Run unit tests
command: npm run test:unit -- --coverage --reporters=jest-junit
environment:
JEST_JUNIT_OUTPUT_DIR: ./reports/junit
- store_test_results:
path: ./reports/junit
- store_artifacts:
path: ./reports/junit
- persist_to_workspace:
root: .
paths:
- .
integration-test:
executor: node-executor
steps:
- checkout
- setup-node
- run:
name: Start test database
command: docker-compose -f docker-compose.test.yml up -d
- run:
name: Run integration tests
command: |
npx wait-on tcp:localhost:5432 --timeout 30000
npm run test:integration
- store_test_results:
path: ./reports/junit
security-scan:
executor: docker-executor
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run:
name: Build image for scanning
command: docker build -t app:scan .
- run:
name: Run Trivy vulnerability scan
command: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image --severity HIGH,CRITICAL --exit-code 1 app:scan
build-and-push:
executor: docker-executor
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
- aws-ecr/ecr-login:
region: ${AWS_REGION}
- run:
name: Build and push image
command: |
IMAGE_TAG="sha-$(echo ${CIRCLE_SHA1} | cut -c1-7)"
docker build \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--build-arg VCS_REF=${CIRCLE_SHA1} \
-t ${ECR_REGISTRY}/${CIRCLE_PROJECT_REPONAME}:${IMAGE_TAG} \
-t ${ECR_REGISTRY}/${CIRCLE_PROJECT_REPONAME}:latest \
.
docker push ${ECR_REGISTRY}/${CIRCLE_PROJECT_REPONAME}:${IMAGE_TAG}
docker push ${ECR_REGISTRY}/${CIRCLE_PROJECT_REPONAME}:latest
echo "export IMAGE_TAG=${IMAGE_TAG}" >> workspace/env_vars
- persist_to_workspace:
root: workspace
paths:
- env_vars
deploy:
executor: aws-eks/default
parameters:
environment:
type: string
cluster_name:
type: string
steps:
- attach_workspace:
at: workspace
- run:
name: Load environment variables
command: cat workspace/env_vars >> $BASH_ENV
- aws-eks/update-kubeconfig-with-authenticator:
cluster-name: << parameters.cluster_name >>
aws-region: ${AWS_REGION}
- run:
name: Deploy with Helm
command: |
helm upgrade --install ${CIRCLE_PROJECT_REPONAME} ./helm-chart \
--namespace << parameters.environment >> \
--set image.tag=${IMAGE_TAG} \
--set environment=<< parameters.environment >> \
--wait --timeout 300s
- run:
name: Verify deployment
command: |
kubectl rollout status deployment/${CIRCLE_PROJECT_REPONAME} \
--namespace << parameters.environment >> --timeout=120s
# โโ Workflows: Job orchestration โโโโโโโโโโโโโโโโโโโโโโโโโ
workflows:
version: 2
ci-cd:
jobs:
- lint-and-unit-test:
filters:
branches:
only: [main, develop]
tags:
only: /^v.*/
- integration-test:
requires: [lint-and-unit-test]
filters:
branches:
only: [main, develop]
- security-scan:
requires: [lint-and-unit-test]
filters:
branches:
only: [main, develop]
- build-and-push:
context: aws-credentials
requires: [integration-test, security-scan]
filters:
branches:
only: [main, develop]
tags:
only: /^v.*/
- deploy-staging:
name: deploy-staging
environment: staging
cluster_name: staging-cluster
context: aws-credentials
requires: [build-and-push]
filters:
branches:
only: [main]
- deploy-production:
name: deploy-production
environment: production
cluster_name: production-cluster
context: aws-credentials
requires: [build-and-push]
filters:
branches:
ignore: /.*/
tags:
only: /^v.*/
post-steps:
- notify-slack:
event: pass
template: basic_success_1
# Nightly security scan
nightly:
triggers:
- schedule:
cron: "0 2 * * *"
filters:
branches:
only: [main]
jobs:
- security-scan
Orbs: Reusable Packages
Orbs are shareable packages of CircleCI configuration. They encapsulate commands, jobs, and executors that can be reused across projects:
| Orb | Publisher | Purpose |
|---|---|---|
circleci/aws-ecr | CircleCI | Build, tag, and push images to ECR |
circleci/aws-cli | CircleCI | Install and configure AWS CLI |
circleci/aws-eks | CircleCI | EKS cluster operations and kubectl |
circleci/kubernetes | CircleCI | Deploy to any Kubernetes cluster |
circleci/slack | CircleCI | Send Slack notifications |
circleci/node | CircleCI | Node.js installation and caching |
circleci/docker | CircleCI | Docker build/push utilities |
sonarsource/sonarcloud | SonarSource | Code quality and security scanning |
snyk/snyk | Snyk | Vulnerability scanning |
Creating a Custom Orb
# Register a namespace (once per organization)
circleci namespace create my-org --provider github
# Create the orb
circleci orb create my-org/build-helpers
# Initialize orb development
circleci orb init ./my-orb --private
# Orb source structure (./my-orb/src/)
# src/
# commands/
# setup-node.yml
# notify-deploy.yml
# jobs/
# build-and-test.yml
# executors/
# node.yml
# @orb.yml
# Publish development version
circleci orb publish ./my-orb/src/@orb.yml my-org/build-helpers@dev:alpha
# Publish production version
circleci orb publish promote my-org/build-helpers@dev:alpha patch
Workspaces and Caching Strategies
CircleCI provides three mechanisms for persisting data across jobs:
| Mechanism | Scope | Use Case | Lifetime |
|---|---|---|---|
cache | Project-wide (key-based) | Dependency caches (node_modules, ~/.m2) | 15-30 days (configurable) |
workspace | Single workflow | Passing artifacts between jobs | Duration of workflow |
artifacts | Project-wide, post-build | Test reports, build outputs for download | 30 days |
# Caching dependencies (best practice)
- restore_cache:
keys:
- v2-deps-{{ .Branch }}-{{ checksum "package-lock.json" }}
- v2-deps-{{ .Branch }}-
- v2-deps-
- run: npm ci
- save_cache:
key: v2-deps-{{ .Branch }}-{{ checksum "package-lock.json" }}
paths:
- ~/.npm
- node_modules
# Persisting workspace between jobs
- persist_to_workspace:
root: .
paths:
- dist/
- coverage/
- env_vars
# Attaching workspace in downstream job
- attach_workspace:
at: .
# Storing artifacts
- store_artifacts:
path: coverage/
destination: coverage-report
- store_test_results:
path: test-results/junit
Matrix Jobs and Parallelism
Matrix Builds
jobs:
test:
parameters:
node-version:
type: string
os:
type: string
docker:
- image: cimg/node:<< parameters.node-version >>
steps:
- checkout
- run: npm ci
- run: npm test
workflows:
test-matrix:
jobs:
- test:
matrix:
parameters:
node-version: ["18.20", "20.10", "21.5"]
os: ["linux"]
# Generates 3 parallel jobs: (18.20, linux), (20.10, linux), (21.5, linux)
Test Parallelism
jobs:
e2e-tests:
parallelism: 4 # Split tests across 4 containers
executor: node-executor
steps:
- checkout
- setup-node
- run:
name: Run E2E tests (split by timing)
command: |
# CircleCI automatically sets TEST_FILES based on test splitting
npx jest --testPathPattern="e2e" \
$(circleci tests glob "e2e/**/*.test.js" | circleci tests split --split-by=timings)
- store_test_results:
path: reports/junit
Migration from CircleCI to GitHub Actions
The following mapping guides migration from CircleCI to GitHub Actions:
| CircleCI Concept | GitHub Actions Equivalent |
|---|---|
.circleci/config.yml | .github/workflows/*.yml |
| Orbs | Actions from GitHub Marketplace |
| Executors | runs-on with optional container |
| Commands | Composite actions |
| Jobs | Jobs |
| Workflows | Workflows |
Workspaces (persist_to_workspace) | upload-artifact / download-artifact |
Cache (restore_cache / save_cache) | actions/cache or built-in setup caching |
| Contexts | Repository/organization secrets + environments |
setup_remote_docker | Multi-step or container-based jobs |
resource_class | runs-on with self-hosted runner labels |
store_test_results | No direct equivalent (test results in logs) |
filters (branches/tags) | on.push.branches / on.push.tags |
schedule trigger | on.schedule with cron |
matrix | strategy.matrix |
parallelism with test splitting | Matrix jobs or custom test splitting |
when / unless | if conditionals |
Context Migration
CircleCI contexts (collections of environment variables) map to GitHub Actions secrets at different scopes:
# CircleCI: contexts in workflows
jobs:
deploy:
context: aws-production-credentials
# GitHub Actions: repository secrets + environments
jobs:
deploy:
environment: production # Uses production environment secrets
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
# Or use OIDC (recommended):
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
Best Practices
| Practice | Implementation |
|---|---|
| Use orbs for common operations | Leverage official orbs rather than shell scripts |
| Layer cache keys | Use fallback keys (checksum โ branch โ generic) |
| Workspace for inter-job data | Pass build artifacts, not dependencies |
| Docker layer caching | Enable DLC for Docker-based builds |
| Resource classes | Right-size: small for lint, medium+ for build/test |
| Filter branches and tags precisely | Avoid running unnecessary jobs |
| Store test results | Always use store_test_results for JUnit output |
| Contexts for credential grouping | Group related secrets into contexts |
| Restrict contexts to protected branches | Require branch filters on sensitive contexts |
| Use project settings for secrets | Avoid hardcoding values in config.yml |