CI/CD 시스템을 구축하는 게 얼마나 어려울까?

발행: (2026년 5월 29일 AM 07:31 GMT+9)
8 분 소요
원문: Dev.to

출처: Dev.to

CI/CD 시스템을 만드는 것이 얼마나 어려울까?

그 질문이 내 머릿속에 오래 남아 있었고, 결국 직접 만들게 되었다. 누군가 시킨 것이 아니다. 시장의 빈틈을 발견해서도 아니다. 그저 그 질문이 사라지지 않았기 때문이다.

계기가 된 것은 Concourse CI였다. 꽤 오래 사용해 왔는데, 내가 가장 마음에 들어 하는 점은 리소스 추상화—외부와 통신하기 위해 따라야 하는 인터페이스: 새 버전을 확인하고, 가져오고, 다시 푸시한다. Go 개발자로서 이런 깔끔한 인터페이스는 큰 매력이다. 파이프라인에 있는 모든 것은 그 계약을 구현하는 무언가일 뿐이다.

하지만 운영 오버헤드는 상당했다. 또한 GitHub Actions가 제공하지 못하는 맞춤형 환경이 필요한 내 사이드 프로젝트(게임 및 오픈소스 도구)에도 CI가 필요했다. 그래서 PikoCI를 만들기 시작했다.


수평 확장이 가능한 단일 바이너리

./pikoci server \
  --db-system mem \
  --pubsub-system mem \
  --run-worker \
  --pipeline-config pipeline.hcl

이 한 줄이 완전한 CI/CD 시스템이다:

  • 기본은 메모리—파일도 없고 외부 서비스도 없다.
  • --db-system sqlite 로 영속성을 추가한다.
  • NATS 로 분산 워커를 추가하고 다른 머신에서 워커를 실행한다.
  • 파이프라인 설정은 절대 변하지 않는다.

네 가지 플러그인 가능한 추상화

PikoCI는 HCL에 정의하고 URL 로 가져올 수 있는 네 가지 개념을 제공한다:

  1. 리소스 타입 – 무언가의 변화를 감시하고 가져오는 방법.
  2. 러너 타입 – 작업이 실행되는 위치.
  3. 시크릿 타입 – 자격 증명이 어디서 오는지.
  4. 서비스 타입 – 작업과 함께 실행되는 프로세스.

각각은 동일한 패턴을 따른다: 타입을 한 번 정의하고, 파라미터로 인스턴스화한다.

1. 리소스 타입

resource_type "git" {
  source = "pikoci://git"  # built‑in
}

resource "git" "my-app" {
  params {
    url  = "https://github.com/org/app"
    name = "app"
  }
  check_interval = "@every 1m"
}

2. 러너 타입

dockerexec 러너는 기본 제공이므로 선언할 필요가 없다.
아래는 Docker 러너가 내부적으로 어떻게 생겼는지 보여준다(직접 정의하고 싶을 때 참고).

# 이것이 pikoci://docker 의 모습이다
# 직접 정의해서 커스터마이즈하거나 교체할 수 있다
runner_type "docker" {
  run {
    path = "docker"
    args = [
      "run", "--rm",
      "-v", "$WORKDIR:/workdir",
      "-w", "/workdir",
      "$image",
      "/bin/sh", "-ec", "$cmd",
    ]
  }
}

Docker 러너를 사용하는 예시 잡:

job "test" {
  get "git" "my-app" { trigger = true }
  task "run-tests" {
    run "docker" {
      image = "golang:1.25"
      cmd   = "cd app && make test"
      args  = ["-v", "/cache/go:/root/go/pkg/mod"]
    }
  }
}

3. 시크릿 타입

secret_type "vault" {
  source = "pikoci://vault"
}

variable "db_password" {
  secret "vault" {
    path = "secret/data/db"
    key  = "password"
  }
}

시크릿은 변수에 바인딩되며 파이프라인 어디서든 참조할 수 있다.

4. 서비스 타입

서비스는 작업과 함께 실행되며, 잡이 시작되기 전에 시작하고 끝난 뒤에 중지된다. 결과와 무관하게 보장된다. 내가 가장 자랑스러워하는 기능이다:

service_type "postgres" {
  # source = "pikoci://postgres" — 혹은 인라인 정의
  start "exec" {
    path = "/bin/sh"
    args = ["-ec", "docker run -d --name db -p 5432:5432 postgres:16"]
  }
  ready_check "exec" {
    path    = "/bin/sh"
    args    = ["-ec", "pg_isready -h localhost"]
    timeout = "30s"
  }
  stop "exec" {
    path = "/bin/sh"
    args = ["-ec", "docker rm -f db"]
  }
}

잡에서 서비스 사용하기:

job "integration" {
  service "postgres" {}
  get "git" "my-app" { trigger = true }
  task "test" {
    run "exec" {
      path = "make"
      args = ["integration-test"]
    }
  }
}

Docker‑in‑Docker도 없고, CI 옆에 docker‑compose도 없다. 서비스는 잡이 성공하든 실패하든 무조건 중지된다.

네 가지 타입 모두 URL 로 가져올 수 있다. 기본 제공 타입은 pikoci:// 를 사용하고, 직접 호스팅하는 어떤 것이든 같은 메커니즘으로 참조한다. Kubernetes 나 Azure 에서 작업을 실행하는 러너가 필요하면? 한 번 구현하고, 어디에든 호스팅한 뒤 URL 로 지정하면 된다.


네 가지 추상화를 모두 활용한 전체 파이프라인

resource_type "git" {
  source = "pikoci://git"
}

resource "git" "my-app" {
  params {
    url  = "https://github.com/org/app"
    name = "app"
  }
  check_interval = "@every 1m"
}

secret_type "vault" {
  source = "pikoci://vault"
}

variable "db_password" {
  secret "vault" {
    path = "secret/data/db"
    key  = "password"
  }
}

service_type "postgresql" {
  source = "pikoci://postgresql"
}

job "test" {
  get "git" "my-app" { trigger = true }
  service "postgresql" {
    version  = "17"
    port     = "5432"
    password = var.db_password
  }
  task "run-tests" {
    run "docker" {
      image = "golang:1.25"
      cmd   = "cd app && make integration-test"
      args  = ["-v", "/cache/go:/root/go/pkg/mod"]
    }
  }
}
  • git 리소스가 변화를 감시한다.
  • Vault 시크릿이 Postgres 비밀번호를 제공한다.
  • Postgres 가 서비스로 시작된다.
  • 작업은 Docker 안에서 실행된다.

네 가지 추상화가 모두 들어간 하나의 파이프라인이다.


로컬에서 파이프라인 실행하기

pikoci run --pipeline-config pipeline.hcl --job test

노트북 하나만 있으면 어떤 잡도 실행 가능 — 서버가 필요 없다.
리소스를 로컬 경로로 오버라이드하고, --var 로 시크릿을 주입한다. CI 에서 쓰던 파이프라인을 그대로 노트북에서도 돌릴 수 있다.


분산 워커

워커는 서버에 직접 연결되지 않는다; 큐를 구독한다. 따라서 워커는 다음과 같은 환경에서도 가능하다:

  • NAT 뒤에 있을 때
  • 노트북에서
  • 다른 데이터센터에 있을 때
  • 완전히 일시적인 경우

서버는 워커가 어디에 있는지 알 필요가 없다. 분산 워커를 추가하는 일은 매우 간단하다—네트워크가 큐에 접근할 수 있는 어디서든 워커를 실행하면 자동으로 동작한다.


PikoCI 스스로를 배포한다

파이프라인은 PR 체크, 목 테스트, 그리고 여섯 가지 데이터베이스·큐 백엔

0 조회
Back to Blog

관련 글

더 보기 »