commit fde91012b64d678f8da2b553f8480bcc9e9c8642 Author: 변정훈 Date: Tue Dec 30 16:34:43 2025 +0900 최초 업로드 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..befa27f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# =============================== +# Stage 1: kubectl +# =============================== +FROM bitnami/kubectl AS kubectl + + +# =============================== +# Stage 2: runtime +# =============================== +FROM python:3.11-slim + +# kubectl 복사 +COPY --from=kubectl /opt/bitnami/kubectl/bin/kubectl /usr/local/bin/kubectl + +# 필수 패키지 +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# python 패키지 +RUN pip install --no-cache-dir ruamel.yaml + +# 작업 디렉토리 +WORKDIR /app + +# 스크립트 복사 +COPY migrate.py /app/migrate.py + +# 기본 mapping.yaml +COPY mapping.yaml /app/mapping.yaml + +# 실행 권한 +RUN chmod +x /app/migrate.py + +# 기본 엔트리포인트 +ENTRYPOINT ["python", "/app/migrate.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..ead12cf --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# 구성 (Local 실행 기준) +1. mapping.yaml + > 마이그레이션 할 어노테이션 정의가 작성 되는 파일 + ```yaml + # 예시 + nginx.ingress.kubernetes.io/ssl-redirect: + haproxy: haproxy.org/ssl-redirect + support: full + + nginx.ingress.kubernetes.io/force-ssl-redirect: + haproxy: haproxy.org/ssl-redirect + support: full + + nginx.ingress.kubernetes.io/backend-protocol: + haproxy: haproxy.org/backend-protocol + support: full + + nginx.ingress.kubernetes.io/load-balance: + haproxy: haproxy.org/load-balance + support: full + ``` + support 기준은 아래와 같음 + ```yaml + full : 1:1 매핑 되는 옵션 + # ex) + # nginx.ingress.kubernetes.io/ssl-redirect: "true" + # haproxy.org/ssl-redirect: "true" + + partial : 기능은 같으나, 방식이 다름 + # nginx.ingress.kubernetes.io/limit-rps: "10" + # haproxy.org/rate-limit-requests: "10" + # haproxy.org/warning-limit-rps: "burst/period semantics differ" + + unsupported : 지원 되지 않음 + + # ex) + # nginx.ingress.kubernetes.io/configuration-snippet: | + # more_set_headers "X-Test: foo"; + ``` + +2. migrate.py + > 실행 파일, 옵션은 아래와 같음 (pip install ruamel.yaml 실행 필요) + ```shell + --single : 하나의 파일로 결과물 생성 + --split : 각각의 yaml 파일로 저장 + --out : 저장 경로 설정 + --ingress-class : ingressClassName 설정 + --report : report 파일 저장 경로 + --mapping : 맵핑 파일 지정 (default : mapping.yaml) + ``` + +# 구성 (Kubernetes 실행 기준) +1. Dockerfile + > 이미지 빌드 수행 + +2. mapping.yaml 파일 configmap으로 Kubernetes에 배포 + ```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: ingress-annotation-mapping + namespace: default + data: + mapping.yaml: | + nginx.ingress.kubernetes.io/ssl-redirect: + haproxy: haproxy.org/ssl-redirect + support: full + + nginx.ingress.kubernetes.io/force-ssl-redirect: + haproxy: haproxy.org/ssl-redirect + support: full + + nginx.ingress.kubernetes.io/backend-protocol: + haproxy: haproxy.org/backend-protocol + support: full + + nginx.ingress.kubernetes.io/load-balance: + haproxy: haproxy.org/load-balance + support: full + + nginx.ingress.kubernetes.io/rewrite-target: + haproxy: haproxy.org/path-rewrite + support: full + + nginx.ingress.kubernetes.io/proxy-connect-timeout: + haproxy: haproxy.org/proxy-connect-timeout + support: full + ``` + +3. Pod 배포 + ``` + 작성중 + ``` diff --git a/mapping.yaml b/mapping.yaml new file mode 100644 index 0000000..6ece837 --- /dev/null +++ b/mapping.yaml @@ -0,0 +1,77 @@ +nginx.ingress.kubernetes.io/ssl-redirect: + haproxy: haproxy.org/ssl-redirect + support: full + +nginx.ingress.kubernetes.io/force-ssl-redirect: + haproxy: haproxy.org/ssl-redirect + support: full + +nginx.ingress.kubernetes.io/backend-protocol: + haproxy: haproxy.org/backend-protocol + support: full + +nginx.ingress.kubernetes.io/load-balance: + haproxy: haproxy.org/load-balance + support: full + +nginx.ingress.kubernetes.io/rewrite-target: + haproxy: haproxy.org/path-rewrite + support: full + +nginx.ingress.kubernetes.io/proxy-connect-timeout: + haproxy: haproxy.org/proxy-connect-timeout + support: full + +nginx.ingress.kubernetes.io/proxy-read-timeout: + haproxy: haproxy.org/proxy-read-timeout + support: full + +nginx.ingress.kubernetes.io/proxy-send-timeout: + haproxy: haproxy.org/proxy-send-timeout + support: full + +nginx.ingress.kubernetes.io/whitelist-source-range: + haproxy: haproxy.org/allow-list + support: full + +nginx.ingress.kubernetes.io/limit-rps: + haproxy: haproxy.org/rate-limit-requests + support: partial + note: "burst/period semantics differ" + +nginx.ingress.kubernetes.io/limit-rps-burst: + haproxy: haproxy.org/rate-limit-period + support: partial + +nginx.ingress.kubernetes.io/affinity: + haproxy: haproxy.org/affinity + support: full + +nginx.ingress.kubernetes.io/session-cookie-name: + haproxy: haproxy.org/session-cookie-name + support: full + +nginx.ingress.kubernetes.io/cors-allow-origin: + haproxy: haproxy.org/cors-allow-origin + support: full + +nginx.ingress.kubernetes.io/cors-allow-methods: + haproxy: haproxy.org/cors-allow-methods + support: full + +nginx.ingress.kubernetes.io/cors-allow-headers: + haproxy: haproxy.org/cors-allow-headers + support: full + +nginx.ingress.kubernetes.io/cors-expose-headers: + haproxy: haproxy.org/cors-expose-headers + support: full + +nginx.ingress.kubernetes.io/enable-cors: + haproxy: haproxy.org/enable-cors + support: full + +nginx.ingress.kubernetes.io/configuration-snippet: + haproxy: null + support: unsupported + note: "HAProxy does not support arbitrary NGINX snippets" diff --git a/migrate.py b/migrate.py new file mode 100644 index 0000000..18d17cd --- /dev/null +++ b/migrate.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +from copy import deepcopy +from pathlib import Path +from collections import defaultdict + +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap + + +# ----------------------------- +# YAML 설정 +# ----------------------------- +yaml = YAML() +yaml.preserve_quotes = True +yaml.indent(mapping=2, sequence=4, offset=2) + + +# ----------------------------- +# kubectl ingress 조회 +# ----------------------------- +def kubectl_ingress(): + out = subprocess.check_output( + ["kubectl", "get", "ingress", "--all-namespaces", "-o", "yaml"] + ) + return yaml.load(out) + + +# ----------------------------- +# mapping.yaml 로드 +# ----------------------------- +def load_mapping(path): + with open(path) as f: + return yaml.load(f) + + +# ----------------------------- +# ingress 변환 로직 +# ----------------------------- +def migrate_ingress(data, mapping, ingress_class=None): + report = { + "converted": [], + "partial": [], + "unsupported": [], + "detail": defaultdict(lambda: {"partial": [], "unsupported": []}), + } + + new = deepcopy(data) + + for item in new.get("items", []): + meta = item.setdefault("metadata", {}) + spec = item.setdefault("spec", {}) + + name = meta.get("name") + namespace = meta.get("namespace", "default") + ingress_id = f"{namespace}/{name}" + + if ingress_class: + spec["ingressClassName"] = ingress_class + + anns = meta.get("annotations", {}) or {} + new_anns = CommentedMap() + + for k, v in anns.items(): + if k in mapping: + rule = mapping[k] + support = rule.get("support", "unsupported") + haproxy_key = rule.get("haproxy") + + if support == "full": + new_anns[haproxy_key] = v + report["converted"].append(k) + + elif support == "partial": + new_anns[haproxy_key] = v + note = rule.get("note", "") + new_anns.yaml_set_comment_before_after_key( + haproxy_key, + before=f"PARTIAL SUPPORT: {note}", + ) + report["partial"].append(k) + report["detail"][ingress_id]["partial"].append( + { + "nginx": k, + "haproxy": haproxy_key, + "note": note, + } + ) + + else: + lines = [f"UNSUPPORTED {k}:"] + for line in str(v).splitlines(): + lines.append(f" {line}") + new_anns.yaml_set_end_comment("\n".join(lines)) + + report["unsupported"].append(k) + report["detail"][ingress_id]["unsupported"].append( + { + "nginx": k, + "value": v, + } + ) + else: + new_anns[k] = v + + meta["annotations"] = new_anns + + return new, report + + +# ----------------------------- +# 리포트 생성 (Markdown) +# ----------------------------- +def write_report(report, path): + with open(path, "w") as f: + f.write("# Ingress Migration Report\n\n") + f.write("## Summary\n") + f.write(f"- Converted : {len(report['converted'])}\n") + f.write(f"- Partial : {len(report['partial'])}\n") + f.write(f"- Unsupported: {len(report['unsupported'])}\n\n") + f.write("---\n\n") + + if any(v["partial"] for v in report["detail"].values()): + f.write("## ⚠️ Partial Support\n\n") + for ingress, items in report["detail"].items(): + if not items["partial"]: + continue + f.write(f"### {ingress}\n") + for p in items["partial"]: + f.write( + f"- {p['nginx']}\n" + f" - mapped to: {p['haproxy']}\n" + f" - note: {p['note']}\n" + ) + f.write("\n") + + if any(v["unsupported"] for v in report["detail"].values()): + f.write("## ❌ Unsupported\n\n") + for ingress, items in report["detail"].items(): + if not items["unsupported"]: + continue + f.write(f"### {ingress}\n") + for u in items["unsupported"]: + f.write(f"- {u['nginx']}\n") + f.write(" ```nginx\n") + f.write(f"{u['value']}\n") + f.write(" ```\n") + f.write("\n") + + +# ----------------------------- +# 파일 저장 +# ----------------------------- +def save_single(data, out_path): + with open(out_path, "w") as f: + yaml.dump(data, f) + + +def save_split(data, out_dir): + Path(out_dir).mkdir(parents=True, exist_ok=True) + for item in data.get("items", []): + name = item["metadata"]["name"] + namespace = item["metadata"].get("namespace", "default") + path = Path(out_dir) / f"{namespace}__{name}.yaml" + with open(path, "w") as f: + yaml.dump(item, f) + + +# ----------------------------- +# main +# ----------------------------- +def main(): + parser = argparse.ArgumentParser( + description="nginx ingress → HAProxy ingress migration utility" + ) + parser.add_argument("--mapping", default="mapping.yaml") + parser.add_argument("--single", action="store_true") + parser.add_argument("--split", action="store_true") + parser.add_argument("--out", default="output") + parser.add_argument("--ingress-class") + parser.add_argument( + "--report", + default="migration-report.md", + help="migration summary report file", + ) + + args = parser.parse_args() + + if not args.single and not args.split: + parser.error("one of --single or --split must be specified") + + ingress = kubectl_ingress() + mapping = load_mapping(args.mapping) + + converted, report = migrate_ingress( + ingress, + mapping, + ingress_class=args.ingress_class, + ) + + if args.single: + out_file = args.out if args.out.endswith(".yaml") else f"{args.out}.yaml" + save_single(converted, out_file) + + if args.split: + save_split(converted, args.out) + + write_report(report, args.report) + + print("\n=== Migration Summary ===") + print(f"Converted : {len(report['converted'])}") + print(f"Partial : {len(report['partial'])}") + print(f"Unsupported: {len(report['unsupported'])}") + print(f"Report : {args.report}") + + +if __name__ == "__main__": + main()