NGINX Gateway Fabric 오프라인 설치: Harbor 등록부터 Gateway API 병행 검증까지
기존 Ingress 경로를 유지하면서 별도 NodePort로 NGF를 설치하고 검증한다. 내부 도메인·IP·포트·업무명·계정·경로는 일관된 예시 값으로 변경했다.

1. 설치 범위와 준비사항
반입한 NGF 2.7.0 자료를 기준으로 설명한다. 이 버전을 최신 버전으로 단정하지 않으며, 설치 전 선택한 릴리스의 Kubernetes·Gateway API 호환성을 확인한다. Helm Chart와 컨트롤 플레인·데이터 플레인 이미지는 같은 릴리스로 준비한다.
Bash, kubectl, Helm, ctr, Python 3, jq, curl, OpenSSL, rg가 필요하다. kubectl은 대상 클러스터의 관리 권한을 사용하고 ctr은 실제 Kubernetes containerd 소켓에 연결되어야 한다. 배치 작업을 피해 승인된 작업 시간에 진행한다.
- 작업 경로: /opt/ngf-install
- Harbor: registry.example.com:5443/platform
- 기존 경로: 192.0.2.10:31443
- 새 노드 예시: 192.0.2.21, 192.0.2.22
- 새 HTTP / HTTPS NodePort: 32080 / 32443
- Gateway 리스너: 8080 / 8443
- 업무 Service 포트: 8080
- 도메인: *.example.com / *.example.net
- 네임스페이스: edge-control, edge-gateway, app-one부터 app-six
IP와 도메인은 문서용 예시다. 업무 네임스페이스와 Service는 이미 존재하며 8080 포트를 제공한다는 전제다. backendRefs.port는 Service의 port를 사용한다. 공식 API 그룹과 제품명, 표준 필드명, 공식 이미지 경로는 실행 의미를 유지하기 위해 그대로 사용한다.
export WORK_DIR=/opt/ngf-install
export OLD_NODE=192.0.2.10
export NEW_NODE=192.0.2.21
export REGISTRY=registry.example.com:5443
export NGF_VERSION=2.7.0
mkdir -p "$WORK_DIR/backup"
chmod 700 "$WORK_DIR/backup"
kubectl config current-context
kubectl get nodes -o wide
helm version
ctr version
kubectl get svc -A -o json | jq -r '.items[] | .metadata.namespace as $ns | .metadata.name as $name | .spec.ports[]? | select(.nodePort != null) | [$ns,$name,.nodePort] | @tsv'
2. 기존 설정 백업과 기준 응답
legacy-ingress는 기존 컨트롤러 네임스페이스의 예시 이름이다. 실제 위치를 확인해 사용한다. 기존 루트 경로가 로그인 화면으로 이동한다면 정상 리다이렉트가 기준값일 수 있다. 백업에는 내부 정보가 포함될 수 있으므로 공개 자료에 포함하지 않는다.
umask 077
kubectl get ingress -A -o yaml > "$WORK_DIR/backup/ingress-all.yaml"
kubectl get deploy,svc,cm -n legacy-ingress -o yaml \
> "$WORK_DIR/backup/legacy-controller.yaml"
curl -skI --max-time 10 \
--resolve "portal.example.com:31443:$OLD_NODE" \
https://portal.example.com:31443/
3. 반입 자료 확인과 이미지 등록
반입 묶음은 로컬 Helm Chart, 이미지 아카이브, 해당 릴리스와 호환되는 Gateway API 표준 CRD 파일을 포함한다. 두 이미지 외에 설치 Job 등에서 사용하는 추가 이미지도 확인해 반입한다. 인증서 오류는 내부 CA 신뢰 설정으로 해결한다.
cd "$WORK_DIR"
tar -tzf ngf-offline.tgz
tar -xzf ngf-offline.tgz
ls -l ngf-images.tar standard-install.yaml nginx-gateway-fabric/Chart.yaml
helm show chart ./nginx-gateway-fabric
helm show values ./nginx-gateway-fabric > chart-values.yaml
ctr -n k8s.io images import ngf-images.tar
ctr -n k8s.io images ls
ctr -n k8s.io images tag \
"ghcr.io/nginx/nginx-gateway-fabric:$NGF_VERSION" \
"$REGISTRY/platform/nginx-gateway-fabric:$NGF_VERSION"
ctr -n k8s.io images tag \
"ghcr.io/nginx/nginx-gateway-fabric/nginx:$NGF_VERSION" \
"$REGISTRY/platform/nginx-gateway-fabric/nginx:$NGF_VERSION"
# 비밀번호 입력을 요청하는 ctr 환경에서 계정명만 전달
ctr -n k8s.io images push --user registry-user \
"$REGISTRY/platform/nginx-gateway-fabric:$NGF_VERSION"
ctr -n k8s.io images push --user registry-user \
"$REGISTRY/platform/nginx-gateway-fabric/nginx:$NGF_VERSION"
4. Chart와 Gateway API CRD 확인
값 이름은 반입한 Chart를 기준으로 확인한다. CRD가 이미 설치되어 있다면 사용 리소스와 호환성을 점검한 뒤 적용한다. 기존 CRD가 없어야만 정상인 것은 아니다. diff의 종료 코드 1은 차이 존재를 뜻하므로 오류와 구분한다.
rg -n 'replicas:|externalTrafficPolicy|nodePorts|listenerPort|imagePullSecret|pullPolicy|gatewayClassName' \
"$WORK_DIR/chart-values.yaml"
kubectl get crd -o name | rg 'gateway.networking.k8s.io' || true
kubectl get gatewayclass
kubectl diff -f "$WORK_DIR/standard-install.yaml"
# 차이와 기존 사용 리소스 검토 후 적용
kubectl apply -f "$WORK_DIR/standard-install.yaml"
kubectl wait --for=condition=Established --timeout=60s \
crd/gateways.gateway.networking.k8s.io \
crd/httproutes.gateway.networking.k8s.io
5. NGF Helm 설치
아래 GatewayClass와 이미지 인증 설정 키를 반입 Chart에서 확인한 뒤 사용한다. 특히 nginxGateway.imagePullSecret, nginx.imagePullSecret 및 데이터 플레인 인증 전달 방식은 선택한 릴리스 기준으로 확인한다. 인증 파일은 승인된 방법으로 준비하고 비밀번호를 명령에 직접 넣지 않는다.
멀티 아키텍처 인덱스의 일부 콘텐츠가 아카이브에 없다면 올바른 단일 아키텍처 자료를 다시 준비한다. tag만 바꾸어 누락 콘텐츠 문제를 해결할 수는 없다. Harbor를 사용하지 않는다면 제어·데이터 플레인과 설치 Job이 배치될 모든 노드에 필요한 이미지를 반입하고 Pull 정책을 확인한다.
kubectl create namespace edge-control
kubectl create namespace edge-gateway
for ns in edge-control edge-gateway; do
kubectl create secret generic registry-auth -n "$ns" \
--type=kubernetes.io/dockerconfigjson \
--from-file=.dockerconfigjson=/opt/registry-auth/config.json
done
cat > "$WORK_DIR/ngf-values.yaml" <<'EOF'
nginxGateway:
gatewayClassName: public-edge
image:
repository: registry.example.com:5443/platform/nginx-gateway-fabric
tag: "2.7.0"
imagePullSecret: registry-auth
nginx:
replicas: 2
image:
repository: registry.example.com:5443/platform/nginx-gateway-fabric/nginx
tag: "2.7.0"
imagePullSecret: registry-auth
service:
type: NodePort
externalTrafficPolicy: Local
nodePorts:
- port: 32080
listenerPort: 8080
- port: 32443
listenerPort: 8443
EOF
helm template edge-fabric "$WORK_DIR/nginx-gateway-fabric" \
-n edge-control -f "$WORK_DIR/ngf-values.yaml" \
> "$WORK_DIR/rendered.yaml"
rg -n 'image:|imagePullSecrets:|kind: GatewayClass|public-edge' \
"$WORK_DIR/rendered.yaml"
# 렌더링된 이미지, GatewayClass 이름, 인증 설정을 확인한 뒤 설치
helm install edge-fabric "$WORK_DIR/nginx-gateway-fabric" \
-n edge-control -f "$WORK_DIR/ngf-values.yaml" \
--wait --timeout 5m
kubectl get pods,jobs -n edge-control
kubectl get gatewayclass public-edge -o yaml
kubectl get nginxproxy -A
6. TLS Secret 복사
인증서 데이터를 파이프로 전달하고 개인키 파일을 생성하지 않는다. app-one의 예시 인증서는 *.example.com과 *.example.net을 각각 포함해야 한다. 목적지에 같은 이름의 Secret이 있으면 덮어쓰기 전에 확인한다.
for secret in tls-example-com tls-example-net; do
kubectl get secret "$secret" -n app-one -o json \
| jq --arg ns edge-gateway \
'{apiVersion:"v1",kind:"Secret",metadata:{name:.metadata.name,namespace:$ns},type:.type,data:.data}' \
| kubectl create -f -
done
# 인증서와 개인키의 해시를 각각 비교
for secret in tls-example-com tls-example-net; do
for field in 'tls.crt' 'tls.key'; do
for ns in app-one edge-gateway; do
printf '%s/%s %s ' "$ns" "$secret" "$field"
kubectl get secret "$secret" -n "$ns" -o json \
| jq -r --arg field "$field" '.data[$field]' \
| base64 -d | sha256sum
done
done
done
7. Gateway 생성
두 도메인에 각각 HTTP와 HTTPS 리스너를 생성한다. allowedRoutes는 라벨로 허용한 네임스페이스만 연결한다. 아래 Python 코드는 Kubernetes가 지원하는 JSON 매니페스트를 생성한다.
replicas=2만으로 서로 다른 노드 배치가 보장되지는 않는다. Pod 위치를 확인하고 필요하면 릴리스에서 지원하는 affinity·topology spread 설정을 적용한다. Local 정책에서는 준비된 로컬 Endpoint가 없는 노드의 NodePort로 요청하면 처리되지 않을 수 있다.
for ns in app-one app-two app-three app-four app-five app-six edge-gateway; do
kubectl label namespace "$ns" gateway-access=enabled --overwrite
done
python3 - <<'PY'
import json, os
from pathlib import Path
listeners = []
for suffix, domain, cert in [
("com", "*.example.com", "tls-example-com"),
("net", "*.example.net", "tls-example-net"),
]:
for protocol, port in [("HTTP", 8080), ("HTTPS", 8443)]:
listener = {
"name": protocol.lower() + "-" + suffix,
"port": port,
"protocol": protocol,
"hostname": domain,
"allowedRoutes": {"namespaces": {
"from": "Selector",
"selector": {
"matchLabels": {"gateway-access": "enabled"}
}
}}
}
if protocol == "HTTPS":
listener["tls"] = {
"mode": "Terminate",
"certificateRefs": [{"kind": "Secret", "name": cert}]
}
listeners.append(listener)
gateway = {
"apiVersion": "gateway.networking.k8s.io/v1",
"kind": "Gateway",
"metadata": {
"name": "public-gateway",
"namespace": "edge-gateway"
},
"spec": {
"gatewayClassName": "public-edge",
"listeners": listeners
}
}
Path(os.environ["WORK_DIR"], "gateway.json").write_text(
json.dumps(gateway, indent=2) + "\n"
)
PY
kubectl apply --dry-run=server -f "$WORK_DIR/gateway.json"
kubectl apply -f "$WORK_DIR/gateway.json"
kubectl wait --for=condition=Programmed --timeout=180s \
gateway/public-gateway -n edge-gateway
kubectl get gateway public-gateway -n edge-gateway -o yaml
kubectl get pods,deploy,svc -n edge-gateway -o wide
8. 요청 본문 제한 설정
Gateway와 같은 네임스페이스에 Policy를 생성한다. maxSize 100m은 요청 본문 전체 제한이다. multipart 부가 데이터와 애플리케이션 자체 제한도 확인한다. Policy의 Accepted 조건을 확인하며 Gateway의 Programmed 조건과 구분한다.
cat > "$WORK_DIR/client-settings.yaml" <<'EOF'
apiVersion: gateway.nginx.org/v1alpha1
kind: ClientSettingsPolicy
metadata:
name: request-body-policy
namespace: edge-gateway
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: public-gateway
body:
maxSize: 100m
EOF
kubectl apply --dry-run=server -f "$WORK_DIR/client-settings.yaml"
kubectl apply -f "$WORK_DIR/client-settings.yaml"
kubectl get clientsettingspolicy request-body-policy -n edge-gateway -o yaml
9. HTTPRoute 7개 생성
업무 Route 6개가 13개 호스트를 처리하고 HTTP 리다이렉트 Route 1개를 추가한다. app-two는 example.com에만 연결한다. HSTS의 includeSubDomains는 관련 하위 도메인 전체가 HTTPS를 지원하는지 확인한 뒤 사용한다.
직접 NodePort로 검증하므로 HTTPS 리다이렉트 목적지 포트를 32443으로 지정한다. 외부 LB로 전환할 때는 실제 외부 HTTPS 포트에 맞게 변경한다.
python3 - <<'PY'
import json, os
from pathlib import Path
apps = [
("app-one", "web-one", ["portal", "message"], ["com", "net"]),
("app-two", "web-two", ["files"], ["com"]),
("app-three", "web-three", ["visitor"], ["com", "net"]),
("app-four", "web-four", ["booking"], ["com", "net"]),
("app-five", "web-five", ["catalog"], ["com", "net"]),
("app-six", "web-six", ["support"], ["com", "net"]),
]
routes, domains = [], []
for namespace, service, names, suffixes in apps:
hosts = [
f"{name}.example.{suffix}"
for suffix in suffixes
for name in names
]
domains.extend(hosts)
routes.append({
"apiVersion": "gateway.networking.k8s.io/v1",
"kind": "HTTPRoute",
"metadata": {
"name": namespace + "-route",
"namespace": namespace
},
"spec": {
"parentRefs": [
{
"name": "public-gateway",
"namespace": "edge-gateway",
"sectionName": "https-" + s
}
for s in suffixes
],
"hostnames": hosts,
"rules": [{
"matches": [{
"path": {"type": "PathPrefix", "value": "/"}
}],
"filters": [{
"type": "ResponseHeaderModifier",
"responseHeaderModifier": {
"set": [{
"name": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
}]
}
}],
"backendRefs": [{"name": service, "port": 8080}]
}]
}
})
routes.append({
"apiVersion": "gateway.networking.k8s.io/v1",
"kind": "HTTPRoute",
"metadata": {
"name": "https-redirect",
"namespace": "edge-gateway"
},
"spec": {
"parentRefs": [
{"name": "public-gateway", "sectionName": "http-" + s}
for s in ["com", "net"]
],
"rules": [{
"filters": [{
"type": "RequestRedirect",
"requestRedirect": {
"scheme": "https",
"port": 32443,
"statusCode": 308
}
}]
}]
}
})
work = Path(os.environ["WORK_DIR"])
work.joinpath("http-routes.json").write_text(
json.dumps({
"apiVersion": "v1",
"kind": "List",
"items": routes
}, indent=2) + "\n"
)
work.joinpath("domains.txt").write_text("\n".join(domains) + "\n")
PY
kubectl apply --dry-run=server -f "$WORK_DIR/http-routes.json"
kubectl apply -f "$WORK_DIR/http-routes.json"
for ns in app-one app-two app-three app-four app-five app-six; do
kubectl get httproute "$ns-route" -n "$ns" -o json \
| jq '{name:.metadata.name,parents:.status.parents}'
done
kubectl get httproute https-redirect -n edge-gateway -o yaml
10. 병행 검증
각 Route parent의 Accepted=True와 ResolvedRefs=True, Gateway의 Accepted·Programmed=True를 확인한다. backend는 Route와 같은 네임스페이스의 Service를 사용하므로 이 예시에서는 backend용 ReferenceGrant가 필요하지 않다.
기존 DNS·LB 대상은 유지하고 --resolve로 새 경로를 검증한다. -k는 응답 비교용으로만 사용한다. 인증서 합격 여부는 SAN, 유효기간, 체인과 클라이언트 신뢰로 판단한다. 특정 발급기관 이름만으로 정상이라고 판단하지 않는다.
# 준비된 데이터 플레인 Pod가 있는 노드를 NEW_NODE로 선택
kubectl get pods -n edge-gateway -o wide
while IFS= read -r domain; do
old_code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 \
--resolve "$domain:31443:$OLD_NODE" "https://$domain:31443/")
new_code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 \
--resolve "$domain:32443:$NEW_NODE" "https://$domain:32443/")
printf '%s old=%s new=%s\n' "$domain" "$old_code" "$new_code"
done < "$WORK_DIR/domains.txt"
for domain in portal.example.com portal.example.net; do
openssl s_client -connect "$NEW_NODE:32443" -servername "$domain" \
</dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -enddate -ext subjectAltName
done
# CA 신뢰를 포함한 검증: -k를 사용하지 않는다.
curl --cacert /opt/certs/trust-ca.pem -I --max-time 10 \
--resolve "portal.example.com:32443:$NEW_NODE" \
https://portal.example.com:32443/
# 308과 Location의 호스트 및 포트 확인
curl -sI --max-time 10 \
--resolve "portal.example.com:32080:$NEW_NODE" \
http://portal.example.com:32080/
kubectl get events -n edge-control --sort-by=.lastTimestamp
kubectl get events -n edge-gateway --sort-by=.lastTimestamp
kubectl get deploy -n edge-gateway
read -r -p '데이터 플레인 Deployment 이름: ' DATA_DEPLOY
kubectl exec -n edge-gateway "deploy/$DATA_DEPLOY" -c nginx -- nginx -T \
| rg 'client_max_body_size|server_name'
curl -skI --max-time 10 \
--resolve "portal.example.com:31443:$OLD_NODE" \
https://portal.example.com:31443/
kubectl get pods -n legacy-ingress
11. 브라우저 검증과 장애 확인
관리자 권한으로 운영체제의 표준 hosts 파일에 예시 호스트와 새 노드 주소를 임시 등록한다. https://portal.example.com:32443/과 https://message.example.com:32443/에서 화면, 인증서, 로그인·업로드·로그아웃을 확인하고 테스트 후 임시 항목을 제거한다. 테스트 포트가 Origin과 SSO callback URL에 영향을 줄 수 있으므로 허용 조건을 확인한다.
- ImagePullBackOff: 이미지 경로·태그·노드 CA 신뢰·인증·아키텍처 확인
- Gateway 준비 실패: GatewayClass, 리스너 상태, Events 확인
- Route 연결 실패: parentRefs, allowedRoutes, Service와 EndpointSlice 확인
- NodePort 실패: 실제 Service 포트, 방화벽, Local Endpoint 유무 확인
- 421: SNI·Host·인증서 SAN·연결 재사용 조건 확인
SNI 검증 해제를 일괄 적용하지 않고 원인과 해당 릴리스의 NginxProxy 옵션을 확인한다. Service 직접 patch보다 Helm 또는 NginxProxy 원본 설정을 변경해 관리한다. ctr 업로드 성공과 워커 CRI Pull 성공은 별도로 확인한다.
12. 롤백과 완료 기준
아래는 이번 설치 전용 리소스를 제거하는 절차다. 공유 여부를 확인한 후 실행한다. 기존 업무 Service와 Ingress는 삭제하지 않는다. CRD는 다른 컨트롤러가 사용할 수 있으므로 일괄 삭제하지 않는다. Helm이 제거하는 클러스터 범위 리소스도 확인한다.
# 목록과 공유 여부 확인 후 필요한 경우에만 실행
kubectl get all -n edge-gateway
kubectl get all -n edge-control
kubectl delete -f "$WORK_DIR/http-routes.json" --ignore-not-found
kubectl delete -f "$WORK_DIR/client-settings.yaml" --ignore-not-found
kubectl delete -f "$WORK_DIR/gateway.json" --ignore-not-found
helm uninstall edge-fabric -n edge-control
# 이번 설치 전용 네임스페이스일 때만 삭제
kubectl delete namespace edge-gateway edge-control
# 새로 추가한 라벨만 제거: 기존 라벨이 있었다면 원래 값으로 복구
for ns in app-one app-two app-three app-four app-five app-six; do
kubectl label namespace "$ns" gateway-access-
done
13. 설치 결과 정리
- 필요한 모든 이미지가 폐쇄망에서 Pull 가능한가?
- GatewayClass·Gateway·HTTPRoute의 상태가 정상인가?
- 데이터 플레인 두 Pod의 준비 상태와 노드 분산을 확인했는가?
- 13개 호스트 응답, 인증서, HSTS, HTTPS 리다이렉트를 확인했는가?
- 실제 로그인·업로드·로그아웃과 기존 서비스 유지 여부를 확인했는가?
완료 범위는 NGF 설치와 병행 검증이다. 실제 트래픽 전환은 외부 LB·DNS, 리다이렉트 포트, 인증과 세션 동작을 검증한 뒤 별도 전환 계획으로 진행한다.
공식 참고자료
- NGINX Gateway Fabric: Helm 설치
- NGINX Gateway Fabric: ClientSettingsPolicy
- NGINX Gateway Fabric: API Reference
공식 문서는 갱신될 수 있으므로 반입한 릴리스의 Chart와 대조한다.
'지식 공유 > Kubernetes' 카테고리의 다른 글
| Kubernetes NeuVector 사용법 2편: Network Activity·Groups·Security Events 읽기 (0) | 2026.10.06 |
|---|---|
| Kubernetes NeuVector 사용법 1편: 메뉴와 Dashboard 지표 이해하기 (0) | 2026.10.06 |
| 2026 Kubernetes 트렌드: Gateway API·AI GPU·리소스 조정·GitOps (0) | 2026.10.06 |
| Kubernetes NeuVector란? K8s 클라우드 네이티브 보안 솔루션 기능과 활용 범위 (0) | 2026.09.29 |
| Kubernetes CI/CD 흐름도 상세 설명 - GitLab Jenkins Maven Docker Harbor K8s 배포 구조 (0) | 2026.09.29 |
