최초 업로드
This commit is contained in:
36
Dockerfile
Normal file
36
Dockerfile
Normal file
@@ -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"]
|
||||
93
README.md
Normal file
93
README.md
Normal file
@@ -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 배포
|
||||
```
|
||||
작성중
|
||||
```
|
||||
77
mapping.yaml
Normal file
77
mapping.yaml
Normal file
@@ -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"
|
||||
219
migrate.py
Normal file
219
migrate.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user