CI/CD 컴포넌트 예시

CI/CD 컴포넌트 예시 (CI/CD component examples)

컴포넌트를 실제로 어떻게 테스트하고, 공통 패턴을 어떻게 구현하는지, 기존 CI/CD 템플릿을 어떻게 재사용 컴포넌트로 옮기는지를 예시로 확인해 볼게요. 코드와 YAML을 그대로 놓고 단계를 하나씩 따라가면 컴포넌트 감이 금방 잡힙니다.

출처: 문서

본문

컴포넌트 테스트 (Test a component)

컴포넌트의 기능에 따라 컴포넌트 테스트에는 저장소의 추가 파일이 필요할 수 있어요. 예를 들어 특정 프로그래밍 언어의 소프트웨어를 린트·빌드·테스트하는 컴포넌트는 실제 소스 코드 샘플이 필요합니다. 소스 코드 예시, 구성 파일 등을 같은 저장소에 둘 수 있어요. 예를 들어 Code Quality CI/CD 컴포넌트에는 테스트용 코드 샘플이 여러 개 있습니다.

예시: Rust 언어 CI/CD 컴포넌트 테스트 (Example: Test a Rust language CI/CD component)

다음은 Rust 프로그래밍 언어의 “hello world” 예시로, 단순함을 위해 cargo 툴체인을 사용해요.

  1. CI/CD 컴포넌트 루트 디렉터리로 이동합니다.
  2. cargo init 명령으로 새 Rust 프로젝트를 초기화합니다.
cargo init

이 명령은 src/main.rs “hello world” 예시를 포함한 모든 필요한 프로젝트 파일을 만들어요. 이 단계만으로도 컴포넌트 job에서 cargo build로 Rust 소스를 빌드할 수 있어요.

tree
.
├── Cargo.toml
├── LICENSE.md
├── README.md
├── src
│   └── main.rs
└── templates
    └── build.yml
  1. 예를 들어 templates/build.yml에서 컴포넌트가 Rust 소스를 빌드하는 job을 갖도록 합니다.
spec:
  inputs:
    stage:
      default: build
      description: 'Defines the build stage'
    rust_version:
      default: latest
      description: 'Specify the Rust version, use values from https://hub.docker.com/_/rust/tags Defaults to latest'
---


"build-$[[ inputs.rust_version ]]":
  stage: $[[ inputs.stage ]]
  image: rust:$[[ inputs.rust_version ]]
  script:
    - cargo build --verbose

이 예시에서:

  • stagerust_version inputs는 기본값에서 수정할 수 있어요. CI/CD job은 build- 접두사로 시작하고 rust_version inputs에 따라 이름을 동적으로 만듭니다. cargo build --verbose 명령이 Rust 소스를 컴파일해요.
  1. 프로젝트의 .gitlab-ci.yml 구성 파일에서 컴포넌트의 build 템플릿을 테스트합니다.
include:
  # include the component located in the current project from the current SHA
  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build@$CI_COMMIT_SHA
    inputs:
      stage: build

stages: [build, test, release]
  1. 테스트 실행 등을 위해 Rust 코드에 추가 함수·테스트를 넣고, templates/test.ymlcargo test를 실행하는 컴포넌트 템플릿과 job을 추가합니다.
spec:
  inputs:
    stage:
      default: test
      description: 'Defines the test stage'
    rust_version:
      default: latest
      description: 'Specify the Rust version, use values from https://hub.docker.com/_/rust/tags Defaults to latest'
---


"test-$[[ inputs.rust_version ]]":
  stage: $[[ inputs.stage ]]
  image: rust:$[[ inputs.rust_version ]]
  script:
    - cargo test --verbose
  1. test 컴포넌트 템플릿을 include해서 파이프라인에서 추가 job을 테스트합니다.
include:
  # include the component located in the current project from the current SHA
  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build@$CI_COMMIT_SHA
    inputs:
      stage: build
  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/test@$CI_COMMIT_SHA
    inputs:
      stage: test

stages: [build, test, release]

CI/CD 컴포넌트 패턴 (CI/CD component patterns)

이 섹션은 CI/CD 컴포넌트에서 공통 패턴을 구현하는 실제 예시를 제공해요.

boolean inputs로 조건부로 job 구성 (Use boolean inputs to conditionally configure jobs)

boolean 타입 inputs와 extends 기능을 조합하면 두 가지 조건으로 job을 구성할 수 있어요. 예를 들어 boolean input으로 복잡한 캐싱 동작을 구성하려면 이렇게 합니다.

spec:
  inputs:
    enable_special_caching:
      description: 'If set to `true` configures a complex caching behavior'
      type: boolean
---

.my-component:enable_special_caching:false:
  extends: null

.my-component:enable_special_caching:true:
  cache:
    policy: pull-push
    key: $CI_COMMIT_SHA
    paths: [...]

my-job:
  extends: '.my-component:enable_special_caching:$[[ inputs.enable_special_caching ]]'
  script: ... # run some fancy tooling

이 패턴은 enable_special_caching input을 job의 extends 키워드로 전달해서 동작해요. enable_special_cachingtrue인지 false인지에 따라 사전 정의된 숨은 job(.my-component:enable_special_caching:true 또는 .my-component:enable_special_caching:false)에서 적절한 구성이 선택됩니다.

options로 조건부로 job 구성 (Use options to conditionally configure jobs)

여러 옵션으로 job을 구성하면 ifelseif 조건과 비슷하게 동작하게 만들 수 있어요. string 타입과 여러 options를 가진 extends를 사용해서 원하는 만큼 조건을 만들 수 있습니다. 예를 들어 3가지 옵션으로 복잡한 캐싱 동작을 구성하려면 이렇게 해요.

spec:
  inputs:
    cache_mode:
      description: Defines the caching mode to use for this component
      type: string
      options:
        - default
        - aggressive
        - relaxed
---

.my-component:cache_mode:default:
  extends: null

.my-component:cache_mode:aggressive:
  cache:
    policy: push
    key: $CI_COMMIT_SHA
    paths: ['*/**']

.my-component:cache_mode:relaxed:
  cache:
    policy: pull-push
    key: $CI_COMMIT_BRANCH
    paths: ['bin/*']

my-job:
  extends: '.my-component:cache_mode:$[[ inputs.cache_mode ]]'
  script: ... # run some fancy tooling

이 예시에서 cache_mode input은 default, aggressive, relaxed 옵션을 제공하고, 각각 다른 숨은 job에 대응합니다. extends: '.my-component:cache_mode:$[[ inputs.cache_mode ]]'로 컴포넌트 job을 확장하면 선택한 옵션에 따라 적절한 캐싱 구성을 동적으로 상속합니다.

컴포넌트 컨텍스트로 버전 자원 참조 (Use component context to reference versioned resources)

  • GitLab 18.6에서 ci_component_context_interpolation이라는 기능 플래그와 함께 beta도입. 기본으로 활성화되어 있어요.
  • GitLab 18.7에서 일반 공개. 기능 플래그 ci_component_context_interpolation은 제거됐어요.

CI/CD 표현식의 컴포넌트 컨텍스트로 버전·커밋 SHA 같은 컴포넌트 메타데이터를 참조할 수 있어요. 한 가지 사용 사례는 버전 자원(예: Docker 이미지)을 컴포넌트로 빌드·게시하고, 컴포넌트가 일치하는 버전을 사용하도록 하는 거예요. 예를 들어 이렇게 할 수 있습니다.

  • 컴포넌트의 릴리스 파이프라인에서 컴포넌트 버전과 일치하는 태그로 Docker 이미지를 빌드.
  • 컴포넌트가 같은 이미지 버전을 참조하게 하기.

컴포넌트 프로젝트의 릴리스 파이프라인(.gitlab-ci.yml)에서:

build-image:
  stage: build
  image: docker:latest
  script:
    - docker build -t $CI_REGISTRY_IMAGE/my-tool:$CI_COMMIT_TAG .
    - docker push $CI_REGISTRY_IMAGE/my-tool:$CI_COMMIT_TAG

create-release:
  stage: release
  image: registry.gitlab.com/gitlab-org/cli:latest
  script: echo "Creating release $CI_COMMIT_TAG"
  rules:
    - if: $CI_COMMIT_TAG
  release:
    tag_name: $CI_COMMIT_TAG
    description: "Release $CI_COMMIT_TAG"

컴포넌트 템플릿(templates/my-component/template.yml)에서:

spec:
  component: [version, reference]
  inputs:
    stage:
      default: test
---

run-tool:
  stage: $[[ inputs.stage ]]
  image: $CI_REGISTRY_IMAGE/my-tool:$[[ component.version ]]
  script:
    - echo "Running tool version $[[ component.version ]]"
    - echo "Component was included using reference: $[[ component.reference ]]"
    - my-tool --version

이 예시에서:

  • 컴포넌트를 @1.0.0으로 include하면 job은 my-tool:1.0.0 이미지를 사용해요.
  • @1.0으로 include하면 최신 1.0.x 버전(예: 1.0.3)으로 해석되어 my-tool:1.0.3을 씁니다.
  • @~latest로 include하면 최신 릴리스 버전을 사용해요.
  • component.reference 필드는 지정한 정확한 참조(1.0, ~latest, SHA 등)를 보여줍니다. 로깅이나 디버깅에 참조를 쓸 수 있어요.

CI/CD 컴포넌트 마이그레이션 예시 (CI/CD component migration examples)

이 섹션은 CI/CD 템플릿과 파이프라인 구성을 재사용 CI/CD 컴포넌트로 옮기는 실제 예시를 보여줘요.

CI/CD 컴포넌트 마이그레이션 예시: Go (CI/CD component migration example: Go)

소프트웨어 개발 생명주기의 완전한 파이프라인은 여러 job과 스테이지로 구성할 수 있어요. 프로그래밍 언어용 CI/CD 템플릿은 단일 템플릿 파일에 여러 job을 제공할 수 있습니다. 연습 삼아 다음 Go CI/CD 템플릿을 마이그레이션해 볼게요.

default:
  image: golang:latest

stages:
  - test
  - build
  - deploy

format:
  stage: test
  script:
    - go fmt $(go list ./... | grep -v /vendor/)
    - go vet $(go list ./... | grep -v /vendor/)
    - go test -race $(go list ./... | grep -v /vendor/)

compile:
  stage: build
  script:
    - mkdir -p mybinaries
    - go build -o mybinaries ./...
  artifacts:
    paths:
      - mybinaries

[!NOTE] 더 점진적인 접근을 원한다면 job을 한 번에 하나씩 마이그레이션하세요. build job부터 시작하고 format, test job에 같은 단계를 반복하세요.

CI/CD 템플릿 마이그레이션은 다음 단계를 포함합니다.

  1. CI/CD job과 의존성을 분석하고 마이그레이션 작업을 정의합니다.
    • image 구성은 전역이라서 job 정의 안으로 옮겨야 해요.
    • format job은 한 job에서 여러 go 명령을 실행해요. 파이프라인 효율을 높이려면 go test 명령을 별도 job으로 옮겨야 합니다.
    • compile job은 go build를 실행하며 build로 이름을 바꿔야 해요.
  2. 더 나은 파이프라인 효율을 위한 최적화 전략을 정의합니다.
    • stage job 속성은 서로 다른 CI/CD 파이프라인 소비자들을 허용하도록 구성 가능해야 해요.
    • image 키는 하드코딩된 이미지 태그 latest를 써요. 더 유연·재사용 가능한 파이프라인을 위해 golang_version을 input으로 추가하고 기본값은 latest로 둡니다. input은 Docker Hub 이미지 태그 값과 일치해야 해요.
    • compile job은 하드코딩된 대상 디렉터리 mybinaries에 바이너리를 빌드하는데, 이를 동적 input과 기본값 mybinaries로 개선할 수 있어요.
  3. job마다 템플릿 하나씩으로 새 컴포넌트의 디렉터리 구조를 만듭니다.
    • 템플릿 이름은 go 명령을 따라야 해요. 예: format.yml, build.yml, test.yml.
    • 새 프로젝트를 만들고, Git 저장소를 초기화하고, 변경을 추가·커밋하고, 원격 origin을 설정하고, 푸시합니다. URL을 여러분의 CI/CD 컴포넌트 프로젝트 경로에 맞게 수정하세요.
    • 컴포넌트 작성 가이드에 따라 추가 파일(README.md, LICENSE.md, .gitlab-ci.yml, .gitignore)을 만듭니다. 다음 셸 명령으로 Go 컴포넌트 구조를 초기화합니다.
git init

mkdir templates
touch templates/{format,build,test}.yml

touch README.md LICENSE.md .gitlab-ci.yml .gitignore

git add -A
git commit -avm "Initial component structure"

git remote add origin https://gitlab.example.com/components/golang.git

git push
  1. CI/CD job을 템플릿으로 만듭니다. build job부터 시작하세요.
    • spec 섹션에 stage, golang_version, binary_directory inputs를 정의합니다.
    • inputs.golang_version에 접근하는 동적 job 이름 정의를 추가합니다.
    • inputs.golang_version에 접근하는 동적 Go 이미지 버전에도 비슷한 패턴을 사용합니다.
    • 스테이지를 inputs.stage 값으로 지정합니다.
    • inputs.binary_directory로 바이너리 디렉터리를 만들고 go build의 매개변수로 추가합니다.
    • artifact 경로를 inputs.binary_directory로 정의합니다.
    spec:
      inputs:
        stage:
          default: 'build'
          description: 'Defines the build stage'
        golang_version:
          default: 'latest'
          description: 'Go image version tag'
        binary_directory:
          default: 'mybinaries'
          description: 'Output directory for created binary artifacts'
    ---

    "build-$[[ inputs.golang_version ]]":
      image: golang:$[[ inputs.golang_version ]]
      stage: $[[ inputs.stage ]]
      script:
        - mkdir -p $[[ inputs.binary_directory ]]
        - go build -o $[[ inputs.binary_directory ]] ./...
      artifacts:
        paths:
          - $[[ inputs.binary_directory ]]
    ```

   - `format` job 템플릿은 같은 패턴을 따르지만 `stage`와 `golang_version` inputs만 필요해요.

```yaml
    spec:
      inputs:
        stage:
          default: 'format'
          description: 'Defines the format stage'
        golang_version:
          default: 'latest'
          description: 'Golang image version tag'
    ---

    "format-$[[ inputs.golang_version ]]":
      image: golang:$[[ inputs.golang_version ]]
      stage: $[[ inputs.stage ]]
      script:
        - go fmt $(go list ./... | grep -v /vendor/)
        - go vet $(go list ./... | grep -v /vendor/)
    ```

   - `test` job 템플릿은 같은 패턴을 따르지만 `stage`와 `golang_version` inputs만 필요해요.

```yaml
    spec:
      inputs:
        stage:
          default: 'test'
          description: 'Defines the format stage'
        golang_version:
          default: 'latest'
          description: 'Golang image version tag'
    ---

    "test-$[[ inputs.golang_version ]]":
      image: golang:$[[ inputs.golang_version ]]
      stage: $[[ inputs.stage ]]
      script:
        - go test -race $(go list ./... | grep -v /vendor/)
    ```

5. 컴포넌트를 테스트하려면 `.gitlab-ci.yml` 구성 파일을 수정하고 [테스트](/ci/components/#test-the-component)를 추가합니다.
   - `build` job의 input으로 `golang_version`의 다른 값을 지정합니다.
   - URL을 여러분의 CI/CD 컴포넌트 경로에 맞게 수정하세요.

```yaml
    stages: [format, build, test]

    include:
      - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/format@$CI_COMMIT_SHA
      - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build@$CI_COMMIT_SHA
      - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build@$CI_COMMIT_SHA
        inputs:
          golang_version: "1.21"
      - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/test@$CI_COMMIT_SHA
        inputs:
          golang_version: latest
    ```

6. CI/CD 컴포넌트를 테스트할 Go 소스를 추가합니다. `go` 명령은 루트 디렉터리에 `go.mod`와 `main.go`가 있는 Go 프로젝트를 기대해요.
   - Go 모듈을 초기화합니다. URL을 여러분의 CI/CD 컴포넌트 경로에 맞게 수정하세요.

```bash
    go mod init example.gitlab.com/components/golang
    ```

   - `main.go` 파일에 `Hello, CI/CD component`를 출력하는 main 함수를 만듭니다. GitLab Duo Code Suggestions로 Go 코드를 생성하려면 코드 주석을 사용할 수 있어요.

```go
    // Specify the package, import required packages
    // Create a main function
    // Inside the main function, print "Hello, CI/CD Component"

    package main

    import "fmt"

    func main() {
      fmt.Println("Hello, CI/CD Component")
    }
    ```

   - 디렉터리 트리는 이렇게 보여야 해요.

tree
.
├── LICENSE.md
├── README.md
├── go.mod
├── main.go
└── templates
    ├── build.yml
    ├── format.yml
    └── test.yml
```

CI/CD 템플릿을 컴포넌트로 변환 섹션의 나머지 단계를 따라 마이그레이션을 완료합니다.

  1. 변경을 커밋·푸시하고 CI/CD 파이프라인 결과를 확인합니다.
  2. 컴포넌트 작성 가이드에 따라 README.mdLICENSE.md 파일을 업데이트합니다.
  3. 컴포넌트 릴리스하고 CI/CD 카탈로그에서 확인합니다.
  4. CI/CD 컴포넌트를 스테이징·프로덕션 환경에 추가합니다.

GitLab이 관리하는 Go 컴포넌트는 inputs와 컴포넌트 모범 사례로 개선된 Go CI/CD 템플릿의 성공적인 마이그레이션 예시를 제공해요. Git 이력을 살펴보며 더 배울 수 있습니다.

더 알아보기

컴포넌트는 입력을 extends의 이름에 넣어 조건부 구성을 만들고, 컴포넌트 컨텍스트로 버전 자원을 참조할 수 있다는 게 가장 강력한 포인트예요. 템플릿을 컴포넌트로 옮길 때는 전역 키워드를 job 정의 안으로 이동하고, 하드코딩된 값을 inputs로 바꾸는 작업부터 하면 깔끔하게 시작할 수 있어요. 자체 테스트 데이터를 저장소에 두고 job마다 템플릿을 나눠 만드는 구조가 유지보수에 좋습니다.