airflow fernet key 를 콘솔이 만든 Secret 으로 넘긴다 (#38)

차트가 fernet key Secret 을 직접 만들면 helm.sh/hook: pre-install +
hook-delete-policy: before-hook-creation 이 붙는다. ArgoCD 는 훅을 "삭제 후 재생성" 하는데,
변경이 없는 sync 에서는 삭제 직후 operation 이 종료되어 재생성되지 않는다. 컨트롤러 로그로
확인했다.

  변경 있는 sync  waiting for deletion ... -> serverside-applied -> Succeeded 'all tasks run'
  no-op sync      waiting for deletion ... -> Succeeded (같은 초)             'no more tasks'

파드는 이미 주입된 env 로 계속 돌아 증상이 즉시 드러나지 않지만, 재시작하는 순간
CreateContainerConfigError 로 기동하지 못한다. refresh·selfHeal 로 흔히 발생하는 상황이다.

fernetKeySecretName 을 주면 차트가 Secret 을 아예 만들지 않으므로
(templates/secrets/fernetkey-secret.yaml:23) 훅 자체가 사라지고, 모든 워크로드가 그 이름을
secretKeyRef 로 참조한다(templates/_helpers.yaml:395-397).

$FERNET_KEY_SECRET 은 placeholder 다. dip-console-api 가 <릴리스명>-fernet-key Secret 을
만들고(없을 때만) 이 자리를 그 이름으로 치환한다. 콘솔이 만든 Secret 은 ArgoCD 추적 대상이
아니라 sync 의 영향을 받지 않는다.

값이 아니라 이름을 넘기는 이유가 하나 더 있다 — fernet key 는 메타DB 의
Connection·Variable 암호화에 쓰이므로 tenant 레포(git)에 평문으로 남기지 않는다.
업스트림도 can only be set during install, not upgrade 라고 못박고 있어, 이미 배포된
테넌트는 재배포 시 현재 Secret 을 그대로 인수한다.

CUSTOM-README 5절은 이 문서의 성격(배포 방법 · custom-values.yaml 키 설명)에 맞게 키
테이블과 배포 절차 중심으로 다시 썼다(69줄 -> 48줄). 상세 근거는 이슈로 넘긴다.

redis 훅 Secret 2종은 CeleryExecutor 전환 예정이 없어 조치하지 않는다. 참조 워크로드가
0개인 것을 클러스터에서 확인했다. 전환 시 처리 방법만 문서에 남긴다.

검증

service-catalog 에 선반영해 테스트 클러스터에서 확인했다. 재배포 후 sync 를 걸었을 때 훅인
redis Secret 2종은 또 삭제됐고 콘솔이 관리하는 fernet Secret 만 살아남았다(resourceVersion
미동, 값 동일). 이 레포 기준으로도 helm template 2회 렌더에서 차트가 fernet Secret 을 만들지
않고 webserver 체크섬이 동일함을 확인했다.

관련: paasup/dip-catalog#38, paasup/dip-console-api#100

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
wbsong111
2026-08-21 13:56:36 +09:00
parent 235cfef2be
commit d3816a14ca
3 changed files with 56 additions and 61 deletions
+40 -61
View File
@@ -201,70 +201,49 @@ Airflow 웹서버는 Flask-AppBuilder 구조라 번들링 없이 CSS/JS를 개
### 5) 반복 재배포 방지 설정
`dip-values.yaml`에 있는 두 항목은 airflow가 반복 재배포되는 것을 막기 위한 이다. 원인이
서로 다르므로 따로 설명한다 (issue #29).
ArgoCD 배포에서 airflow가 반복 재배포되거나 Secret이 사라지는 것을 막기 위한 설정이다.
배경과 실측 근거는 paasup/dip-catalog#29 · #38 참고.
#### 5.1) `webserverSecretKey` — 렌더 결정성
| Name | 설명 | 기본값 |
| ---- | ---- | ------ |
| `webserverSecretKey` | 세션 서명 키. 비우면 차트가 렌더할 때마다 난수를 만들어 webserver가 계속 롤링되고 웹 세션이 끊긴다 | `~` |
| `fernetKeySecretName` | fernet key Secret 이름. 비우면 차트가 `pre-install` 훅으로 Secret을 만드는데, 변경이 없는 sync에서 삭제만 되고 재생성되지 않는다 | `~` |
| `migrateDatabaseJob.ttlSecondsAfterFinished` | `useHelmHooks: false`와 같이 두면 TTL 삭제 ↔ selfHeal 재생성 루프가 생긴다. `~`로 끈다 | `300` |
``` yaml
webserverSecretKey: "$WEBSERVER_SECRET_KEY"
```
- dip-values.yaml
``` yaml
webserverSecretKey: "$WEBSERVER_SECRET_KEY"
fernetKeySecretName: "$FERNET_KEY_SECRET"
- 이 값을 주지 않으면 차트가 템플릿 안에서 직접 난수를 만든다
(`templates/secrets/webserver-secret-key-secret.yaml`의 `randAlphaNum 32`).
- 그 Secret은 webserver·worker Deployment의 `checksum/webserver-secret-key` 애노테이션에
물려 있어서, **매니페스트를 다시 렌더할 때마다 Deployment spec이 바뀌고 롤링이 걸린다.**
실측에서 webserver가 반복 재배포되고 그때마다 웹 세션이 끊겼다. 캐시된 매니페스트를
그대로 쓰는 reconcile로는 일어나지 않지만, **일반 refresh만으로도 일어난다.**
- **차트 안에서는 못 고친다.** `lookup`으로 기존 Secret을 재사용하는 방법은 ArgoCD
repo-server가 클러스터 접근 없이 클라이언트 렌더를 하므로 항상 빈 값이 된다(클러스터에
Secret이 실제로 있는데도 빈 값인 것을 실측 확인). 릴리스명·네임스페이스 기반 결정적
파생은 렌더가 안정되는 대신 파생 입력이 전부 공개 정보라 **세션 서명 키를 예측할 수 있어**
부적합하다. 값이 차트 바깥에서 와야 한다.
- 그래서 `$WEBSERVER_SECRET_KEY`는 **placeholder**다. 배포 시점에 dip-console-api가 난수
치환한다 — `catalogs/catalogtype/catalog_airflow.go`의 `DeployPreInstall`
(crypto/rand 32바이트, URL-safe base64). 치환된 values가 tenant 레포에 저장되므로 이후
렌더는 그 고정값을 쓴다.
- **dip-console-api와 짝이다.** 한쪽만 반영되면 placeholder 문자열이 그대로 키가 되거나
(카탈로그만) 아무 일도 일어나지 않는다(console만).
- **helm으로 직접 배포할 때는** dip-console-api를 거치지 않으므로 치환이 일어나지 않는다.
`custom-values.yaml`에 실제 값을 직접 넣는다.
migrateDatabaseJob:
useHelmHooks: false
ttlSecondsAfterFinished: ~
```
- `$WEBSERVER_SECRET_KEY`와 `$FERNET_KEY_SECRET`는 placeholder다. dip-console-api가 배포
시점에 각각 난수 키와 Secret 이름으로 치환한다
(`catalogs/catalogtype/catalog_airflow.go`의 `DeployPreInstall`).
**카탈로그와 dip-console-api를 함께 반영해야 한다** — 한쪽만 반영하면 placeholder 문자열이
그대로 값이 되거나 아무 일도 일어나지 않는다.
- 같은 내용을 `manifests/applicationset/airflow/1.16.0/dip-values.yaml`(`---` 뒤 두 번째
YAML 문서)에도 반영한다.
- 이미 배포된 테넌트는 **재배포해야 적용된다.** fernet key는 재배포해도 기존 Secret을 그대
인수하므로 Connection·Variable은 유지된다. webserver 세션은 끊겨 다시 로그인해야 한다.
- **helm으로 직접 배포할 때**는 dip-console-api를 거치지 않으므로 두 값을 직접 넣는다.
``` sh
# 값 생성
# fernet key Secret 생성 (키 이름은 반드시 fernet-key)
kubectl create secret generic airflow-fernet-key -n airflow \
--from-literal=fernet-key="$(python3 -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')"
# webserver secret key 값 생성
python3 -c "import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())"
```
- **재배포하면 새 키가 생성되어 기존 웹 세션은 무효화된다** — 사용자는 다시 로그인해야 한다.
#### 5.2) `migrateDatabaseJob.ttlSecondsAfterFinished` — TTL ↔ selfHeal 루프
``` yaml
migrateDatabaseJob:
useHelmHooks: false
ttlSecondsAfterFinished: ~
```
- `useHelmHooks: false`로 두면 migrations Job이 helm hook이 아니라 **ArgoCD가 추적하는 일반
리소스**가 된다. ArgoCD 환경에서 hook을 끄라는 것은 업스트림 권고다
(`values.yaml` 주석: `Disable this if you are using ArgoCD for example`).
- 그런데 차트 기본값 `ttlSecondsAfterFinished: 300`이 그대로 남아 있으면, 완료 300초 뒤 TTL
컨트롤러가 Job을 지우고 ArgoCD는 그것을 OutOfSync로 보고 selfHeal로 다시 만든다.
**migrations job이 5분 주기로 재실행되는 루프**가 된다.
- 둘의 조합이 문제다. 하나만 있으면 발생하지 않는다.
- 리소스 수명은 ArgoCD가 관리하므로 **TTL을 끈다.** `~`(null)이면 템플릿의
`kindIs "invalid"` 가드에 걸려 `ttlSecondsAfterFinished` 필드 자체가 렌더되지 않는다
(`templates/jobs/migrate-database-job.yaml`). 대신 완료된 Job 객체가 네임스페이스에 남는다.
- `createUserJob`도 `ttlSecondsAfterFinished: 300` + `useHelmHooks: true`로 구조가 같지만,
hook이 살아 있어 ArgoCD 추적 대상이 아니므로 지금은 문제가 없다. `useHelmHooks`를 끄게
되면 TTL도 같이 꺼야 한다.
#### 5.3) 반영 위치가 2곳이다
같은 내용을 아래 두 파일에 모두 반영해야 한다. 한쪽만 고치면 배포 경로에 따라 증상이 남는다.
- `manifests/helm/airflow/1.16.0/dip-values.yaml`
- `manifests/applicationset/airflow/1.16.0/dip-values.yaml` (`---` 뒤 두 번째 YAML 문서)
#### 5.4) 이미 배포된 테넌트
tenant 레포에 저장된 values에는 이 설정이 없으므로, **재배포해야 적용된다.** 그전까지 기존
테넌트의 롤링·job 재실행 루프는 계속된다.
``` yaml
webserverSecretKey: "<위에서 생성한 값>"
fernetKeySecretName: "airflow-fernet-key"
```
- 참고: `<릴리스명>-redis-password` · `<릴리스명>-broker-url`도 같은 `pre-install` 훅이라
sync에서 사라진다. `executor: "KubernetesExecutor"`인 현재는 참조하는 워크로드가 없어
무해하다(다음 배포 때 자동으로 다시 생긴다). CeleryExecutor로 바꾼다면 차트가 제공하는
`redis.passwordSecretName` · `data.brokerUrlSecretName`으로 위 5.2와 같게 처리한다 —
`broker-url`은 redis 비밀번호를 DSN에 포함하므로 두 Secret을 함께 생성해야 한다.