OpenStack i18n 번역 자동화 시스템에서는 Weblate API 인증 정보와 같은 민감한 데이터를 안전하게 관리하고 전달하기 위해 Zuul CI의 Secret 관리 체계를 활용한다. 이번 글에서는 암호화된 Secret이 Zuul 파이프라인을 거쳐 최종적으로 실행 스크립트까지 전달되는 전체 과정을 설명하고자 한다.
Secret 전달의 전체 흐름
Zuul에서 Secret이 전달되는 과정은 다음과 같은 순서를 따른다.
[Secret 정의] → [Job 연결] → [Playbook 환경변수 설정] → [Shell 스크립트 사용]
각 단계에서는 데이터의 형식과 전달 방식이 조금씩 달라지며, 보안이 유지되도록 설계되어 있다.
1) Secret 정의 (secrets.yaml)
파일 위치: project-config/secrets.yaml at master - project-config - OpenDev: Free Software Needs Free Tools
Secret은 YAML 파일 내에서 정의되며, 민감한 데이터를 포함한 키-값 쌍으로 구성된다. 하지만 평문으로 저장되지는 않는다. Zuul은 Secret 파일을 커밋할 때 자동으로 **공개키 기반 암호화(PKCS#1 OAEP)**를 적용하며, 이때 사용되는 공개키는 Zuul이 각 테넌트별로 자체적으로 관리한다.
복호화는 Zuul 내부의 스케줄러 프로세스에서만 가능하며, 해당 테넌트의 **비밀키(private key)**를 사용하여 메모리 상에서 복호화된다.
이 Secret 파일은 Git 저장소에 커밋되지만 복호화 전까지는 어떤 값이든 노출되지 않는다.
2) Job 정의에서 Secret 연결
파일 위치: project-config/jobs.yaml at master - project-config - OpenDev: Free Software Needs Free Tools
Job 정의 파일에서는 해당 Job이 어떤 Secret을 사용할 것인지 명시적으로 선언해야 한다. 이를 통해 Zuul은 해당 Job이 실행되기 전, 필요한 Secret을 복호화하고 Ansible 변수로 전달할 준비를 하게 된다.
여기서 중요한 점은 개발자가 직접 Secret의 값을 복호화하거나 변수로 지정할 필요가 없다는 것이다. 단지 Secret의 이름만 선언하면 Zuul이 Job 실행 전에 해당 Secret을 메모리에 복호화하고, 내부적으로 적절한 Ansible 변수로 자동 변환해준다.
Zuul의 Secret 처리 로직
Secret이 Job에 연결되면, Zuul은 다음의 순서로 데이터를 처리한다.
-
Zuul 스케줄러는 해당 Job의
secrets항목을 파싱한다. -
필요한 Secret 파일이 존재하는지 확인하고 접근 권한을 검증한다.
-
복호화는 Job 실행 직전에 이루어지며, 암호화된 데이터는 해당 테넌트의 비밀키로만 복호화된다.
-
복호화된 결과는 디스크에 저장되지 않고 메모리에서만 존재한다.
-
이 값들은 각각 개별적인 Ansible 변수로 변환된다.
예를 들어, 다음과 같이 Secret이 정의되어 있다면
- secret:
name: weblate_api_credentials
data:
url: https://openstack.weblate.cloud/
key: xxxxxxxx
이를 Job에 연결하면 아래와 같은 변수들이 자동 생성된다.
-
{{ weblate_api_url }} -
{{ weblate_api_key }}
변수명은 Secret 이름 없이 필드 이름만 사용되며, Playbook 내에서 바로 사용할 수 있도록 가공된다.
3) Playbook에서 환경변수 설정
기존 Zanata 기반 번역 워크플로우에서는 인증 정보를 zanata.ini 템플릿 파일을 통해 전달하는 구조이다. Zuul에서 Secret을 복호화한 다음 환경변수 형태로 전달되는 게 아니라, **Ansible 템플릿 시스템(Jinja2)**을 이용해 zanata.ini 설정 파일을 동적으로 생성하고, 그 파일이 실제 Zanata CLI의 인증 설정으로 사용되는 것이다.
실제 zanata.ini 구성
[servers]
{{ zanata_api_credentials.server_id }}.url={{ zanata_api_credentials.url }}
{{ zanata_api_credentials.server_id }}.username={{ zanata_api_credentials.username }}
{{ zanata_api_credentials.server_id }}.key={{ zanata_api_credentials.key }}
이 구성은 하나의 Secret (zanata_api_credentials) 안에 여러 개의 키(server_id, url, username, key)가 포함되어 있다는 전제 하에 동작한다. 이 zanata.ini는 워크 디렉토리에 저장되며, 이후 zanata-cli 실행 시 자동으로 사용된다.
전체 흐름을 정리하자면 아래와 같다.
[secrets.yaml] → [Job secrets 선언] → [Ansible template: zanata.ini.j2] → [zanata.ini 생성] → [zanata-cli 실행]
Zanata 기반 워크플로우는 환경변수보다는 클라이언트 설정 파일 기반 접근을 사용하였으며, 이를 위해 Ansible의 템플릿 렌더링 기능을 적극 활용하는 구조인 것이다.
반면에 Weblate에서는 인증 정보를 설정 파일에 작성하지 않고 환경변수로 직접 전달하는 구조를 사용한다. Weblate는 RESTful API 기반으로 동작하며, 인증 또한 HTTP 헤더에 API 키를 포함시키는 방식이기 때문에 별도의 설정 파일 없이도 동작이 가능하다.
즉, Zuul에서 Secret을 복호화한 후 Ansible Playbook에서 이 값을 환경변수로 주입한 다음 스크립트에서 이를 직접 참조하는 구조이다. 이를 통해 설정 파일 없이도 API 키를 안전하게 전달하고, 인증된 요청을 수행할 수 있게 된다.
이 흐름은 기존의 Zanata 방식과는 조금 다르기 때문에, Weblate 버전의 환경변수 전달 작업은 https://review.opendev.org/c/openstack/project-config/+/961499 패치에서 진행하고 있다.
Ansible은 자체적으로 {{ variable_name }} 형태의 템플릿 시스템을 사용하지만, 실제 셸 스크립트는 해당 변수에 접근할 수 없다. 셸 스크립트는 운영체제 수준의 환경변수($VAR)만 인식하므로, Ansible 변수 → 환경변수로의 매핑 작업이 필요하다.
이를 위해 Ansible의 environment 블록을 사용하여 환경변수를 주입한다.
- name: Run propose_translation_update_weblate.sh script
command: "{{ ansible_user_dir }}/scripts/propose_translation_update_weblate.sh {{ zuul.project.short_name }} {{ zuul.branch }}"
args:
chdir: "{{ zuul.project.src_dir }}"
environment:
WEBLATE_URL: "{{ url }}"
WEBLATE_TOKEN: "{{ key }}"
여기서 주의할 점은 Ansible 변수명은 소문자 + 언더스코어, 반면 환경변수는 일반적으로 대문자 + 언더스코어 형식을 따른다는 것이다.
4) 셸 스크립트에서 환경변수 사용
Weblate 버전(패치셋): https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/921878/19/roles/prepare-weblate-client/files/propose_translation_update_weblate.sh
최종적으로 셸 스크립트는 아무 설정 없이도 환경변수를 바로 사용할 수 있다. 예를 들어 다음과 같이 사용할 수 있다.
#!/bin/bash
curl -H "Authorization: Token $WEBLATE_API_KEY" \
"$WEBLATE_API_URL/projects/i18n/translations/"
이 시점에서 WEBLATE_API_KEY는 이미 Zuul → Ansible → Shell 환경으로 안전하게 전달된 상태이므로, 인증을 포함한 모든 API 호출에 활용할 수 있다.
5) 보안은 어떻게 유지되는가?
Zuul의 Secret 전달 체계는 매우 철저하게 설계되어 있다.
-
복호화된 데이터는 Job이 실행되는 노드의 메모리에만 존재하며, 디스크에 저장되지 않는다.
-
Job 실행이 끝나면 즉시 메모리에서 제거된다.
-
스크립트에서 실수로 환경변수를
echo하거나 API 로그에 민감 값이 포함되더라도, Zuul은 이를 탐지하여 자동으로***로 마스킹한다.- 따라서 로그를 통해 인증 정보가 유출되는 사고를 방지할 수 있다.
-
Secret은 선언된 Job 내에서만 사용 가능하며, 다른 Job이나 다른 프로젝트에서는 접근할 수 없다.
- 즉, 같은 테넌트 내에서도 Secret은 Job 단위로 범위가 제한된다.
정리
-
Zuul의 Secret은 공개키로 암호화되어 Git에 저장되고, Job 실행 시 복호화된다.
-
복호화된 Secret은 Ansible 변수로 전달되며, 필요에 따라 설정 파일 또는 환경변수로 매핑된다.
-
Zanata는
.ini파일 기반 인증, Weblate는 REST API 기반 환경변수 인증을 사용한다. -
Job 실행이 끝나면 Secret은 메모리에서 완전히 제거되며, 로그 출력 시 자동 마스킹된다.
-
Secret은 Job 범위 내에서만 접근 가능하며, 프로젝트 간 격리가 철저히 보장된다.


