From a8530d3968f8181edc70e0a71e2c894e29d7b76c Mon Sep 17 00:00:00 2001 From: wbsong111 Date: Tue, 18 Aug 2026 11:19:01 +0900 Subject: [PATCH] =?UTF-8?q?airflow:=20OIDC=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=EA=B2=BD=EB=A1=9C=20Ingress=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 세션이 없는 상태로 접속하면 Airflow UI 한 페이지가 /static 자원을 수십 개 동시에 요청하고, 각 요청이 저마다 APISIX openid-connect 로그인 플로우를 시작한다. 세션은 state를 하나만 보관하므로 콜백이 동시에 돌아오면 마지막 하나를 뺀 전부가 state 검증에 실패해 500이 난다. templates/webserver/webserver-ingress-static.yaml 은 업스트림 Apache Airflow 차트에 없는 PaaSup 추가 템플릿이다. dip.unauthenticatedPaths 가 있을 때만 애노테이션 없는 Ingress를 하나 더 렌더해 해당 경로를 인증 없이 통과시킨다. 애노테이션을 전부 비우는 것은 의도된 것으로, plugin-config-name 을 빼는 게 목적이고 cert-manager.io/* 까지 빼는 이유는 웹 Ingress와 같은 TLS Secret 을 두고 Certificate 를 중복 생성하지 않게 하기 위해서다. 값을 ingress.web 아래가 아니라 dip 아래에 두는 이유는 values.schema.json 의 ingress.web 이 additionalProperties: false 라 그 아래 새 키를 넣으면 스키마 검증에서 배포가 실패하기 때문이다. qdrant·litellm 의 dip.mainPath 와 같은 자리를 쓴다. webserver-ingress.yaml 과 동일한 조건으로 렌더하므로 Airflow 3.0 (webserver → api-server) 전환 시 함께 재작업이 필요하다. 업스트림 파일은 수정하지 않았고 fork 델타는 신규 템플릿 1개뿐이다. 이슈 #24 는 dip-console 이 차트 밖에서 생성하는 방안(B안)을 권고했으나, 실제로 증상이 재현되는 서비스가 airflow 하나로 좁혀져 차트 템플릿(A안)으로 갔다. dip-console 로 옮길 때는 dip.unauthenticatedPaths 를 비우면 된다. redirect_uri 고정은 여전히 dip-console 몫으로 남는다. Co-Authored-By: Claude Opus 5 (1M context) --- .../airflow/1.16.0/dip-values.yaml | 14 ++++ .../helm/airflow/1.16.0/CUSTOM-README.md | 39 +++++++++++ manifests/helm/airflow/1.16.0/dip-values.yaml | 14 ++++ .../webserver/webserver-ingress-static.yaml | 69 +++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 manifests/helm/airflow/1.16.0/templates/webserver/webserver-ingress-static.yaml diff --git a/manifests/applicationset/airflow/1.16.0/dip-values.yaml b/manifests/applicationset/airflow/1.16.0/dip-values.yaml index a7bdc14..4ff32ab 100644 --- a/manifests/applicationset/airflow/1.16.0/dip-values.yaml +++ b/manifests/applicationset/airflow/1.16.0/dip-values.yaml @@ -105,6 +105,20 @@ ingress: enabled: true secretName: "{{ .Name }}-tls-secret" +# OIDC 인증에서 제외할 경로. 위 plugin-config-name(oidc-keycloak-authz)이 걸린 웹 Ingress와 +# 별개로, 애노테이션 없는 Ingress를 하나 더 만들어 이 경로들을 인증 없이 통과시킨다 +# (templates/webserver/webserver-ingress-static.yaml — PaaSup 추가 템플릿). +# +# 세션이 없을 때 /static 자원 수십 개가 동시에 OIDC 로그인 플로우를 시작하면서 state가 +# 서로 덮어써져 500이 나는 문제를 막는다 (paasup/dip-catalog#24). +# Airflow의 /static은 공개 CSS/JS라 인증 제외에 보안상 문제가 없다. +# +# 비우면(또는 키를 지우면) 이 Ingress는 렌더되지 않는다 — 향후 dip-console이 같은 +# 리소스를 차트 밖에서 생성하게 되면 여기를 비워서 중복을 없앤다. +dip: + unauthenticatedPaths: + - /static + executor: "KubernetesExecutor" config: diff --git a/manifests/helm/airflow/1.16.0/CUSTOM-README.md b/manifests/helm/airflow/1.16.0/CUSTOM-README.md index 2fe03a2..2950f6a 100644 --- a/manifests/helm/airflow/1.16.0/CUSTOM-README.md +++ b/manifests/helm/airflow/1.16.0/CUSTOM-README.md @@ -120,6 +120,45 @@ logs: secretName: "airflow-tls" ``` +#### 3.3) OIDC 인증 제외 경로 (`dip.unauthenticatedPaths`) + +> **업스트림 차트에 없는 PaaSup 추가 템플릿이다.** +> `templates/webserver/webserver-ingress-static.yaml` — 차트 업그레이드 시 함께 이관한다. + +APISIX `openid-connect` 플러그인(`oidc-keycloak-authz`)을 거는 배포에서만 필요하다. +`dip-values.yaml` 계열에 아래 키가 있으면 **애노테이션 없는 Ingress를 하나 더** 만들어 +해당 경로를 인증 없이 통과시킨다. + +``` yaml +dip: + unauthenticatedPaths: + - /static +``` + +**왜 필요한가** — 세션이 없는 상태로 접속하면 Airflow UI 한 페이지가 `/static` 자원을 +수십 개 *동시에* 요청한다. 각 요청이 저마다 OIDC 로그인 플로우를 시작하면서 새 `state`를 +발급해 세션에 덮어쓰는데, 세션은 `state`를 하나만 보관하므로 콜백이 동시에 돌아오면 +마지막 하나를 뺀 전부가 state 검증에 실패해 **500**이 난다 (paasup/dip-catalog#24). + +Airflow 웹서버는 Flask-AppBuilder 구조라 번들링 없이 CSS/JS를 개별로 받는다. 동시 요청 +수가 커서 이 경합이 실제로 터지는 이유이고, 번들링하는 SPA 계열 서비스에서 같은 증상이 +보고되지 않은 이유이기도 하다. + +**주의** + +- 이 Ingress는 애노테이션을 **하나도** 붙이지 않는다. `plugin-config-name`을 빼는 게 목적이고, + `cert-manager.io/*`까지 빼는 이유는 웹 Ingress와 같은 TLS Secret을 두고 Certificate를 + 중복 생성하지 않게 하기 위해서다. `tls` 블록도 두지 않는다 — 같은 호스트의 SNI 인증서는 + 웹 Ingress가 이미 제공한다. +- 값을 `ingress.web` 아래에 두면 **배포가 실패한다.** `values.schema.json`의 `ingress.web`이 + `additionalProperties: false`라 새 키가 스키마 검증에서 거부된다. 그래서 카탈로그 공통 + `dip:` 네임스페이스(qdrant·litellm의 `dip.mainPath`와 같은 자리)에 둔다. +- 웹 Ingress는 `path: /.*` + `use-regex: "true"`이고 이 Ingress는 literal `/static` Prefix다. + APISIX가 더 구체적인 쪽을 고르는 것에 의존한다. 수동 적용으로 실측 확인된 동작이지만 + (`/static/dist/d3.min.js` 200, `/home` 302), APISIX 업그레이드 후에는 재확인한다. +- 키를 비우거나 지우면 이 Ingress는 렌더되지 않는다. 향후 dip-console이 같은 리소스를 + 차트 밖에서 생성하게 되면 여기를 비워 중복을 없앤다 (issue #24의 B안). + ### 4) Postgresql 설정 - 내장 bitnami postgresql 서브차트 대신, `cnpg-cluster`(전용 인스턴스)를 외부 DB로 사용한다. diff --git a/manifests/helm/airflow/1.16.0/dip-values.yaml b/manifests/helm/airflow/1.16.0/dip-values.yaml index 3652c1c..6d2f1f1 100644 --- a/manifests/helm/airflow/1.16.0/dip-values.yaml +++ b/manifests/helm/airflow/1.16.0/dip-values.yaml @@ -14,6 +14,20 @@ ingress: enabled: true secretName: "{{ .Name }}-tls-secret" +# OIDC 인증에서 제외할 경로. 위 plugin-config-name(oidc-keycloak-authz)이 걸린 웹 Ingress와 +# 별개로, 애노테이션 없는 Ingress를 하나 더 만들어 이 경로들을 인증 없이 통과시킨다 +# (templates/webserver/webserver-ingress-static.yaml — PaaSup 추가 템플릿). +# +# 세션이 없을 때 /static 자원 수십 개가 동시에 OIDC 로그인 플로우를 시작하면서 state가 +# 서로 덮어써져 500이 나는 문제를 막는다 (paasup/dip-catalog#24). +# Airflow의 /static은 공개 CSS/JS라 인증 제외에 보안상 문제가 없다. +# +# 비우면(또는 키를 지우면) 이 Ingress는 렌더되지 않는다 — 향후 dip-console이 같은 +# 리소스를 차트 밖에서 생성하게 되면 여기를 비워서 중복을 없앤다. +dip: + unauthenticatedPaths: + - /static + executor: "KubernetesExecutor" config: diff --git a/manifests/helm/airflow/1.16.0/templates/webserver/webserver-ingress-static.yaml b/manifests/helm/airflow/1.16.0/templates/webserver/webserver-ingress-static.yaml new file mode 100644 index 0000000..3fd6c41 --- /dev/null +++ b/manifests/helm/airflow/1.16.0/templates/webserver/webserver-ingress-static.yaml @@ -0,0 +1,69 @@ +{{/* + PaaSup 추가 템플릿 — 업스트림 Apache Airflow 차트에는 없다. + + 목적: APISIX openid-connect 플러그인을 우회하는 인증 제외 경로 전용 Ingress. + + 세션이 없는 상태에서 Airflow UI 한 페이지가 /static 자원을 수십 개 *동시에* 요청하면 + 각 요청이 저마다 OIDC 로그인 플로우를 시작한다. 세션은 state를 하나만 보관하므로 + 콜백이 동시에 돌아오면 마지막 하나를 뺀 전부가 state 검증에 실패해 500이 난다. + Airflow 웹서버는 Flask-AppBuilder 구조라 번들링 없이 CSS/JS를 개별로 받기 때문에 + 동시 요청 수가 커서 이 경합이 실제로 터진다 (paasup/dip-catalog#24). + + 이 Ingress는 애노테이션을 하나도 붙이지 않는다. 의도된 것이다: + - k8s.apisix.apache.org/plugin-config-name 미지정 → OIDC 플러그인 미적용 (이 파일의 목적) + - cert-manager.io/* 미지정 → 웹 Ingress와 같은 TLS Secret을 두고 Certificate를 중복 + 생성하지 않는다 + tls 블록도 두지 않는다. 같은 호스트의 SNI 인증서는 웹 Ingress가 이미 제공한다. + + 업스트림 차트 업그레이드 시 이 파일을 함께 이관한다. webserver-ingress.yaml과 동일한 + 조건으로 렌더하므로 Airflow 3.0(webserver → api-server) 전환 시 함께 재작업해야 한다. + + 값은 ingress.web 이 아니라 dip 아래에 둔다 — values.schema.json 의 ingress.web 이 + additionalProperties: false 라 그 아래 새 키를 넣으면 스키마 검증에서 배포가 실패한다. +*/}} +{{- if and .Values.webserver.enabled (semverCompare "<3.0.0" .Values.airflowVersion) }} +{{- if or .Values.ingress.web.enabled .Values.ingress.enabled }} +{{- $paths := (.Values.dip | default dict).unauthenticatedPaths }} +{{- if $paths }} +{{- $fullname := (include "airflow.fullname" .) }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullname }}-ingress-unauth + labels: + tier: airflow + component: airflow-ingress + release: {{ .Release.Name }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: {{ .Release.Service }} + {{- if or (.Values.labels) (.Values.webserver.labels) }} + {{- mustMerge .Values.webserver.labels .Values.labels | toYaml | nindent 4 }} + {{- end }} +spec: + rules: + {{- range .Values.ingress.web.hosts | default (list .Values.ingress.web.host) }} + {{- $hostname := . -}} + {{- if . | kindIs "string" | not }} + {{- $hostname = .name -}} + {{- end }} + - http: + paths: + {{- range $paths }} + - path: {{ . | quote }} + pathType: Prefix + backend: + service: + name: {{ $fullname }}-webserver + port: + name: airflow-ui + {{- end }} + {{- if $hostname }} + host: {{ tpl $hostname $ | quote }} + {{- end }} + {{- end }} + {{- if .Values.ingress.web.ingressClassName }} + ingressClassName: {{ .Values.ingress.web.ingressClassName }} + {{- end }} +{{- end }} +{{- end }} +{{- end }}