Nexus¶
이 문서는 tirosh-home site의 Nexus Repository Manager를 package registry로 운영하는 방법을 설명합니다.
Nexus Kubernetes 리소스는 tirosh-gitops가 관리합니다. Nexus 내부의 repository, privilege, role, user 같은 애플리케이션 운영 상태는 tirosh-infra에서 REST API로 관리합니다.
1. 운영 개요¶
1-1. Nexus가 맡는 일¶
Nexus는 package manager나 artifact client가 repository protocol을 기대하는 생태계에 사용합니다. Python은 PyPI, frontend는 npm, JVM은 Maven, Kubernetes chart는 Helm, Rust는 Cargo, Go는 Go repository, C/C++ library는 Conan repository로 운영합니다.
Raw repository는 embedded firmware와 board artifact처럼 file repository가 자연스러운 대상에 한정합니다. AI model, checkpoint, dataset 같은 큰 blob artifact는 기본적으로 S3/MinIO 계열 object storage를 우선하고, Hugging Face client 호환성이 필요해질 때 별도 repository로 판단합니다.
1-2. 관리 경계¶
Nexus 운영은 GitOps 리소스와 Nexus 애플리케이션 상태로 나뉩니다.
| 저장소 | 책임 |
|---|---|
tirosh-gitops |
Nexus namespace, Deployment/StatefulSet, PVC, Service, Ingress, backup CronJob |
tirosh-infra |
Nexus repository, privilege, role, user, GitHub Actions package publishing settings, 수동 restore Job 생성 |
profile.toml은 Nexus desired state를 직접 담지 않고, site별 vars 파일 경로만 가리킵니다. Nexus repository catalog와 role/user desired state는 sites/<site_id>/nexus/vars.yml이 SoT입니다.
1-3. 기본 운영 원칙¶
- consume endpoint는 group repository를 사용합니다. 예를 들어 Python install은
pypi를 봅니다. - publish endpoint는 hosted repository를 사용합니다. 예를 들어 Python publish는
pypi-hosted로 보냅니다. - guide/sample artifact는
*-guide-hostedrepository로 publish합니다. 기존 group repository에는 guide hosted를 member로 포함해 consume endpoint를 유지합니다. - proxy repository는 외부 공식 registry cache입니다.
- publish credential과 developer credential은 분리합니다.
- 운영 restore는 GitOps에 영구 Job으로 두지 않고, 필요할 때만 one-off Job으로 생성합니다.
Conan repository를 사용할 때는 Nexus의 Conan Bearer Token Realm이 활성화되어 있어야 합니다. tirosh-infra는 site vars의 nexus_required_active_realms를 기준으로 ConanToken realm을 함께 수렴시킵니다.
2. 빠른 적용¶
2-1. 전체 순서¶
처음 구성할 때는 로컬 secret을 준비하고, Nexus 내부 상태를 적용한 뒤, GitHub Actions variables/secrets를 등록합니다.
cp .env.example .env
$EDITOR .env
make nexus/doctor SITE=tirosh-home
make nexus/apply SITE=tirosh-home
make nexus/github/apply
2-2. Local secrets 준비¶
.env는 .gitignore에 포함되어 있으므로 git에 커밋하지 않습니다.
NEXUS_ADMIN_USERNAME=admin
NEXUS_ADMIN_PASSWORD=<nexus-admin-password>
NEXUS_CI_PASSWORD=<new-ci-user-password>
NEXUS_BASE_URL=https://nexus.internal.tirosh.ai
NEXUS_PYPI_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/pypi-hosted/
NEXUS_PYPI_INDEX_URL=https://nexus.internal.tirosh.ai/repository/pypi/simple/
NEXUS_NPM_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/npm-hosted/
NEXUS_NPM_REGISTRY_URL=https://nexus.internal.tirosh.ai/repository/npm/
NEXUS_USERNAME=github-actions
NEXUS_PASSWORD=<NEXUS_CI_PASSWORD>
| 변수 | 의미 |
|---|---|
NEXUS_ADMIN_USERNAME |
Nexus REST API를 호출할 관리자 계정 |
NEXUS_ADMIN_PASSWORD |
관리자 계정 비밀번호 |
NEXUS_CI_PASSWORD |
생성 또는 갱신할 Nexus CI user의 비밀번호. 최소 8자 이상 |
NEXUS_BASE_URL |
GitHub Actions endpoint 변수를 자동 생성할 Nexus base URL |
NEXUS_<ECOSYSTEM>_*_URL |
ecosystem별 endpoint override. 비어 있으면 NEXUS_BASE_URL 기준 기본값 사용 |
NEXUS_USERNAME |
GitHub Actions가 package install/publish에 사용할 Nexus user |
NEXUS_PASSWORD |
GitHub Actions secret으로 등록할 Nexus password |
NEXUS_PASSWORD가 비어 있거나 change-me이면 GitHub Actions 등록 target은 NEXUS_CI_PASSWORD를 대신 사용합니다.
2-3. Nexus state 적용¶
아래 target은 Nexus 내부 repository, privilege, role, user를 원하는 상태로 수렴시킵니다.
make nexus/doctor SITE=tirosh-home
make nexus/apply SITE=tirosh-home
내부적으로 실행되는 playbook은 다음입니다.
infra/ansible/playbooks/nexus/provision-repositories.yml
nexus/apply는 v2 public interface이고, 기존 nexus/repositories/apply는 같은 작업을 수행하는 상세 target으로 남아 있습니다.
CI user password를 강제로 다시 설정하려면 site vars에서 아래 값을 임시로 켭니다.
nexus_ci_user:
reset_password: true
password reset 후에는 다시 false로 돌립니다.
2-4. GitHub Actions 설정 적용¶
Nexus CI user가 준비되면 GitHub repository에 package publish 설정을 등록합니다.
make nexus/github/apply
다른 GitHub repository에 등록할 때는 gh가 지원하는 GH_REPO를 지정합니다.
GH_REPO=tirosh-chain/tirosh-dev-guide make nexus/github/apply
등록되는 값은 다음입니다.
| 이름 | GitHub 저장 위치 | 값 |
|---|---|---|
NEXUS_BASE_URL |
Variable | https://nexus.internal.tirosh.ai |
NEXUS_PYPI_PUBLISH_URL |
Variable | https://nexus.internal.tirosh.ai/repository/pypi-hosted/ |
NEXUS_PYPI_INDEX_URL |
Variable | https://nexus.internal.tirosh.ai/repository/pypi/simple/ |
NEXUS_NPM_PUBLISH_URL |
Variable | https://nexus.internal.tirosh.ai/repository/npm-hosted/ |
NEXUS_NPM_REGISTRY_URL |
Variable | https://nexus.internal.tirosh.ai/repository/npm/ |
NEXUS_NPM_REGISTRY |
Variable | https://nexus.internal.tirosh.ai/repository/npm/ |
NEXUS_NPM_PUBLISH_REGISTRY |
Variable | https://nexus.internal.tirosh.ai/repository/npm-hosted/ |
NEXUS_MAVEN_PUBLISH_URL |
Variable | https://nexus.internal.tirosh.ai/repository/maven-hosted/ |
NEXUS_MAVEN_REPOSITORY_URL |
Variable | https://nexus.internal.tirosh.ai/repository/maven/ |
NEXUS_HELM_PUBLISH_URL |
Variable | https://nexus.internal.tirosh.ai/repository/helm-hosted/ |
NEXUS_HELM_REPOSITORY_URL |
Variable | https://nexus.internal.tirosh.ai/repository/helm/ |
NEXUS_CARGO_PUBLISH_URL |
Variable | https://nexus.internal.tirosh.ai/repository/cargo-hosted/ |
NEXUS_CARGO_REGISTRY_URL |
Variable | https://nexus.internal.tirosh.ai/repository/cargo/ |
NEXUS_CARGO_PROXY_REGISTRY |
Variable | sparse+https://nexus.internal.tirosh.ai/repository/cargo/ |
NEXUS_CARGO_REGISTRY_INDEX |
Variable | sparse+https://nexus.internal.tirosh.ai/repository/cargo-hosted/ |
NEXUS_GO_PUBLISH_URL |
Variable | https://nexus.internal.tirosh.ai/repository/go-hosted/ |
NEXUS_GO_PROXY_URL |
Variable | https://nexus.internal.tirosh.ai/repository/go/ |
NEXUS_CONAN_PUBLISH_URL |
Variable | https://nexus.internal.tirosh.ai/repository/conan-hosted/ |
NEXUS_CONAN_REMOTE_URL |
Variable | https://nexus.internal.tirosh.ai/repository/conan/ |
NEXUS_CONAN_PUBLISH_REMOTE_URL |
Variable | https://nexus.internal.tirosh.ai/repository/conan-hosted/ |
NEXUS_RAW_EMBEDDED_PUBLISH_URL |
Variable | https://nexus.internal.tirosh.ai/repository/raw-embedded-hosted/ |
NEXUS_RAW_FIRMWARE_URL |
Variable | https://nexus.internal.tirosh.ai/repository/raw-embedded-hosted |
NEXUS_USERNAME |
Variable | github-actions |
NEXUS_PASSWORD |
Secret | Nexus CI user password |
tirosh-dev-guide처럼 guide artifact를 publish하는 repository는 GitHub Actions variable을 guide hosted endpoint로 override합니다. 이 값은 Nexus repository 생성이 아니라 GitHub repository 설정입니다.
GH_REPO=tirosh-chain/tirosh-dev-guide \
NEXUS_PYPI_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/pypi-guide-hosted/ \
NEXUS_NPM_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/npm-guide-hosted/ \
NEXUS_CARGO_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/cargo-guide-hosted/ \
NEXUS_CARGO_REGISTRY_INDEX=sparse+https://nexus.internal.tirosh.ai/repository/cargo-guide-hosted/ \
NEXUS_CONAN_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/conan-guide-hosted/ \
NEXUS_RAW_EMBEDDED_PUBLISH_URL=https://nexus.internal.tirosh.ai/repository/raw-embedded-guide-hosted/ \
NEXUS_RAW_FIRMWARE_URL=https://nexus.internal.tirosh.ai/repository/raw-embedded-guide-hosted \
make nexus/github/apply
3. Desired State 구성¶
3-1. 설정 파일¶
Nexus desired state는 site profile, site vars, local env 세 곳으로 나뉩니다. 저장소에 남겨도 되는 값은 YAML/TOML에 두고, password와 token은 .env에 둡니다.
sites/<site_id>/profile.toml은 site별 vars 파일 경로만 가리킵니다.
[nexus]
vars = "sites/tirosh-home/nexus/vars.yml"
sites/tirosh-home/nexus/vars.yml은 Nexus repository catalog와 aggregate role/user 설정을 담습니다.
nexus_url: "https://nexus.internal.tirosh.ai"
nexus_privilege_name_prefix: "tirosh-nexus"
nexus_ci_user:
id: "github-actions"
first_name: "GitHub Actions"
last_name: "Nexus CI"
email: "github-actions@tirosh.local"
reset_password: false
roles:
- "tirosh-nexus-publisher"
- "tirosh-nexus-reader"
nexus_roles:
publisher:
id: "tirosh-nexus-publisher"
name: "Tirosh Nexus Publisher"
description: "Publish packages and artifacts to Tirosh Nexus repositories."
actions: [BROWSE, READ, ADD]
reader:
id: "tirosh-nexus-reader"
name: "Tirosh Nexus Reader"
description: "Read packages and artifacts from Tirosh Nexus repositories."
actions: [BROWSE, READ]
nexus_repository_defaults:
hosted:
blob_store: "default"
write_policy: "ALLOW_ONCE"
strict_content_type_validation: true
proxy:
blob_store: "default"
strict_content_type_validation: true
content_max_age: 1440
metadata_max_age: 1440
group:
blob_store: "default"
strict_content_type_validation: true
nexus_required_active_realms:
- "ConanToken"
nexus_repositories:
- format: "pypi"
description: "Python packages"
hosted:
name: "pypi-hosted"
proxy:
name: "pypi-proxy"
remote_url: "https://pypi.org/"
pypi:
index_path: "/simple"
group:
name: "pypi"
- format: "npm"
description: "Frontend npm packages"
hosted:
name: "npm-hosted"
proxy:
name: "npm-proxy"
remote_url: "https://registry.npmjs.org"
group:
name: "npm"
- format: "helm"
description: "Kubernetes Helm charts"
hosted:
name: "helm-hosted"
group:
name: "helm"
- format: "conan"
description: "C and C++ Conan packages"
hosted:
name: "conan-hosted"
proxy:
name: "conan-proxy"
remote_url: "https://center2.conan.io"
conan:
version: "V2"
group:
name: "conan"
Helm은 조직마다 upstream chart repository가 다르므로 기본 catalog에서는 helm-hosted만 group member로 둡니다.
3-2. Repository 이름¶
client가 바라보는 endpoint는 생태계 이름 그대로 둡니다.
| 종류 | 이름 규칙 | 용도 | 예 |
|---|---|---|---|
| hosted | <ecosystem>-hosted |
내부 package publish | pypi-hosted, npm-hosted |
| guide hosted | <ecosystem>-guide-hosted |
guide/sample artifact publish | pypi-guide-hosted, npm-guide-hosted |
| proxy | <ecosystem>-proxy 또는 upstream 이름 포함 |
외부 공식 registry cache | pypi-proxy, maven-central-proxy |
| group | <ecosystem> |
developer/CI consume endpoint | pypi, npm, maven |
publish는 hosted repository로 보냅니다. install, restore, dependency resolution은 group repository를 봅니다. group repository는 내부 hosted, guide hosted, 외부 proxy를 한 endpoint로 묶습니다.
3-3. 생성되는 리소스¶
현재 catalog가 만드는 Nexus 상태는 다음입니다.
| 생태계 | hosted | proxy | group |
|---|---|---|---|
| PyPI | pypi-hosted, pypi-guide-hosted |
pypi-proxy |
pypi |
| npm | npm-hosted, npm-guide-hosted |
npm-proxy |
npm |
| Maven | maven-hosted |
maven-central-proxy |
maven |
| Helm | helm-hosted |
없음 | helm |
| Cargo | cargo-hosted, cargo-guide-hosted |
cargo-proxy |
cargo |
| Go | go-hosted |
go-proxy |
go |
| Conan | conan-hosted, conan-guide-hosted |
conan-proxy |
conan |
| Raw embedded | raw-embedded-hosted, raw-embedded-guide-hosted |
없음 | 없음 |
그 외에 아래 보안 리소스도 생성 또는 갱신합니다.
| 대상 | 값 |
|---|---|
| Publisher role | tirosh-nexus-publisher |
| Reader role | tirosh-nexus-reader |
| CI user | github-actions |
repository write policy는 기본적으로 ALLOW_ONCE입니다. 같은 version artifact를 덮어쓰지 않도록 하기 위한 설정입니다.
Go hosted repository는 Nexus Repository 3.93.0부터 지원됩니다. Terraform hosted repository는 Nexus에서 지원하지만 GPG signing key와 passphrase가 필수이므로 별도 secret 설계가 준비될 때 추가합니다. Docker image는 현재 GHCR을 기준 registry로 사용하므로 Nexus 기본 catalog에 넣지 않습니다.
Conan 2 repository는 Nexus Repository 3.76.0 이상에서 proxy, hosted, group 구성을 지원합니다. C/C++ library package는 Conan hosted repository에 publish하고, .elf, .hex, .bin, .map 같은 firmware release artifact는 Raw repository에 보관합니다.
3-4. 권한 모델¶
Nexus credential은 consume과 publish 책임을 분리합니다. 개발자는 dependency를 내려받을 수 있어야 하지만 release artifact를 올리거나 수정할 필요는 없습니다. CI만 hosted repository에 publish하고, 사람 계정은 기본적으로 group repository를 read-only로 사용합니다.
| 주체 | role | actions | 용도 |
|---|---|---|---|
| GitHub Actions | tirosh-nexus-publisher, tirosh-nexus-reader |
repository별 publisher actions, BROWSE, READ |
package install and publish |
| 개발자 | tirosh-nexus-reader |
BROWSE, READ |
package install |
github-actions 계정은 GitHub Actions secret 전용입니다. CI는 build 중 dependency를 group repository에서 내려받고, release 때 hosted repository에 publish하므로 reader role과 publisher role을 모두 가집니다. publish 권한이 있으므로 사내 개발자에게 공유하지 않습니다.
package registry마다 publisher actions가 다른 이유는 publish API가 서로 다르기 때문입니다. Nexus의 repository-view privilege는 BROWSE, READ, ADD, EDIT, DELETE 같은 공통 action을 제공하지만, 각 package client가 publish 중 호출하는 HTTP/API 동작은 format마다 다릅니다.
단순 파일 업로드형 repository는 새 artifact 추가 권한인 ADD가 중심입니다. 반면 Cargo와 Conan처럼 archive upload와 registry metadata 갱신을 함께 수행하는 repository는 content 수정에 해당하는 EDIT도 필요할 수 있습니다. 이 차이는 guide repository 여부가 아니라 registry protocol의 차이입니다. 따라서 cargo-hosted와 cargo-guide-hosted는 같은 publisher actions를 사용합니다.
| repository 유형 | publisher actions | 이유 |
|---|---|---|
| 기본 hosted package repository | BROWSE, READ, ADD |
새 artifact를 추가합니다. repository write policy는 ALLOW_ONCE이므로 같은 version overwrite는 막습니다. |
| Cargo hosted, Cargo guide hosted | BROWSE, READ, ADD, EDIT |
cargo publish는 crate archive upload와 registry metadata 갱신을 함께 수행합니다. |
| Conan hosted, Conan guide hosted | BROWSE, READ, ADD, EDIT |
Conan package publish는 recipe/package revision metadata 갱신을 포함합니다. |
| Raw embedded hosted | BROWSE, READ, ADD |
firmware binary와 manifest를 append-only artifact로 보관합니다. |
| proxy/group repository | reader actions only | dependency consume endpoint입니다. CI와 개발자는 group/proxy에서 read만 수행합니다. |
기업 운영에서는 보통 다음 기준을 적용합니다.
| 운영 기준 | 권장 방식 | 이유 |
|---|---|---|
| 사람 계정과 CI 계정 분리 | developer는 reader role, CI는 publisher role | 실수나 로컬 환경 유출로 release artifact가 변경되는 일을 줄입니다. |
| hosted와 group/proxy 분리 | publish는 hosted, consume은 group | dependency 소비자는 stable endpoint 하나만 사용하고, publish 권한은 hosted에만 부여합니다. |
| repository별 privilege 생성 | tirosh-nexus-<repository>-publisher, tirosh-nexus-<repository>-reader |
registry별 권한 차이를 코드로 추적할 수 있습니다. |
| delete 권한 제한 | 기본 publisher role에는 DELETE를 넣지 않음 |
release artifact는 감사와 재현성 대상입니다. 삭제는 운영자 또는 break-glass 절차로 제한합니다. |
| overwrite 방지 | hosted write policy는 ALLOW_ONCE |
같은 version을 다시 publish하지 못하게 하여 build reproducibility를 지킵니다. |
| 권한 코드화 | Nexus UI 수동 변경 대신 Ansible vars로 관리 | 권한 변경 이유와 diff를 PR에서 검토할 수 있습니다. |
참고 문서:
| 주제 | 레퍼런스 | 이 문서에서 사용하는 판단 |
|---|---|---|
| Nexus repository-view privilege actions | Sonatype Nexus Repository - Privileges | ADD는 content 추가, EDIT은 repository content 수정 동작에 해당합니다. |
| Cargo registry publish/auth | The Cargo Book - Registries, Registry Authentication | Cargo publish는 registry endpoint와 token credential provider 구성이 필요합니다. |
| Conan revisions | Conan 2 - Revisions | Conan package publish는 recipe/package revision metadata를 다룹니다. |
4. 적용 후 확인¶
4-1. Make output¶
Make target이 성공하면 마지막에 적용된 repository 목록이 출력됩니다.
Repositories: pypi-hosted, pypi-guide-hosted, npm-hosted, npm-guide-hosted, maven-hosted, helm-hosted, cargo-hosted, cargo-guide-hosted, go-hosted, conan-hosted, conan-guide-hosted, raw-embedded-hosted, raw-embedded-guide-hosted, pypi-proxy, npm-proxy, maven-central-proxy, cargo-proxy, go-proxy, conan-proxy, pypi, npm, maven, helm, cargo, go, conan
4-2. Nexus UI¶
| 화면 | 확인할 값 |
|---|---|
| Repositories | pypi-hosted, pypi-guide-hosted, npm-hosted, npm-guide-hosted, maven-hosted, helm-hosted, cargo-hosted, cargo-guide-hosted, go-hosted, conan-hosted, conan-guide-hosted, raw-embedded-hosted, raw-embedded-guide-hosted |
| Repositories | pypi-proxy, npm-proxy, maven-central-proxy, cargo-proxy, go-proxy, conan-proxy |
| Repositories | pypi, npm, maven, helm, cargo, go, conan |
| Security > Realms | Conan Bearer Token Realm |
| Security > Privileges | tirosh-nexus-<repository>-publisher, tirosh-nexus-<repository>-reader |
| Security > Roles | tirosh-nexus-publisher, tirosh-nexus-reader |
| Security > Users | github-actions |
4-3. REST API¶
set -a
. ./.env
set +a
curl -fsS \
-u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
https://nexus.internal.tirosh.ai/service/rest/v1/repositories/pypi/hosted/pypi-hosted
curl -fsS \
-u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
https://nexus.internal.tirosh.ai/service/rest/v1/repositories/pypi/proxy/pypi-proxy
curl -fsS \
-u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
https://nexus.internal.tirosh.ai/service/rest/v1/repositories/pypi/group/pypi
5. Package 사용¶
사내 개발자는 CI credential을 사용하지 않습니다. 개인 Nexus 계정 또는 SSO/LDAP group을 통해 필요한 reader role만 부여받습니다.
5-1. PyPI with uv¶
package를 사용하는 프로젝트에서는 Nexus group index를 pyproject.toml에 정의합니다.
[project]
dependencies = [
"infra-tools",
"object-store-cli",
]
[tool.uv.sources]
infra-tools = { index = "tirosh-pypi" }
object-store-cli = { index = "tirosh-pypi" }
[[tool.uv.index]]
name = "tirosh-pypi"
url = "https://nexus.internal.tirosh.ai/repository/pypi/simple/"
explicit = true
authenticate = "always"
이 repo는 root pyproject.toml에 같은 Nexus index를 이미 정의합니다.
explicit = true를 사용하면 [tool.uv.sources]에 명시한 package만 Nexus index에서 가져옵니다. 내부 package 이름과 같은 package가 외부 PyPI에 생기는 dependency confusion 위험을 줄이기 위해 이 방식을 기본으로 사용합니다.
credential은 pyproject.toml에 넣지 않습니다.
export UV_INDEX_TIROSH_PYPI_USERNAME="<nexus-user>"
export UV_INDEX_TIROSH_PYPI_PASSWORD="<nexus-password-or-token>"
uv lock
uv sync
환경 변수 이름은 index 이름 tirosh-pypi를 대문자로 바꾸고 구분자를 underscore로 바꾼 TIROSH_PYPI를 사용합니다.
5-2. PyPI with pip¶
pip만 쓰는 프로젝트에서는 pip 설정 또는 환경 변수를 사용합니다.
python -m pip install \
--index-url https://nexus.internal.tirosh.ai/repository/pypi/simple/ \
object-store-cli
pip URL에 credential을 직접 넣지 않는 편이 좋습니다. 필요한 경우 pip.conf, keyring, .netrc, CI secret 같은 별도 credential store를 사용합니다.
5-3. npm¶
frontend package install은 npm group registry를 사용하고, publish는 npm-hosted registry를 사용합니다.
npm config set @tirosh:registry https://nexus.internal.tirosh.ai/repository/npm/
npm publish --registry https://nexus.internal.tirosh.ai/repository/npm-hosted/
인증 정보는 project-local .npmrc에 직접 커밋하지 않고 개발자 환경이나 CI secret에서 주입합니다.
5-4. Other ecosystems¶
각 ecosystem의 표준 client가 Nexus repository URL을 바라보도록 설정합니다.
| 생태계 | consume repository | publish repository | 기본 client |
|---|---|---|---|
| JVM | maven |
maven-hosted |
Maven, Gradle |
| Kubernetes charts | helm |
helm-hosted |
Helm |
| Rust | cargo |
cargo-hosted |
Cargo |
| Go | go |
go-hosted |
Go module tooling |
| C/C++ | conan |
conan-hosted |
Conan 2 |
Go hosted repository는 Nexus 3.93.0 이상에서만 사용할 수 있습니다. Cargo는 Nexus 문서 기준 cargo publish를 통한 hosted repository publish를 지원합니다.
6. GitHub Actions Publishing¶
6-1. Workflow 대상¶
.github/workflows/python-packages.yml은 Python package를 검증하고, main branch push 또는 package release tag에서 Nexus PyPI hosted repository로 publish합니다.
make nexus/github/apply는 PyPI뿐 아니라 npm, Maven, Helm, Cargo, Go, Conan, Raw endpoint 변수도 함께 등록합니다. 현재 이 repo의 publish workflow는 Python package만 사용하므로 NEXUS_PYPI_*와 shared credential만 참조합니다. 다른 ecosystem workflow를 추가할 때는 이미 등록된 ecosystem별 NEXUS_*_URL 변수를 사용합니다.
| package | path | release tag prefix |
|---|---|---|
infra-tools |
packages/infra-tools |
infra-tools-v |
object-store-cli |
packages/object-store-cli |
object-store-cli-v |
PR에서는 package별로 아래 작업을 수행합니다.
uv sync
ruff check
pytest
uv build
tests 디렉터리가 없는 package는 test step에서 건너뜁니다.
6-2. Publish endpoint¶
publish는 hosted repository로 보내고, install/resolve는 group repository를 봅니다. make nexus/github/apply는 아래 endpoint를 GitHub Actions variable로 등록합니다.
| 생태계 | publish variable | consume variable |
|---|---|---|
| Python/PyPI | NEXUS_PYPI_PUBLISH_URL |
NEXUS_PYPI_INDEX_URL |
| npm | NEXUS_NPM_PUBLISH_URL, NEXUS_NPM_PUBLISH_REGISTRY |
NEXUS_NPM_REGISTRY_URL, NEXUS_NPM_REGISTRY |
| Maven | NEXUS_MAVEN_PUBLISH_URL |
NEXUS_MAVEN_REPOSITORY_URL |
| Helm | NEXUS_HELM_PUBLISH_URL |
NEXUS_HELM_REPOSITORY_URL |
| Cargo | NEXUS_CARGO_PUBLISH_URL, NEXUS_CARGO_REGISTRY_INDEX |
NEXUS_CARGO_REGISTRY_URL, NEXUS_CARGO_PROXY_REGISTRY |
| Go | NEXUS_GO_PUBLISH_URL |
NEXUS_GO_PROXY_URL |
| Conan | NEXUS_CONAN_PUBLISH_URL, NEXUS_CONAN_PUBLISH_REMOTE_URL |
NEXUS_CONAN_REMOTE_URL |
| Raw embedded | NEXUS_RAW_EMBEDDED_PUBLISH_URL, NEXUS_RAW_FIRMWARE_URL |
직접 artifact URL |
현재 tirosh-infra 자체 publish workflow는 Python package인 infra-tools, object-store-cli만 Nexus에 올립니다. 하지만 Nexus 운영 기준과 GitHub Actions variable catalog는 PyPI에 한정하지 않습니다. tirosh-dev-guide 같은 source repo가 npm, Cargo, Conan, firmware artifact workflow를 추가하면 같은 Nexus endpoint catalog를 사용합니다.
Nexus hosted repository는 같은 version 또는 같은 artifact path를 다시 올리는 일을 허용하지 않는 방향으로 운영합니다. 따라서 같은 commit의 publish job을 재실행하면 ecosystem에 따라 중복 version, 중복 package, 중복 path 오류가 날 수 있습니다.
6-3. Versioning¶
Version은 Nexus가 새로 정하지 않습니다. Nexus는 각 ecosystem의 package metadata와 artifact path를 저장하고 제공하며, version 계산은 source repo CI와 package manager가 맡습니다.
기본 정책은 release tag가 있으면 고정 release version을 쓰고, release tag가 없으면 추적 가능한 dev version을 쓰는 것입니다. Git tag를 매 commit마다 만들지 않고 CI run 안에서 dev version을 계산하는 이유는 tag 폭증을 막으면서도 artifact가 어느 source revision에서 왔는지 남기기 위해서입니다.
| 생태계 | release 예시 | dev 예시 | version을 반영하는 곳 |
|---|---|---|---|
| Python/PyPI | 0.1.0 |
0.2.0.dev37 |
pyproject.toml, wheel/sdist metadata |
| npm | 0.1.0 |
0.2.0-dev.37 |
package.json |
| Maven | 0.1.0 |
0.2.0-dev.37 또는 0.2.0-SNAPSHOT |
pom.xml, Gradle project version |
| Helm | 0.1.0 |
0.2.0-dev.37 |
Chart.yaml의 version |
| Cargo | 0.1.0 |
0.2.0-dev.37 |
Cargo.toml |
| Go | v0.1.0 |
pseudo-version 또는 module-path dev tag | module path와 Git tag |
| Conan | 0.1.0 |
0.2.0-dev.37 |
Conan package reference |
| Raw embedded | 0.1.0 |
0.2.0-dev.37 |
artifact path, filename, manifest |
Python만 0.2.0.dev37처럼 점(.)을 사용합니다. PEP 440이 SemVer prerelease 문자열인 0.2.0-dev.37을 그대로 쓰지 않기 때문입니다. npm, Cargo, Helm, Conan은 SemVer prerelease 표기를 자연스럽게 사용합니다.
Version을 package metadata에 반영할 때는 생태계별 표준 도구를 사용합니다. 직접 sed로 파일을 고치지 않습니다.
| 생태계 | CI에서 쓰는 도구 | 이유 |
|---|---|---|
| Python/uv | uv version --package ... --frozen 또는 dynamic version backend |
PEP 440 version을 표준 metadata에 반영합니다. |
| npm | npm version --no-git-tag-version --allow-same-version |
package.json과 npm metadata를 표준 방식으로 갱신합니다. |
| Maven/Gradle | Maven Versions Plugin, Gradle property/task | JVM build tool이 metadata와 publish coordinate를 관리합니다. |
| Helm | chart packaging step에서 Chart.yaml version 반영 |
chart archive 이름과 index metadata가 같은 version을 봅니다. |
| Cargo | cargo set-version |
Cargo.toml 갱신은 Cargo 생태계 도구에 맡깁니다. |
| Go | module-path semver tag 또는 pseudo-version | Go는 registry upload보다 VCS tag와 module proxy가 배포 단위입니다. |
| Conan | conan create --version ... |
recipe invocation에서 package version을 명시합니다. |
| Raw embedded | CI가 filename/path/manifest 생성 | package metadata가 없으므로 artifact 경로와 manifest가 release identity입니다. |
이 repo의 packages/ 아래 Python package는 pyproject.toml에 version을 직접 쓰지 않고 hatch-vcs 기반 dynamic version을 사용합니다. build 시점에 Git tag와 현재 commit 사이의 거리를 읽어 wheel/sdist version을 계산합니다.
tirosh-infra는 monorepo이므로 Python package마다 자기 tag만 봅니다. infra-tools는 infra-tools-v*, object-store-cli는 object-store-cli-v* tag만 version source로 사용합니다.
| package | tag pattern | fallback |
|---|---|---|
infra-tools |
infra-tools-v<version> |
0.1.0.dev0 |
object-store-cli |
object-store-cli-v<version> |
0.1.0.dev0 |
git tag infra-tools-v0.1.0
git push origin infra-tools-v0.1.0
git tag object-store-cli-v0.1.0
git push origin object-store-cli-v0.1.0
버전이 계산되는 방식은 아래처럼 읽으면 됩니다.
| 상황 | 예시 | 의미 |
|---|---|---|
| matching release tag가 아직 없음 | 0.1.0.dev0 |
첫 release 전 fallback version입니다. |
현재 commit이 infra-tools-v0.1.0 tag와 정확히 일치 |
0.1.0 |
Nexus에 올리는 고정 release version입니다. |
infra-tools-v0.1.0 이후 7번째 commit |
0.1.1.dev7 |
다음 patch release 후보를 향한 dev build입니다. |
현재 commit이 object-store-cli-v0.2.0 tag와 정확히 일치 |
0.2.0 |
object-store-cli만의 release입니다. infra-tools version에는 영향을 주지 않습니다. |
local_scheme = "no-local-version"을 사용하므로 build 산출물 version에 +g<sha> 같은 local version segment를 붙이지 않습니다. Nexus/PyPI 계열 repository에서는 같은 version을 다시 업로드할 수 없기 때문에, 같은 commit의 publish job을 재실행하면 중복 version으로 실패할 수 있습니다.
workflow는 fetch-depth: 0으로 checkout합니다. shallow checkout이면 tag와 commit distance를 알 수 없어 dynamic version 계산이 흔들릴 수 있으므로, package publish workflow에서는 전체 Git history와 tag를 가져와야 합니다.
7. Backup과 Restore¶
Nexus backup/restore는 GitOps, infra-tools, backup image가 역할을 나눠서 처리합니다. 어디서 무엇을 책임지는지 먼저 정리하고, 그 다음에 rehearsal과 운영 restore 절차를 봅니다.
7-1. 전체 운영 모델¶
Nexus의 실제 데이터는 nexus namespace의 nexus-data PVC에 있습니다. backup은 이 PVC를 archive해서 object storage에 올리고, restore는 선택한 backup id를 내려받아 같은 PVC를 덮어씁니다.
Nexus Deployment
-> nexus-data PVC
-> CronJob/nexus-backup
-> object storage: nexus/backups/<backup-id>/
restore는 반대 방향으로 진행합니다.
object storage backup id
-> infra-tools nexus-restore
-> one-off Kubernetes Job
-> existing nexus-data PVC overwrite
-> Nexus ready, API, package serving 확인
운영 원칙은 세 가지입니다.
| 원칙 | 설명 |
|---|---|
| 정기 backup은 GitOps가 관리 | CronJob, RBAC, ConfigMap, PVC mount는 tirosh-gitops에 둡니다. |
| restore 실행은 infra-tools가 생성 | live CronJob의 image, env, volume, service account를 읽어서 one-off Job을 만듭니다. |
| 운영 restore는 PVC overwrite | 새 PVC로 복원하는 rehearsal이 아니라, 기존 nexus-data PVC를 덮어쓰는 destructive 작업입니다. |
7-2. GitOps가 맡는 리소스¶
이 단계에서는 infra 도구로 만든 backup/restore 흐름을 클러스터의 상시 리소스로 연결합니다. 독자가 바로 찾아볼 수 있도록 실제 위치를 기준으로 확인합니다.
| 리소스 | 위치 | 역할 |
|---|---|---|
| backup CronJob | tirosh-gitops/catalog/platform/nexus/base/backup-cronjob.yaml |
매일 03:00 Asia/Seoul에 backup image를 실행합니다. |
| backup RBAC | tirosh-gitops/catalog/platform/nexus/base/backup-rbac.yaml |
Nexus deployment scale/get/watch, Pod 조회, PVC 접근에 필요한 권한을 제공합니다. |
| restore Job 예시 | tirosh-gitops/catalog/platform/nexus/base/backup-restore-job.example.yaml |
운영에 상시 적용하지 않는 참고용 Job 형태입니다. |
| object storage ConfigMap | tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml |
endpoint, region, bucket, prefix를 제공합니다. |
| object storage Secret | tirosh-infra/sites/tirosh-home/secrets.toml |
local에서 infra-tools로 Kubernetes Secret을 생성합니다. 값은 GitOps에 commit하지 않습니다. |
| GHCR image pull Secret | tirosh-infra/sites/tirosh-home/secrets.toml |
private GHCR image pull용 kubernetes.io/dockerconfigjson Secret을 만듭니다. |
GitOps의 CronJob은 아래 흐름을 기준으로 구성됩니다.
schedule: "0 3 * * *"
timeZone: "Asia/Seoul"
containers:
- name: nexus-backup
image: ghcr.io/tirosh-chain/nexus-backup:latest
args:
- --namespace
- nexus
- --deployment
- nexus
- --selector
- app.kubernetes.io/name=nexus
- --data-path
- /nexus-data
- --bucket
- $(OBJECT_STORAGE_BUCKET)
- --prefix
- $(OBJECT_STORAGE_PREFIX)
- backup
여기서 중요한 점은 restore가 별도의 설정 파일을 다시 해석하지 않는다는 것입니다. infra-tools nexus-restore는 live CronJob/nexus-backup을 읽고, 그 CronJob의 image, env, volume, service account 설정을 복사해서 restore Job을 만듭니다. 그래서 backup과 restore는 같은 bucket, prefix, credential, PVC mount를 사용합니다.
7-3. Backup image¶
Nexus /nexus-data 백업/복구 도구는 tools/nexus-backup에 둡니다. object storage 접근은 packages/object-store-cli를 사용하고, 운영용 container image는 GHCR에 publish합니다.
| 대상 | 값 |
|---|---|
| CLI package | tools/nexus-backup |
| object storage package | packages/object-store-cli |
| image | ghcr.io/tirosh-chain/nexus-backup |
| workflow | .github/workflows/nexus-backup-image.yml |
| self-hosted validation | .github/workflows/nexus-backup-self-hosted-validation.yml |
image workflow는 package test, lint, wheel build, Docker build를 검증하고 main 또는 nexus-backup-v* tag에서 image를 publish합니다. workflow는 tirosh-ubuntu-latest label을 가진 self-hosted runner pool에서 실행합니다.
tirosh-home runner에서만 검증해야 하는 경우 self-hosted validation workflow를 수동 실행합니다.
GitHub Actions
-> nexus-backup-self-hosted-validation
-> Run workflow
현재 runner pool이 한 대뿐이면 image build와 frontend E2E가 동시에 밀릴 수 있으므로, GitHub CI 문서의 runner pool 확장 절차에 따라 github-ci-02 이후 VM을 추가합니다.
7-4. Object storage와 Secret¶
object storage 설정은 공개 가능한 연결 정보와 비밀값으로 나눕니다.
| 값 | 위치 | 관리 방식 |
|---|---|---|
OBJECT_STORAGE_ENDPOINT_URL |
tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml |
GitOps ConfigMap |
OBJECT_STORAGE_REGION |
tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml |
GitOps ConfigMap |
OBJECT_STORAGE_BUCKET |
tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml |
GitOps ConfigMap |
OBJECT_STORAGE_PREFIX |
tirosh-gitops/clusters/tirosh-home/k3s/platform/nexus/nexus-backup-object-store.configmap.yaml |
GitOps ConfigMap |
OBJECT_STORAGE_ACCESS_KEY_ID |
tirosh-infra/sites/tirosh-home/secrets.toml의 nexus-backup-object-store entry |
local env에서 Kubernetes Secret 생성 |
OBJECT_STORAGE_SECRET_ACCESS_KEY |
tirosh-infra/sites/tirosh-home/secrets.toml의 nexus-backup-object-store entry |
local env에서 Kubernetes Secret 생성 |
Object storage는 AWS/NCloud/MinIO 같은 S3-compatible provider를 같은 방식으로 다루기 위해
OBJECT_STORAGE_* env contract를 유지합니다. 로컬 검증에서만 AWS profile을 쓰고 싶다면
OBJECT_STORAGE_PROFILE 또는 object-store --profile <profile>을 사용할 수 있지만, Kubernetes
Secret으로 전달되는 운영 credential은 provider-neutral env 이름을 기준으로 관리합니다.
inventory.toml이나 site profile에는 bucket/prefix를 넣지 않습니다. 이 값은 Kubernetes에서 CronJob이 읽는 런타임 설정이고, infra-tools는 live CronJob을 기준으로 restore Job을 만들기 때문입니다.
GHCR package가 private이면 backup CronJob이 image를 pull할 수 있도록 image pull Secret도 먼저 등록합니다. token은 GitHub classic PAT이고 scope는 read:packages만 사용합니다.
organization 자체 token이 아니라, 개인 계정 또는 machine user에서 만든 token을 씁니다. Kubernetes Secret과 imagePullSecrets는 namespace-scoped이므로, 현재는 nexus-backup CronJob이 실행되는 nexus namespace에 Secret을 생성합니다.
export GHCR_USERNAME="<github-user-or-machine-user>"
export GHCR_PULL_TOKEN="<classic-pat-with-read-packages>"
make kubernetes/image-pull-secret/plan SITE=tirosh-home
make kubernetes/image-pull-secret/apply SITE=tirosh-home
unset GHCR_USERNAME GHCR_PULL_TOKEN
plan은 실제 token 값을 출력하지 않는 redacted manifest 미리보기입니다. apply는 sites/tirosh-home/secrets.toml의 ghcr-pull entry를 읽고, kubernetes.io/dockerconfigjson Secret을 nexus namespace에 적용합니다. GitOps 쪽 ServiceAccount 또는 Pod spec은 이 Secret을 imagePullSecrets로 참조해야 합니다.
7-5. Backup 확인¶
nexus-backup backup과 restore는 실제 Nexus deployment replica를 조정하고 /nexus-data를 archive 또는 overwrite합니다. 운영 namespace에서 dry-run처럼 실행하지 않습니다.
먼저 bucket, prefix, credential이 맞는지 object-store dry-run으로 확인합니다.
export OBJECT_STORAGE_ENDPOINT_URL="<object-storage-endpoint>"
export OBJECT_STORAGE_REGION="<region>"
export OBJECT_STORAGE_ACCESS_KEY_ID="<access-key>"
export OBJECT_STORAGE_SECRET_ACCESS_KEY="<secret-key>"
export OBJECT_STORAGE_BUCKET="<backup-bucket>"
export OBJECT_STORAGE_PREFIX="nexus/backups/rehearsal"
tmpdir="$(mktemp -d)"
printf 'nexus-backup rehearsal\n' > "$tmpdir/probe.txt"
object-store \
--bucket "$OBJECT_STORAGE_BUCKET" \
--prefix "$OBJECT_STORAGE_PREFIX/" \
--dry-run \
upload --source "$tmpdir"
object-store \
--bucket "$OBJECT_STORAGE_BUCKET" \
--prefix "nexus/backups/" \
list --json
GitOps가 적용된 뒤에는 CronJob/nexus-backup이 하루 한 번 실행됩니다. 수동 확인이 필요하면 마지막 Job의 성공 여부와 object storage prefix를 함께 봅니다.
kubectl -n nexus get cronjob nexus-backup
kubectl -n nexus get jobs -l app.kubernetes.io/name=nexus-backup
7-6. Restore rehearsal¶
restore rehearsal의 목표는 단순히 Pod가 ready가 되는지 보는 것이 아닙니다. 복구된 Nexus가 backup 시점의 repository, component, artifact를 실제로 제공하는지 확인해야 합니다.
권장 방식은 임시 namespace와 임시 PVC에 restore한 뒤, restored Nexus를 별도로 띄워 비교하는 것입니다. make nexus/restore/*는 운영 PVC overwrite용 명령이므로 rehearsal에서는 바로 사용하지 않습니다.
rehearsal에서 확인할 항목은 다음입니다.
| 확인 | 기준 |
|---|---|
| backup id | nexus/backups/<backup-id>/ prefix 생성 |
| archive | nexus-data.tar.gz 존재 |
| manifest | manifest.json 존재, deployment/image/namespace가 기대값 |
| checksum | sha256sum.txt 존재, restore 전 검증 통과 |
| ownership | 필요 시 --restore-owner 200:200으로 /nexus-data 권한 보정 |
| restore | Nexus deployment가 다시 ready 상태 |
| repository API | restored Nexus의 repository 목록과 component count가 기준 Nexus와 일치 |
| package serving | 대표 hosted repository의 index와 artifact download가 기준 Nexus와 일치 |
deployment ready는 restore의 최소 조건입니다. rehearsal에서는 port-forward로 기준 Nexus와 restored Nexus를 동시에 열고, hosted repository의 목록과 대표 artifact를 비교합니다.
curl -fsS -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
http://127.0.0.1:18081/service/rest/v1/repositories
curl -fsS -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
http://127.0.0.1:18081/repository/pypi-hosted/simple/
curl -fsS -u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
http://127.0.0.1:18081/repository/pypi-hosted/packages/<package>/<version>/<file> \
-o /tmp/restored-artifact
기준 Nexus에서 받은 같은 artifact와 checksum 또는 byte 비교가 일치하면, 복구된 Nexus가 backup 시점의 package를 실제로 serve한다고 볼 수 있습니다.
7-7. 운영 Restore 실행¶
운영 restore는 기존 nexus-data PVC를 덮어쓰는 operating PVC overwrite restore입니다. GitOps에 영구 Job으로 두지 않고, 필요할 때만 infra-tools로 one-off Job을 생성합니다.
먼저 manifest를 확인합니다.
make nexus/restore/plan \
SITE=tirosh-home \
BACKUP_ID=2026-06-19T092915Z
실제 restore는 명시 확인을 붙여 실행합니다. CONFIRM_RESTORE=true를 넘겨도 바로 진행하지 않고, 실행 중 경고 메시지에서 정확히 yes를 입력해야 합니다.
make nexus/restore/apply \
SITE=tirosh-home \
BACKUP_ID=2026-06-19T092915Z \
CONFIRM_RESTORE=true
WARNING: this is an operating PVC overwrite restore.
Type exactly 'yes' to continue.
Restore confirmation:
기본 restore owner는 200:200입니다. 필요하면 NEXUS_RESTORE_OWNER=<uid:gid>로 바꿉니다. restore가 끝나면 7-6의 API와 artifact serving 확인을 운영 Nexus에도 적용합니다.
8. 문제 확인¶
문제가 생기면 local secret, Nexus API, GitHub Actions 설정, 개발자 credential 순서로 확인합니다.
8-1. Local secret 확인¶
credential이 없으면 Make target이 실행 전에 멈춥니다.
error: NEXUS_ADMIN_USERNAME is required
hint: set it in .env or export it
이 경우 .env에 필요한 key가 있는지 확인합니다. 값 자체는 출력하지 않는 편이 안전합니다.
awk -F= '/^[A-Za-z_][A-Za-z0-9_]*=/ { print $1 }' .env
8-2. Nexus API¶
Nexus에 접속할 수 있는지 확인합니다.
curl -fsS https://nexus.internal.tirosh.ai/service/rest/swagger.json >/dev/null
API 인증이 실패하면 admin credential을 확인합니다.
set -a
. ./.env
set +a
curl -fsS \
-u "$NEXUS_ADMIN_USERNAME:$NEXUS_ADMIN_PASSWORD" \
https://nexus.internal.tirosh.ai/service/rest/v1/security/users?userId=github-actions
8-3. GitHub Actions¶
publish workflow가 Missing credentials로 실패하면 NEXUS_PYPI_PUBLISH_URL, NEXUS_PYPI_INDEX_URL, NEXUS_USERNAME, NEXUS_PASSWORD가 등록되어 있는지 확인합니다.
gh variable list
gh secret list
8-4. Developer auth¶
개발자가 uv sync에서 인증 실패를 보면 reader role과 환경 변수 이름을 확인합니다.
echo "$UV_INDEX_TIROSH_PYPI_USERNAME"
test -n "$UV_INDEX_TIROSH_PYPI_PASSWORD"