41 pages ยท 8 sections
Ctrl K
GitHub Portfolio

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:

AspectCloudSelf-Hosted (Server)
InfrastructureFully managed by CircleCIRuns in your data center or cloud
ScalingAutomatic, pay-per-useYou manage compute resources
Execution environmentsDocker, Linux VM, macOS, Windows, GPU, ARMDocker, Linux VM, macOS (via runners)
SecurityShared infrastructure, isolated jobsFull control over build environment
PricingFree tier + usage-based creditsLicense + infrastructure cost
Setup timeMinutes (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:

ComponentDescription
versionConfig schema version (always use 2.1)
orbsReusable packages (AWS, Kubernetes, Slack, etc.)
executorsReusable execution environments
commandsReusable command sequences
jobsUnits of work composed of steps
workflowsOrchestration 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:

OrbPublisherPurpose
circleci/aws-ecrCircleCIBuild, tag, and push images to ECR
circleci/aws-cliCircleCIInstall and configure AWS CLI
circleci/aws-eksCircleCIEKS cluster operations and kubectl
circleci/kubernetesCircleCIDeploy to any Kubernetes cluster
circleci/slackCircleCISend Slack notifications
circleci/nodeCircleCINode.js installation and caching
circleci/dockerCircleCIDocker build/push utilities
sonarsource/sonarcloudSonarSourceCode quality and security scanning
snyk/snykSnykVulnerability 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:

MechanismScopeUse CaseLifetime
cacheProject-wide (key-based)Dependency caches (node_modules, ~/.m2)15-30 days (configurable)
workspaceSingle workflowPassing artifacts between jobsDuration of workflow
artifactsProject-wide, post-buildTest reports, build outputs for download30 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 ConceptGitHub Actions Equivalent
.circleci/config.yml.github/workflows/*.yml
OrbsActions from GitHub Marketplace
Executorsruns-on with optional container
CommandsComposite actions
JobsJobs
WorkflowsWorkflows
Workspaces (persist_to_workspace)upload-artifact / download-artifact
Cache (restore_cache / save_cache)actions/cache or built-in setup caching
ContextsRepository/organization secrets + environments
setup_remote_dockerMulti-step or container-based jobs
resource_classruns-on with self-hosted runner labels
store_test_resultsNo direct equivalent (test results in logs)
filters (branches/tags)on.push.branches / on.push.tags
schedule triggeron.schedule with cron
matrixstrategy.matrix
parallelism with test splittingMatrix jobs or custom test splitting
when / unlessif 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
Tip: When migrating from CircleCI, map CircleCI contexts to GitHub Environments with protection rules. This gives you required reviewers, deployment branches, and wait timers for production deployments, which is more powerful than CircleCI's context-level security.

Best Practices

PracticeImplementation
Use orbs for common operationsLeverage official orbs rather than shell scripts
Layer cache keysUse fallback keys (checksum โ†’ branch โ†’ generic)
Workspace for inter-job dataPass build artifacts, not dependencies
Docker layer cachingEnable DLC for Docker-based builds
Resource classesRight-size: small for lint, medium+ for build/test
Filter branches and tags preciselyAvoid running unnecessary jobs
Store test resultsAlways use store_test_results for JUnit output
Contexts for credential groupingGroup related secrets into contexts
Restrict contexts to protected branchesRequire branch filters on sensitive contexts
Use project settings for secretsAvoid hardcoding values in config.yml