[Istio] Istio gateways

xgro·2025년 4월 19일
0

Istio

목록 보기
4/17
post-thumbnail

📌 Notice

Istio Hands-on Study (=Istio)
직접 실습을 통해 Isito를 배포 및 설정하는 내용을 정리한 블로그입니다.

CloudNetaStudy 그룹에서 스터디를 진행하고 있습니다.

Gasida님께 다시한번 🙇 감사드립니다.

EKS 관련 이전 스터디 내용은 아래 링크를 통해 확인할 수 있습니다.



📌 Summary

  • Istio Gateway는 Kubernetes Ingress보다 확장성과 보안 측면에서 유리한 외부 트래픽 진입 지점입니다.

  • TLS, mTLS, 다중 도메인, HTTP 리다이렉트 등 다양한 보안 설정을 유연하게 구성할 수 있습니다.

  • HTTP뿐만 아니라 TCP 프로토콜도 지원하며, VirtualService와의 연계를 통해 정교한 트래픽 제어가 가능합니다.

  • 운영 환경에서는 게이트웨이 주입, 액세스 로그 설정, 클러스터 설정 축소 기능 등을 통해 관리 효율성을 향상시킬 수 있습니다.



📌 Overview

Istio에서 Gateway 리소스는 외부 트래픽이 서비스 메시로 진입하는 공식적인 진입 지점을 정의합니다.

Kubernetes의 Ingress 리소스와는 다르게, Istio Gateway는 Envoy 기반 프록시의 모든 기능(예: L4/L7 라우팅, TLS 종단 처리, mTLS, 로깅, 텔레메트리 등)을 활용할 수 있도록 설계되어 있습니다.

이 장에서는 Istio Gateway를 중심으로 다음과 같은 내용을 다룹니다:

  • Istio Gateway의 동작 구조와 Ingress 리소스와의 차이점

  • TLS, mTLS, 리다이렉션, 다중 호스트 설정과 같은 보안 트래픽 처리

  • HTTP 외에도 TCP 트래픽과 같은 비HTTP 프로토콜 처리

  • 게이트웨이 운영 팁과 최적화 전략

실습을 통해 다양한 설정 방법과 디버깅 방법도 익히며, 실무에 직접 응용할 수 있도록 구성되어 있습니다.



📌 Istio gateways: Getting traffic into a cluster

👉 Step 00. 실습 환경 소개

이번 실습은 Istio Ingress Gateway를 중심으로,
외부에서 클러스터 내부 서비스로 트래픽이 유입되는 전체 흐름을 직접 따라가며 확인할 수 있도록 구성되었습니다.

📦 환경 구성

항목내용
Kuberneteskind (Kubernetes in Docker) 사용
Istio1.17.8 버전
OS 환경macOS (Docker Desktop 기준)
테스트 도메인api.istioinaction.io
테스트 앱httpbin 서비스
Gateway 포트 노출NodePort 방식 사용

🧰 설치된 컴포넌트

Istio 설치 시, 아래 구성 요소들을 함께 배포했습니다:

  • Ingress Gateway
  • Kiali (시각화 도구) – NodePort 30003
  • PrometheusNodePort 30001
  • GrafanaNodePort 30002
  • Jaeger (Tracing)NodePort 30004

이 실습에서는 Kiali를 통해 Gateway 흐름을 시각화하고, Prometheus/Grafana로 메트릭 확인, Jaeger로 트레이싱 흐름 분석까지도 연동할 수 있는 기반을 함께 갖추었습니다.


🧵 도메인 구성

실습에 사용되는 테스트 도메인은 다음과 같습니다:

  • 도메인: api.istioinaction.io
  • 목표: 해당 도메인으로 들어오는 요청을 Gateway에서 받아 → 적절한 VirtualService를 통해 → 내부 httpbin 서비스로 라우팅

📌 /etc/hosts 파일에 다음 항목을 추가해 도메인 매핑을 진행합니다:

127.0.0.1 api.istioinaction.io

🧱 kind 클러스터 생성

Istio Ingress Gateway를 실습하려면, 외부에서 트래픽을 받아들일 수 있도록 NodePort를 통한 포트 노출이 가능한 환경이어야 합니다.
이를 위해 다음과 같이 kind 클러스터를 생성합니다.

우선 아래와 같은 내용을 가진 kind-config.yaml 파일을 준비합니다:

kind create cluster --name myk8s --image kindest/node:v1.23.17 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000 # Sample Application (istio-ingrssgateway) HTTP
    hostPort: 30000
  - containerPort: 30001 # Prometheus
    hostPort: 30001
  - containerPort: 30002 # Grafana
    hostPort: 30002
  - containerPort: 30003 # Kiali
    hostPort: 30003
  - containerPort: 30004 # Tracing
    hostPort: 30004
  - containerPort: 30005 # Sample Application (istio-ingrssgateway) HTTPS
    hostPort: 30005
  - containerPort: 30006 # TCP Route
    hostPort: 30006
  - containerPort: 30007 # New Gateway 
    hostPort: 30007
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
EOF

# 설치 확인
docker ps

# 노드에 기본 툴 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bridge-utils net-tools dnsutils tcpdump ngrep iputils-ping git vim -y'

# (옵션) metrics-server
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm install metrics-server metrics-server/metrics-server --set 'args[0]=--kubelet-insecure-tls' -n kube-system
kubectl get all -n kube-system -l app.kubernetes.io/instance=metrics-server

이 구성은 로컬에서 실행된 kind 클러스터가
컨트롤 플레인 노드에 30001~30004 포트를 직접 노출할 수 있도록 합니다.


▶️ 클러스터 생성 명령어

설정 파일이 준비되었으면, 아래 명령어로 클러스터를 생성합니다:

kind create cluster --name istio-ingress --config kind-config.yaml

클러스터가 정상적으로 생성되었는지 확인하려면 다음 명령어를 실행해보세요:

kubectl get nodes

🚀 Istio 1.17.8 설치 및 구성

이 실습에서는 Istio의 default profile을 사용하여 컨트롤 플레인을 배포하고,
추가 도구인 Prometheus, Grafana, Kiali, Jaeger를 함께 설치합니다.

# myk8s-control-plane 진입 후 설치 진행
docker exec -it myk8s-control-plane bash
-----------------------------------
# (옵션) 코드 파일들 마운트 확인
tree /istiobook/ -L 1
혹은
git clone ... /istiobook

# istioctl 설치
export ISTIOV=1.17.8
echo 'export ISTIOV=1.17.8' >> /root/.bashrc

curl -s -L https://istio.io/downloadIstio | ISTIO_VERSION=$ISTIOV sh -
cp istio-$ISTIOV/bin/istioctl /usr/local/bin/istioctl
istioctl version --remote=false

# default 프로파일 컨트롤 플레인 배포
istioctl install --set profile=default -y

# 설치 확인 : istiod, istio-ingressgateway, crd 등
kubectl get istiooperators -n istio-system -o yaml
kubectl get all,svc,ep,sa,cm,secret,pdb -n istio-system
kubectl get cm -n istio-system istio -o yaml
kubectl get crd | grep istio.io | sort

# 보조 도구 설치
kubectl apply -f istio-$ISTIOV/samples/addons
kubectl get pod -n istio-system

# 빠져나오기
exit
-----------------------------------

# 실습을 위한 네임스페이스 설정
kubectl create ns istioinaction
kubectl label namespace istioinaction istio-injection=enabled
kubectl get ns --show-labels

# istio-ingressgateway 서비스 : NodePort 변경 및 nodeport 지정 변경 , externalTrafficPolicy 설정 (ClientIP 수집)
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort", "ports": [{"port": 80, "targetPort": 8080, "nodePort": 30000}]}}'
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 8443, "nodePort": 30005}]}}'
kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec":{"externalTrafficPolicy": "Local"}}'
kubectl describe svc -n istio-system istio-ingressgateway

# NodePort 변경 및 nodeport 30001~30003으로 변경 : prometheus(30001), grafana(30002), kiali(30003), tracing(30004)
kubectl patch svc -n istio-system prometheus -p '{"spec": {"type": "NodePort", "ports": [{"port": 9090, "targetPort": 9090, "nodePort": 30001}]}}'
kubectl patch svc -n istio-system grafana -p '{"spec": {"type": "NodePort", "ports": [{"port": 3000, "targetPort": 3000, "nodePort": 30002}]}}'
kubectl patch svc -n istio-system kiali -p '{"spec": {"type": "NodePort", "ports": [{"port": 20001, "targetPort": 20001, "nodePort": 30003}]}}'
kubectl patch svc -n istio-system tracing -p '{"spec": {"type": "NodePort", "ports": [{"port": 80, "targetPort": 16686, "nodePort": 30004}]}}'

# Prometheus 접속 : envoy, istio 메트릭 확인
open http://127.0.0.1:30001

# Grafana 접속
open http://127.0.0.1:30002

# Kiali 접속 1 : NodePort
open http://127.0.0.1:30003

# (옵션) Kiali 접속 2 : Port forward
kubectl port-forward deployment/kiali -n istio-system 20001:20001 &
open http://127.0.0.1:20001

# tracing 접속 : 예거 트레이싱 대시보드
open http://127.0.0.1:30004


# 접속 테스트용 netshoot 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: netshoot
spec:
  containers:
  - name: netshoot
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

✅ 참고 사항

  • 모든 구성은 로컬 환경에서 실행되며, Istio는 demo profile을 기반으로 설치되었습니다.
  • Gateway와 VirtualService는 별도 YAML로 정의되어 단계별로 실습하며 생성합니다.

이제 본격적으로 Ingress Gateway가 트래픽을 어떻게 수용하고,
그 흐름을 내부 서비스로 어떻게 전달하는지를 함께 실습해보겠습니다.




👉 Step. 01 - Ingress란 무엇인가요?

✅ 쿠버네티스의 Ingress

기본적인 Kubernetes Ingress는 다음 세 가지 리소스를 조합해 동작합니다:

  • Ingress: 요청 경로와 서비스 간의 매핑을 정의
  • Service: 요청을 받을 백엔드 서비스
  • Pod: 실제 요청을 처리하는 애플리케이션

하지만 Kubernetes Ingress는 동작 방식이 컨트롤러에 따라 다르고, L7 레벨의 세부 제어(예: 재시도, 트래픽 분할, 보안 정책 적용 등)에는 한계가 있습니다.


✅ Istio의 접근 방식: Gateway + VirtualService

Istio에서는 Ingress를 다음 두 가지 리소스로 분리합니다:

  • Gateway
    L4~L5 계층을 담당하며,
    “어떤 포트(예: 80), 어떤 호스트(api.istioinaction.io)를 받을지” 를 정의합니다.
    즉, 트래픽이 클러스터 경계에서 진입할 수 있도록 열어주는 입구입니다.

  • VirtualService
    L7 계층을 담당하며,
    “이 요청을 어떤 서비스로 어떻게 보낼지” 를 정의합니다.
    경로 기반 라우팅, 헤더 조건부 라우팅, 트래픽 분할 등의 기능을 담당합니다.


✅ Istio Gateway 구조를 이해하는 방법

Istio에서 외부 요청이 내부로 유입되는 전체 흐름을 요약하면 다음과 같습니다:

[브라우저] → [Gateway (80 포트 수신)] → [VirtualService (Host/Path 매칭)] → [Service] → [Pod]

각 구성 요소는 아래처럼 역할을 나눕니다:

리소스계층설명
GatewayL4~L5요청 수신 (포트, 호스트 기반)
VirtualServiceL7요청 분기 (도메인, 경로, 조건 기반 라우팅)
ServiceL3~L4쿠버네티스 네트워크 내부 서비스
PodL7실제 비즈니스 로직 수행 컨테이너

이제 이 구조를 실제로 구성해보며,
외부에서 들어오는 요청이 Gateway → VirtualService → Service → Pod로 이어지는 과정을 실습을 통해 확인해보겠습니다.



👉 Step. 02 - Istio ingress gateways

Istio에는 클러스터 외부에서 시작된 트래픽을 수용하고 내부로 전달하는 Ingress Gateway라는 컴포넌트가 있습니다.
이는 단순히 외부 요청을 받아주는 진입점이 아니라, Envoy 프록시 기반으로 구성된 고급 트래픽 제어 지점입니다.
Ingress Gateway는 로드 밸런싱, TLS 종료, 가상 호스트 기반 라우팅 등의 기능을 수행할 수 있으며,
모든 트래픽 흐름은 Envoy 프록시 하나로 처리됩니다.


✅ Istio Ingress Gateway의 동작 구조

Istio의 Ingress Gateway는 내부적으로 단일 Envoy 프록시를 실행하는 구조로 동작합니다.
초기 실행 시에는 별도 애플리케이션 컨테이너 없이 Envoy만 기동되며, 설정은 컨트롤 플레인(istiod)이 동적으로 주입합니다.

다음은 Ingress Gateway가 제대로 구성되어 있고 Envoy가 부트스트랩 되어 있는지를 확인하기 위한 명령어 모음입니다.
실제 구조를 이해하는 데 중요한 실습이므로 한 번에 실행하며 내부 상태를 관찰해보면 좋습니다.


# Ingress Gateway Pod 확인: 컨테이너는 1개, Envoy만 실행됨
kubectl get pod -n istio-system -l app=istio-ingressgateway

# Envoy 프록시 상태 확인 (CDS, LDS, EDS 등 동기화 여부)
docker exec -it myk8s-control-plane istioctl proxy-status

# Envoy 설정 전체 확인
docker exec -it myk8s-control-plane istioctl proxy-config all deploy/istio-ingressgateway.istio-system

# 리스너 설정 (어떤 포트를 수신하고 있는지 확인)
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system

# 라우팅 경로 확인
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system

# 클러스터, 엔드포인트, 로그, 인증서 관련 설정 확인
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config log deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/istio-ingressgateway.istio-system

# 설정에 사용된 IstioOperator 리소스 참고
kubectl get istiooperators -n istio-system -o yaml

# 프로세스 확인: pilot-agent가 envoy를 부트스트랩함
kubectl exec -n istio-system deploy/istio-ingressgateway -- ps

# 실행 중인 프로세스 상세 정보 (envoy가 1개의 프로세스로 동작 중)
kubectl exec -n istio-system deploy/istio-ingressgateway -- ps aux

# 프로세스 실행 유저는 istio-proxy
kubectl exec -n istio-system deploy/istio-ingressgateway -- whoami
kubectl exec -n istio-system deploy/istio-ingressgateway -- id


이 명령어들을 통해 다음을 확인할 수 있습니다:

  • istio-ingressgateway는 별도의 앱 없이 단일 Envoy 프록시로 구성되어 있음
  • Envoy의 설정(CDS, LDS, RDS 등)은 istiod를 통해 동기화됨
  • Envoy는 pilot-agent에 의해 부트스트랩되며, 실제 트래픽을 수신할 준비가 되어 있음
  • 리스너와 라우트 설정은 아직 VirtualService가 없기 때문에 /healthz/stats 정도만 구성됨

📌 Ingress Gateway는 Envoy 프록시이며, 내부 구조는 일반 사이드카 프록시와 동일하지만 클러스터 외부에서 요청을 수용한다는 점이 다릅니다.


이제 Ingress Gateway가 제대로 동작 중인 것을 확인했으니,
다음 단계에서는 GatewayVirtualService 리소스를 정의하고
외부 요청이 어떻게 내부 서비스로 연결되는지 실습을 통해 구성해보겠습니다.


✅ Specifying Gateway resources 게이트웨이 리소스 지정하기

Istio에서 외부 트래픽을 수용하려면 가장 먼저 Gateway 리소스를 정의해야 한다. 이 리소스는 Envoy 프록시가 어떤 포트를 열고, 어떤 도메인을 수용할지를 지정하는 역할을 한다.

다음은 webapp.istioinaction.io 도메인을 80 포트로 수용하도록 Gateway 리소스를 정의하는 예시다.

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: coolstore-gateway #(1) 게이트웨이 이름
spec:
  selector:
    istio: ingressgateway #(2) 어느 게이트웨이 구현체인가?
  servers:
  - port:
      number: 80          #(3) 노출할 포트 
      name: http
      protocol: HTTP
    hosts:
    - "webapp.istioinaction.io" #(4) 이 포트의 호스트

이 리소스를 적용하면, Envoy는 80 포트를 리스닝하고 해당 도메인으로 오는 트래픽을 받을 준비를 하게 된다. 리소스를 적용하고 istiod가 어떤 설정을 Envoy에게 푸시하는지 확인할 수 있다.

# 신규터미널 : istiod 로그
kubectl stern -n istio-system -l app=istiod
istiod-7df6ffc78d-w9szx discovery 2025-04-13T04:50:04.531700Z	info	ads	Push debounce stable[20] 1 for config Gateway/istioinaction/coolstore-gateway: 100.4665ms since last change, 100.466166ms since last push, full=true
istiod-7df6ffc78d-w9szx discovery 2025-04-13T04:50:04.532520Z	info	ads	XDS: Pushing:2025-04-13T04:50:04Z/14 Services:12 ConnectedEndpoints:1 Version:2025-04-13T04:50:04Z/14
istiod-7df6ffc78d-w9szx discovery 2025-04-13T04:50:04.537272Z	info	ads	LDS: PUSH for node:istio-ingressgateway-996bc6bb6-p6k79.istio-system resources:1 size:2.2kB
istiod-7df6ffc78d-w9szx discovery 2025-04-13T04:50:04.545298Z	warn	constructed http route config for route http.8080 on port 80 with no vhosts; Setting up a default 404 vhost
istiod-7df6ffc78d-w9szx discovery 2025-04-13T04:50:04.545584Z	info	ads	RDS: PUSH request for node:istio-ingressgateway-996bc6bb6-p6k79.istio-system resources:1 size:34B cached:0/0

# 터미널2
cat ch4/coolstore-gw.yaml
kubectl -n istioinaction apply -f ch4/coolstore-gw.yaml

# 확인
kubectl get gw,vs -n istioinaction
NAME                                            AGE
gateway.networking.istio.io/coolstore-gateway   26m

#
docker exec -it myk8s-control-plane istioctl proxy-status
NAME                                                  CLUSTER        CDS        LDS        EDS        RDS          ECDS         ISTIOD                      VERSION
istio-ingressgateway-996bc6bb6-p6k79.istio-system     Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED       NOT SENT     istiod-7df6ffc78d-w9szx     1.17.8

docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system
ADDRESS PORT  MATCH DESTINATION
0.0.0.0 8080  ALL   Route: http.8080
...

docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system
NAME          DOMAINS     MATCH                  VIRTUAL SERVICE
http.8080     *           /*                     404
...     

# http.8080 정보의 의미는? 그외 나머지 포트의 역할은?
kubectl get svc -n istio-system istio-ingressgateway -o jsonpath="{.spec.ports}" | jq
[
  {
    "name": "status-port",
    "nodePort": 31674,
    "port": 15021,
    "protocol": "TCP",
    "targetPort": 15021
  },
  {
    "name": "http2",
    "nodePort": 30000,
    "port": 80,
    "protocol": "TCP",
    "targetPort": 8080
  },
  {
    "name": "https",
    "nodePort": 30005,
    "port": 443,
    "protocol": "TCP",
    "targetPort": 8443
  }
]

# HTTP 포트(80)을 올바르게 노출했다. VirtualService 는 아직 아무것도 없다.
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system -o json
docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system -o json --name http.8080
[
    {
        "name": "http.8080",
        "virtualHosts": [
            {
                "name": "blackhole:80",
                "domains": [
                    "*"
                ]
            }
        ],
        "validateClusters": false,
        "ignorePortInHostMatching": true
    }
]

위 실습을 통해 알 수 있듯이, 현재 Envoy의 리스너는 내부적으로 8080 포트를 사용하고 있으며, 해당 포트로 들어오는 모든 요청은 blackhole:80이라는 가상 호스트로 라우팅되고 있습니다.

이는 아직 VirtualService가 정의되지 않아 유효한 경로가 없기 때문입니다.

현재 상태에서는 외부에서 webapp.istioinaction.io 도메인으로 요청을 보내더라도 404 오류가 반환됩니다.

Gateway 리소스는 Envoy가 외부 트래픽을 수용할 수 있도록 리스너를 구성하지만, 실제 트래픽이 어디로 라우팅될지를 결정하는 VirtualService가 없다면
모든 요청은 기본 블랙홀로 빠지게 됩니다.

다음 단계에서는 이 Gateway에 연결되는 VirtualService를 정의하고,
지정된 경로에 따라 내부 서비스로 트래픽을 정상적으로 전달해보겠습니다.


✅ VirtualService로 게이트웨이 라우팅하기

이전 단계에서는 Istio Gateway 리소스를 사용하여 80 포트를 열고, 해당 포트에서 webapp.istioinaction.io 도메인을 수용하는 설정을 구성하였습니다.

이제 이 게이트웨이를 통해 들어온 트래픽을 실제 서비스 메시 내부의 webapp 서비스로 라우팅하기 위한 설정이 필요합니다.

이때 사용하는 리소스가 바로 VirtualService입니다.

Istio에서 VirtualService는 어떤 호스트에 대해, 어떤 조건으로, 어떤 백엔드 서비스로 라우팅할지를 선언하는 핵심 구성 요소입니다.

다음은 Gateway를 통과한 트래픽을 webapp 서비스로 라우팅하는 VirtualService 설정 예시입니다.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: webapp-vs-from-gw     #1 VirtualService 이름
spec:
  hosts:
  - "webapp.istioinaction.io" #2 비교할 가상 호스트네임(또는 호스트네임들)
  gateways:
  - coolstore-gateway         #3 이 VirtualService 를 적용할 게이트웨이
  http:
  - route:
    - destination:            #4 이 트래픽의 목적 서비스
        host: webapp
        port:
          number: 80

이 리소스는 이전에 정의한 coolstore-gateway를 통해 들어온 트래픽 중,
webapp.istioinaction.io 호스트를 대상으로 하는 요청을 내부 서비스 webapp의 80번 포트로 라우팅하도록 구성합니다.

적용과 동시에 istiod 로그를 통해 설정이 프록시에 푸시되는 과정을 확인할 수 있습니다.

# 신규터미널 : istiod 로그
kubectl stern -n istio-system -l app=istiod
istiod-7df6ffc78d-w9szx discovery 2025-04-13T05:15:16.485143Z	info	ads	Push debounce stable[21] 1 for config VirtualService/istioinaction/webapp-vs-from-gw: 102.17225ms since last change, 102.172083ms since last push, full=true
istiod-7df6ffc78d-w9szx discovery 2025-04-13T05:15:16.485918Z	info	ads	XDS: Pushing:2025-04-13T05:15:16Z/15 Services:12 ConnectedEndpoints:1 Version:2025-04-13T05:15:16Z/15
istiod-7df6ffc78d-w9szx discovery 2025-04-13T05:15:16.487330Z	info	ads	CDS: PUSH for node:istio-ingressgateway... resources:23 size:22.9kB cached:22/22
istiod-7df6ffc78d-w9szx discovery 2025-04-13T05:15:16.488346Z	info	ads	LDS: PUSH for node:istio-ingressgateway... resources:1 size:2.2kB
istiod-7df6ffc78d-w9szx discovery 2025-04-13T05:15:16.489937Z	info	ads	RDS: PUSH for node:istio-ingressgateway... resources:1 size:538B cached:0/0

적용한 리소스는 다음과 같이 확인할 수 있습니다.

cat ch4/coolstore-vs.yaml
kubectl apply -n istioinaction -f ch4/coolstore-vs.yaml

kubectl get gw,vs -n istioinaction
NAME                                            AGE
gateway.networking.istio.io/coolstore-gateway   26m

NAME                                                   GATEWAYS                HOSTS                         AGE
virtualservice.networking.istio.io/webapp-vs-from-gw   ["coolstore-gateway"]   ["webapp.istioinaction.io"]   49s

이제 Envoy 프록시 설정이 변경되어, 해당 도메인으로의 요청은 webapp 서비스로 라우팅됩니다.

docker exec -it myk8s-control-plane istioctl proxy-status

docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system

docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system
NAME          DOMAINS               MATCH     VIRTUAL SERVICE
http.8080     webapp.istioinaction.io  /*     webapp-vs-from-gw.istioinaction

이제 클러스터 내부에 실제 서비스인 webappcatalog를 배포합니다.

# 로그 확인
kubectl stern -n istioinaction -l app=webapp
kubectl stern -n istioinaction -l app=catalog
kubectl stern -n istio-system -l app=istiod

# 서비스 배포
cat services/catalog/kubernetes/catalog.yaml
cat services/webapp/kubernetes/webapp.yaml 
kubectl apply -f services/catalog/kubernetes/catalog.yaml -n istioinaction
kubectl apply -f services/webapp/kubernetes/webapp.yaml -n istioinaction

# 상태 확인
kubectl get pod -n istioinaction -owide
kubectl images -n istioinaction
kubectl resource-capacity -n istioinaction -c --pod-count

각 프록시의 설정 상태는 다음 명령어를 통해 확인할 수 있습니다.

docker exec -it myk8s-control-plane istioctl proxy-status
docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/istio-ingressgateway.istio-system
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system | egrep 'TYPE|istioinaction'
docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system | egrep 'ENDPOINT|istioinaction'

정상적으로 설정되었다면, 이제 netshoot 파드를 통해 내부에서 서비스에 접근할 수 있습니다.

kubectl exec -it netshoot -- curl -s http://catalog.istioinaction/items/1 | jq
kubectl exec -it netshoot -- curl -s http://webapp.istioinaction/api/catalog/items/1 | jq

이후 테스트용으로 replicas 수를 변경하거나, 인증서 정보를 확인하며
실제 트래픽 라우팅이 어떻게 이루어지는지를 보다 정밀하게 분석할 수 있습니다.
이 부분은 실습 시점에 맞춰 필요에 따라 진행하시면 됩니다.

마지막으로 외부에서의 호출이 잘 작동하는지 확인합니다.
주의할 점은 Host 헤더를 지정하지 않으면 404 응답이 반환된다는 것입니다.

# 터미널 : istio-ingressgateway 로깅 수준 상향
kubectl exec -it deploy/istio-ingressgateway -n istio-system -- curl -X POST http://localhost:15000/logging?http=debug
kubectl stern -n istio-system -l app=istio-ingressgateway
혹은
kubectl logs -n istio-system -l app=istio-ingressgateway -f
istio-ingressgateway-996bc6bb6-p6k79 istio-proxy 2025-04-13T06:22:13.389828Z	debug	envoy http external/envoy/source/common/http/conn_manager_impl.cc:1032	[C4403][S8504958039178930365] request end stream	thread=42
istio-ingressgateway-996bc6bb6-p6k79 istio-proxy 2025-04-13T06:22:13.390654Z	debug	envoy http external/envoy/source/common/http/filter_manager.cc:917	[C4403][S8504958039178930365] Preparing local reply with details route_not_found	thread=42
istio-ingressgateway-996bc6bb6-p6k79 istio-proxy 2025-04-13T06:22:13.390756Z	debug	envoy http external/envoy/source/common/http/filter_manager.cc:959	[C4403][S8504958039178930365] Executing sending local reply.	thread=42
istio-ingressgateway-996bc6bb6-p6k79 istio-proxy 2025-04-13T06:22:13.390789Z	debug	envoy http external/envoy/source/common/http/conn_manager_impl.cc:1687	[C4403][S8504958039178930365] encoding headers via codec (end_stream=true):
istio-ingressgateway-996bc6bb6-p6k79 istio-proxy ':status', '404'
...

# 외부(?)에서 호출 시도 : Host 헤더가 게이트웨이가 인식하는 호스트가 아니다
curl http://localhost:30000/api/catalog -v
* Host localhost:30000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:30000...
* Connected to localhost (::1) port 30000
> GET /api/catalog HTTP/1.1
> Host: localhost:30000
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 404 Not Found
< date: Sun, 13 Apr 2025 06:22:12 GMT
< server: istio-envoy
< content-length: 0
< 
* Connection #0 to host localhost left intact

# curl 에 host 헤더 지정 후 호출 시도
curl -s http://localhost:30000/api/catalog -H "Host: webapp.istioinaction.io" | jq
[
  {
    "id": 1,
    "color": "amber",
    "department": "Eyewear",
    "name": "Elinor Glasses",
    "price": "282.00"
...

이제 VirtualService를 통해 Gateway를 거쳐 실제 서비스까지의 요청 흐름이 완성되었습니다.


✅ Istio ingress gateway vs. Kubernetes Ingress

Istio를 처음 사용하는 사용자라면,
"왜 Istio는 Kubernetes Ingress 리소스를 그대로 사용하지 않을까?"
라는 의문이 들 수 있습니다.

Istio는 Ingress v1 리소스를 지원하긴 하지만, 해당 사양에는 여러 가지 구조적인 한계가 존재합니다.

첫 번째 한계는 Ingress 사양이 HTTP 기반 워크로드에 맞춰져 있고 구조적으로 매우 단순하다는 점입니다.

Ingress 리소스는 기본적으로 80(HTTP), 443(HTTPS) 포트만을 인그레스 포인트로 간주합니다. 하지만 Kafka나 NATS.io 같은 메시지 시스템은 TCP 연결을 직접 노출할 필요가 있습니다.

이러한 경우 Ingress 리소스는 대응할 수 없습니다.

두 번째는 트래픽 제어 표현력의 부족입니다.
Ingress 사양만으로는 다음과 같은 고급 트래픽 제어가 불가능합니다.

  • 버전 기반 트래픽 분할
  • 트래픽 섀도잉
  • 조건 기반 라우팅
  • 헤더 기반 라우팅

이러한 기능을 구현하려면 벤더가 각자 고유한 애노테이션을 만들어 대응해야 했고,
이 애노테이션들은 상호 호환되지 않기 때문에 운영의 복잡도가 높아졌습니다.

만약 Istio가 Ingress 리소스를 그대로 사용했다면,
Envoy의 고급 기능을 표현하기 위한 방대한 커스텀 애노테이션이 필요했을 것입니다.

결과적으로 Istio는 Ingress 패턴을 새롭게 설계했습니다.
그리고 트래픽 제어를 명확하게 계층별로 분리합니다.

  • Gateway: L4/L5 (포트, 프로토콜, 인증 등)
  • VirtualService: L7 (호스트, 경로, 헤더 기반 라우팅 등)

💡 참고
Kubernetes 커뮤니티에서도 Ingress v1의 한계를 보완하기 위해 Gateway API라는 새로운 사양을 개발 중입니다.
이 Gateway API는 Istio의 리소스 구조에서 많은 영감을 받았으며, 향후 표준으로 자리잡을 가능성이 높습니다.


✅ Istio ingress gateway vs. API gateways

Istio Ingress Gateway는 흔히 사용하는 API Gateway와도 자주 비교됩니다.

두 컴포넌트 모두 외부 트래픽을 내부 서비스로 라우팅한다는 점에서 역할이 유사합니다.

하지만 기능적 범위와 사용 목적은 다릅니다.

  • API Gateway는 보통 트래픽 라우팅 외에도 다음과 같은 기능을 포함합니다:

    • 인증 및 인가
    • 요청/응답 리라이터
    • 속도 제한 (Rate Limiting)
    • API 키 검증
    • 사용자별 쿼터 처리
    • 로깅 및 감사 등
  • 반면 Istio Gateway + VirtualService 조합은 네트워크 계층에 집중되어 있으며, 복잡한 애플리케이션 계층 로직은 별도 컴포넌트로 위임하는 방식입니다.

이는 기능을 단일한 API Gateway에 집중시키기보다, 서비스 메시의 네트워크 제어는 Envoy가, 인증·인가 및 정책은 별도 시스템이 담당하는 관심사 분리(Separation of Concerns)의 철학이 반영된 구조입니다.

따라서 Istio를 사용할 때는 다음과 같이 생각할 수 있습니다:

  • 네트워크 계층의 게이트웨이 → Istio Gateway + VirtualService
  • API 계층의 세밀한 제어가 필요할 경우 → 별도의 API Gateway 또는 External Authorization 필터 활용

이처럼 Istio는 전통적인 API Gateway의 일부 기능은 포함하지 않지만,
네트워크 경로 제어에 있어 훨씬 더 세밀하고 확장 가능한 제어 권한을 제공합니다.



👉 Step 03. Securing gateway traffic - 게이트웨이 트래픽 보안

외부에서 클러스터로 들어오는 트래픽을 수용하는 Istio Gateway는
기본적으로 HTTP 트래픽을 처리할 수 있도록 설정됩니다.

하지만 프로덕션 환경에서는 HTTP 대신 HTTPS를 통한 암호화된 통신이 필수입니다.
이를 통해 다음과 같은 이점을 얻을 수 있습니다.

  • 전송 중 데이터 보호 (Man-in-the-middle 공격 방지)
  • 클라이언트와 게이트웨이 간의 신뢰 확보
  • 기본적인 TLS 기반 인증 수행 가능

Istio는 이러한 보안 요건을 충족하기 위해
게이트웨이에서 TLS termination (TLS 종료) 기능을 지원합니다.


✅ Istio 게이트웨이에서 TLS 트래픽을 처리하는 방식

Istio에서는 외부에서 유입되는 HTTPS 트래픽을 게이트웨이 수준에서 복호화하고,
그 이후 내부 서비스와는 HTTP 또는 mTLS로 통신합니다.

이 구조는 다음과 같이 구성됩니다.

  • 클라이언트 → TLS 연결 → Istio Ingress Gateway
  • Ingress Gateway → HTTP (혹은 mTLS) → 서비스 메시 내부

이를 위해 Istio에서는 다음의 리소스를 설정합니다:

  • Gateway 리소스: HTTPS 포트 수신 및 TLS 설정
  • Secret 리소스: 인증서와 키 보관
  • VirtualService 리소스: 도메인 기반 라우팅

TLS termination은 서버 측 인증만 수행하는 단방향 TLS(Simple TLS) 기준입니다.

이후 mTLS 양방향 인증은 별도로 구성할 수 있습니다.



✅ HTTP traffic with TLS (실습)

HTTPS를 통해 Istio 게이트웨이로 들어오는 트래픽을 보호함으로써,
MITM(Man-In-The-Middle) 공격을 방지하고 클러스터 외부와 내부 간의 신뢰를 확보할 수 있습니다.

이를 위해 Istio 게이트웨이에 TLS를 설정하면,
클라이언트와 게이트웨이 간 통신은 암호화되고,
게이트웨이는 해당 TLS를 종료(Terminate)한 뒤 내부 서비스로 트래픽을 전달하게 됩니다.

HTTPS 수신을 위해서는 게이트웨이가 사용할 인증서와 키가 필요합니다.

이 인증서는 일반적으로 신뢰할 수 있는 인증기관(CA)가 서명한 것으로,
클라이언트는 이를 통해 서버의 정체성을 확인하고 공개키 기반 암호화를 수행합니다.

참고: 실제 TLS 프로토콜은 비대칭키 방식으로 세션키를 교환한 후,
데이터 전송 자체는 대칭키로 처리합니다. 더 자세한 내용은 부록 C 참고.


먼저 Istio ingress gateway가 사용할 인증서 및 개인키를 Kubernetes Secret 리소스로 생성합니다.

# 현재 등록된 시크릿 정보 확인
docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/istio-ingressgateway.istio-system

# 인증서 파일 및 개인키 확인
cat ch4/certs/3_application/private/webapp.istioinaction.io.key.pem
cat ch4/certs/3_application/certs/webapp.istioinaction.io.cert.pem
openssl x509 -in ch4/certs/3_application/certs/webapp.istioinaction.io.cert.pem -noout -text

# Secret 생성
kubectl create -n istio-system secret tls webapp-credential \
--key ch4/certs/3_application/private/webapp.istioinaction.io.key.pem \
--cert ch4/certs/3_application/certs/webapp.istioinaction.io.cert.pem

# Secret 내용 확인 (krew plugin)
kubectl view-secret -n istio-system webapp-credential --all

이 Secret은 반드시 Istio ingress gateway와 동일한 네임스페이스에 존재해야 하며,
프로덕션에서는 gateway를 별도 네임스페이스로 분리하는 것이 일반적입니다.

이제 Gateway 리소스에 HTTPS 관련 포트를 추가하고, 해당 Secret을 참조하도록 설정합니다.

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: coolstore-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "webapp.istioinaction.io"
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: webapp-credential
    hosts:
    - "webapp.istioinaction.io"
# Gateway 리소스 적용
cat ch4/coolstore-gw-tls.yaml
kubectl apply -f ch4/coolstore-gw-tls.yaml -n istioinaction

# Gateway에서 로드된 인증서 확인
docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/istio-ingressgateway.istio-system

✅ HTTPS 요청 테스트

# 실패 테스트 1 (인증서 검증 실패)
curl -v -H "Host: webapp.istioinaction.io" https://localhost:30005/api/catalog
* Host localhost:30005 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:30005...
* Connected to localhost (::1) port 30005
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:30005
* Closing connection
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:30005

# 서버(istio-ingressgateway 파드)에서 제공하는 인증서는 기본 CA 인증서 체인을 사용해 확인할 수 없다는 의미다.
# curl 클라이언트에 적절한 CA 인증서 체인을 전달해보자.
# (호출 실패) 원인: (기본 인증서 경로에) 인증서 없음. 사설인증서 이므로 “사설CA 인증서(체인)” 필요


# 서버에 존재하는 CA 목록 확인
kubectl exec -it deploy/istio-ingressgateway -n istio-system -- ls -l /etc/ssl/certs

# 실패 테스트 2 (인증서 제공 but 도메인 불일치)
curl -v -H "Host: webapp.istioinaction.io" https://localhost:30005/api/catalog \
--cacert ch4/certs/2_intermediate/certs/ca-chain.cert.pem
* Host localhost:30005 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:30005...
* Connected to localhost (::1) port 30005
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: ch4/certs/2_intermediate/certs/ca-chain.cert.pem
*  CApath: none
* LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:30005 
* Closing connection
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:30005 
# (호출 실패) 원인: 인증실패. 서버인증서가 발급된(issued) 도메인 “webapp.istioinaction.io”로 호출하지 않음 (localhost로 호출함)

# /etc/hosts 임시 등록
echo "127.0.0.1       webapp.istioinaction.io" | sudo tee -a /etc/hosts

# 성공 테스트 3 (도메인 및 인증서 일치)
curl -v https://webapp.istioinaction.io:30005/api/catalog \
--cacert ch4/certs/2_intermediate/certs/ca-chain.cert.pem

# 브라우저에서 테스트
open https://webapp.istioinaction.io:30005
open https://webapp.istioinaction.io:30005/api/catalog

# HTTP 요청도 테스트
curl -v http://webapp.istioinaction.io:30000/api/catalog
open http://webapp.istioinaction.io:30000

해당 구성은 다음의 흐름을 따릅니다:

  1. 클라이언트는 TLS를 이용해 게이트웨이에 연결
  2. Istio ingress gateway는 TLS를 종료하고 내부 서비스로 트래픽 전달
  3. 이후의 통신은 HTTP 또는 mTLS로 처리됨 (이후 9장에서 다룸 예정)

참고: 인증서 관리는 cert-manager 등의 도구와 통합하여 자동화할 수 있습니다.
자세한 내용은 https://cert-manager.io/docs/ 참고


✅ HTTP redirect to HTTPS (실습)

TLS를 통한 안전한 통신을 보장하기 위해,
클러스터에 유입되는 모든 HTTP 트래픽을 HTTPS로 강제 리다이렉션할 수 있습니다.

이는 게이트웨이 설정에서 간단히 httpsRedirect: true 옵션을 통해 구현할 수 있습니다.

클라이언트가 HTTP 요청을 보내면, Istio ingress gateway는 301 Moved Permanently 응답과 함께 HTTPS로 유도합니다.

아래는 HTTPS 리다이렉트를 활성화한 Gateway 리소스 예시입니다.

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: coolstore-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "webapp.istioinaction.io"
    tls:
      httpsRedirect: true
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: webapp-credential
    hosts:
    - "webapp.istioinaction.io"

해당 설정을 적용한 후, 실제로 리다이렉션이 작동하는지 테스트해보겠습니다.

# 설정 적용
kubectl apply -f ch4/coolstore-gw-tls-redirect.yaml

# HTTP 요청 → HTTPS로 리다이렉트되는지 확인
curl -v http://webapp.istioinaction.io:30000/api/catalog

# 결과 예시
* Host webapp.istioinaction.io:30000 was resolved.
* IPv4: 127.0.0.1
*   Trying 127.0.0.1:30000...
* Connected to webapp.istioinaction.io (127.0.0.1) port 30000
> GET /api/catalog HTTP/1.1
> Host: webapp.istioinaction.io:30000
> User-Agent: curl/8.7.1
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< location: https://webapp.istioinaction.io:30000/api/catalog
< date: Sun, 13 Apr 2025 08:01:31 GMT
< server: istio-envoy
< content-length: 0

위 결과에서 볼 수 있듯이, 클라이언트는 HTTP 요청에 대해 301 리다이렉션을 받고, 다음 요청을 HTTPS 프로토콜로 수행해야 함을 인지하게 됩니다.

이를 통해 HTTPS 접근을 강제하는 효과를 얻을 수 있습니다.


✅ HTTP traffic with mutual TLS (실습)

앞 절에서는 일반적인 TLS 설정을 통해 서버가 클라이언트에게 본인의 신원을 증명했습니다.
그러나 외부에서 유입되는 트래픽에 대해 클라이언트의 신원도 검증하고 싶다면, mutual TLS(mTLS)를 도입해야 합니다.
mTLS는 클라이언트와 서버가 서로의 인증서를 확인하고 신뢰함으로써 양방향 인증을 제공합니다.

아래 그림은 이러한 mTLS 인증 절차를 시각적으로 표현합니다.

이제 Istio Ingress Gateway가 mTLS 커넥션을 수락할 수 있도록 구성하겠습니다.
이를 위해 인증서와 키, 그리고 신뢰할 수 있는 CA 체인을 포함한 쿠버네티스 Secret을 먼저 생성합니다.

# 인증서 파일들 확인
cat ch4/certs/3_application/private/webapp.istioinaction.io.key.pem
cat ch4/certs/3_application/certs/webapp.istioinaction.io.cert.pem
cat ch4/certs/2_intermediate/certs/ca-chain.cert.pem
openssl x509 -in ch4/certs/2_intermediate/certs/ca-chain.cert.pem -noout -text

# Secret 생성 : (적절한 CA 인증서 체인) 클라이언트 인증서
kubectl create -n istio-system secret \
generic webapp-credential-mtls --from-file=tls.key=\
ch4/certs/3_application/private/webapp.istioinaction.io.key.pem \
--from-file=tls.crt=\
ch4/certs/3_application/certs/webapp.istioinaction.io.cert.pem \
--from-file=ca.crt=\
ch4/certs/2_intermediate/certs/ca-chain.cert.pem

# 확인
kubectl view-secret -n istio-system webapp-credential-mtls --all

위에서 생성한 Secret을 사용할 수 있도록 Gateway 리소스를 구성합니다.

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: coolstore-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "webapp.istioinaction.io"
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: MUTUAL # mTLS 설정
      credentialName: webapp-credential-mtls # 신뢰할 수 있는 CA가 구성된 자격 증명
    hosts:
    - "webapp.istioinaction.io"

Gateway 설정을 적용하고 인증서를 실제로 로드하는지 확인합니다.

# 적용
kubectl apply -f ch4/coolstore-gw-mtls.yaml -n istioinaction

# (옵션) SDS 로그 확인
kubectl stern -n istio-system -l app=istiod
istiod-7df6ffc78d-w9szx discovery 2025-04-13T08:14:14.405397Z	info	ads	SDS: PUSH request for node:...

# Ingress Gateway에서 활성화된 Secret 확인
docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/istio-ingressgateway.istio-system

이제 실제 호출 테스트를 통해 mTLS 동작을 검증합니다.

# 호출 테스트 1 : (호출 실패) 클라이언트 인증서 없음
curl -v https://webapp.istioinaction.io:30005/api/catalog \
--cacert ch4/certs/2_intermediate/certs/ca-chain.cert.pem

# 웹브라우저에서도 인증서 없음으로 인해 접속 실패
open https://webapp.istioinaction.io:30005
open https://webapp.istioinaction.io:30005/api/catalog

# 호출 테스트 2 : 클라이언트 인증서 및 키 제공 → 성공
curl -v https://webapp.istioinaction.io:30005/api/catalog \
--cacert ch4/certs/2_intermediate/certs/ca-chain.cert.pem \
--cert ch4/certs/4_client/certs/webapp.istioinaction.io.cert.pem \
--key ch4/certs/4_client/private/webapp.istioinaction.io.key.pem

이 설정을 통해 Istio ingress gateway는 클라이언트 인증서를 검증하고,
신뢰 가능한 CA에서 발급된 인증서인 경우에만 통신을 허용합니다.

참고:
Ingress Gateway는 내부적으로 istio-agent가 동작하며, 이는 SDS(Secret Discovery Service)를 통해 인증서를 자동으로 관리하고 전달받습니다.

SDS는 인증서가 변경되었을 때 자동으로 서비스 프록시에 반영할 수 있도록 해줍니다.


✅ Serving multiple virtual hosts with TLS (실습)

Istio의 Ingress Gateway는 하나의 HTTPS 포트(예: 443)에서 여러 가상 호스트에 대해 서로 다른 인증서/키를 사용하여 서비스를 제공할 수 있습니다.
이를 통해 다양한 도메인을 하나의 게이트웨이 리소스를 통해 처리할 수 있으며, 각 도메인마다 개별 인증서를 적용할 수 있습니다.

예를 들어, webapp.istioinaction.iocatalog.istioinaction.io 도메인을 HTTPS로 처리하려면 다음과 같이 구성합니다.

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: coolstore-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https-webapp
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: webapp-credential
    hosts:
    - "webapp.istioinaction.io"
  - port:
      number: 443
      name: https-catalog
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: catalog-credential
    hosts:
    - "catalog.istioinaction.io"

각 가상 호스트마다 사용하는 인증서가 다르므로, 해당 도메인에 맞는 키와 인증서를 먼저 준비한 후 Secret으로 생성합니다.

# 인증서 파일 확인
cat ch4/certs2/3_application/private/catalog.istioinaction.io.key.pem
cat ch4/certs2/3_application/certs/catalog.istioinaction.io.cert.pem
openssl x509 -in ch4/certs2/3_application/certs/catalog.istioinaction.io.cert.pem -noout -text

# catalog 도메인용 Secret 생성
kubectl create -n istio-system secret tls catalog-credential \
--key ch4/certs2/3_application/private/catalog.istioinaction.io.key.pem \
--cert ch4/certs2/3_application/certs/catalog.istioinaction.io.cert.pem

# Gateway 리소스 적용
kubectl apply -f ch4/coolstore-gw-multi-tls.yaml -n istioinaction

다음으로 catalog.istioinaction.io 트래픽을 백엔드 서비스로 라우팅하기 위해 VirtualService 리소스를 설정합니다.

# VirtualService 정의
cat ch4/catalog-vs.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: catalog-vs-from-gw
spec:
  hosts:
  - "catalog.istioinaction.io"
  gateways:
  - coolstore-gateway
  http:
  - route:
    - destination:
        host: catalog
        port:
          number: 80

# VirtualService 적용
kubectl apply -f ch4/catalog-vs.yaml -n istioinaction
kubectl get gw,vs -n istioinaction

테스트를 위해 catalog.istioinaction.io 도메인을 로컬에서 지정합니다.
(※ 실습 후 /etc/hosts 항목은 삭제해주세요.)

# 도메인 설정 추가
echo "127.0.0.1       catalog.istioinaction.io" | sudo tee -a /etc/hosts
cat /etc/hosts | tail -n 2

이제 HTTPS 통신을 테스트합니다.

# 호출 테스트 1 - webapp 도메인
curl -v https://webapp.istioinaction.io:30005/api/catalog \
--cacert ch4/certs/2_intermediate/certs/ca-chain.cert.pem

# 호출 테스트 2 - catalog 도메인
curl -v https://catalog.istioinaction.io:30005/items \
--cacert ch4/certs2/2_intermediate/certs/ca-chain.cert.pem

두 호출 모두 정상 응답을 받았다면, Gateway는 각 요청에 대해 SNI(Server Name Indication) 를 기반으로
올바른 인증서를 선택해 응답했음을 의미합니다.

즉, 클라이언트는 HTTPS 연결 과정의 TLS 핸드쉐이크에서 접근하려는 도메인 정보를 전달하며, Istio의 Envoy는 이를 이용해 해당 도메인에 맞는 인증서를 제시하고 라우팅 처리를 수행합니다.

🔎 참고
SNI(Server Name Indication)는 TLS 확장 중 하나로, ClientHello 메시지에서 도메인을 함께 전송함으로써
단일 포트에 대해 다중 도메인을 처리할 수 있도록 도와줍니다.


👉 Step 04. TCP traffic

이스티오의 게이트웨이는 단순히 HTTP나 HTTPS 트래픽만 처리하는 것이 아닙니다.

TCP 기반의 모든 트래픽도 처리할 수 있습니다.

예를 들어, 다음과 같은 비HTTP 트래픽을 서비스 외부에 노출해야 하는 경우가 있습니다.

  • 데이터베이스: MongoDB, PostgreSQL, MySQL 등
  • 메시지 큐: Kafka, NATS 등
  • 기타 TCP 프로토콜 기반 서비스

이러한 트래픽을 Istio Ingress Gateway를 통해 클러스터 외부에서 접근 가능하게 만들 수 있습니다.

하지만 다음과 같은 제약 사항이 존재합니다:

  • Istio는 HTTP와 달리 TCP 트래픽의 내용을 해석하지 못합니다.
    • 즉, 호스트 이름 기반 라우팅, 경로 기반 라우팅, 헤더 기반 정책 적용 등은 불가능합니다.
    • 재시도, 타임아웃, 서킷 브레이커, 트래픽 분할과 같은 고급 기능도 사용할 수 없습니다.

이는 TCP 연결이 OSI 4계층(전송계층) 수준에서 이루어지고, Istio(Envoy)가 해당 프로토콜을 명확하게 이해하지 못하기 때문입니다.

예외적으로, MongoDB처럼 Envoy가 이해할 수 있는 일부 TCP 프로토콜에 한해서는 일부 기능이 제공되기도 합니다.

이번 섹션에서는 클러스터 외부의 클라이언트가, 클러스터 내부에 존재하는 특정 TCP 기반 서비스에 접근할 수 있도록
Istio Gateway를 통해 TCP 포트를 노출하고 연결을 설정하는 방법을 실습 중심으로 살펴보겠습니다.

✅ Exposing TCP ports on an Istio gateway

Istio Ingress Gateway는 HTTP(S)뿐 아니라 TCP 트래픽도 노출할 수 있습니다.
이번 실습에서는 간단한 TCP 에코 서비스를 만들고, 이를 외부로 노출하는 과정을 진행합니다.

에코 서비스는 텔넷(telnet) 같은 TCP 클라이언트로 접속하면 입력한 내용을 그대로 반환해주는 방식입니다.


1단계: TCP 에코 서비스 배포

cat ch4/echo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tcp-echo-deployment
  labels:
    app: tcp-echo
    system: example
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tcp-echo
  template:
    metadata:
      labels:
        app: tcp-echo
        system: example
    spec:
      containers:
        - name: tcp-echo-container
          image: cjimti/go-echo:latest
          imagePullPolicy: IfNotPresent
          env:
            - name: TCP_PORT
              value: "2701"
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: SERVICE_ACCOUNT
              valueFrom:
                fieldRef:
                  fieldPath: spec.serviceAccountName
          ports:
            - name: tcp-echo-port
              containerPort: 2701
---
apiVersion: v1
kind: Service
metadata:
  name: "tcp-echo-service"
  labels:
    app: tcp-echo
    system: example
spec:
  selector:
    app: "tcp-echo"
  ports:
    - protocol: "TCP"
      port: 2701
      targetPort: 2701
kubectl apply -f ch4/echo.yaml -n istioinaction

kubectl get pod -n istioinaction

2단계: istio-ingressgateway 서비스에 TCP 포트 추가

KUBE_EDITOR="nano"  kubectl edit svc istio-ingressgateway -n istio-system
- name: tcp
  nodePort: 30006
  port: 31400
  protocol: TCP
  targetPort: 31400

kubectl get svc istio-ingressgateway -n istio-system -o jsonpath='{.spec.ports[?(@.name=="tcp")]}'

3단계: TCP 게이트웨이 리소스 생성

cat ch4/gateway-tcp.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: echo-tcp-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 31400
      name: tcp-echo
      protocol: TCP
    hosts:
    - "*"
kubectl apply -f ch4/gateway-tcp.yaml -n istioinaction
kubectl get gw -n istioinaction

4단계: VirtualService 리소스 생성

cat ch4/echo-vs.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: tcp-echo-vs-from-gw
spec:
  hosts:
  - "*"
  gateways:
  - echo-tcp-gateway
  tcp:
  - match:
    - port: 31400
    route:
    - destination:
        host: tcp-echo-service
        port:
          number: 2701
kubectl apply -f ch4/echo-vs.yaml -n istioinaction
kubectl get vs -n istioinaction


5단계: 외부에서 TCP 서비스 접속 확인

brew install telnet
telnet localhost 30006
  • 접속 후 다음과 같이 출력되며, 입력한 메시지가 그대로 에코됩니다.
Welcome, you are connected to node myk8s-control-plane.
Running on Pod tcp-echo-deployment-584f6d6d6b-xcpd8.
In namespace istioinaction.
With IP address 10.10.0.20.
Service default.
hello istio!
hello istio!

텔넷 종료: Ctrl + ] 입력 후 quit 입력


이 실습을 통해 Istio Gateway를 이용한 TCP 서비스 외부 노출 방법을 확인했습니다.
HTTP와 달리 경로 기반 라우팅은 불가능하지만, port 기반 라우팅을 통해 여러 TCP 서비스를 구분할 수 있습니다.



👉 Step 05. Istio 게이트웨이 운영 팁

✅ 운영 팁 들어가기

Istio 게이트웨이를 실무에 적용할 때는 단순한 리소스 정의 외에도 다양한 운영 고려사항이 필요합니다. 이 절에서는 게이트웨이 운영 시 마주치게 되는 주요 이슈들과, 실무에서 도움이 되는 팁을 함께 소개합니다.


👉 이 절은 실습보다는 운영 전략과 관찰 포인트, 구성 시 주의사항 위주로 구성됩니다.
실전에서 발생하는 문제를 미리 방지하거나, 문제 해결에 도움이 되는 인사이트를 제공합니다.


✅ 게이트웨이 책임 나누기 (Split gateway responsibilities)

Istio 인그레스 게이트웨이는 하나만 사용해야 하는 것이 아닙니다. 운영 환경에서는 게이트웨이를 여러 개 생성해 서비스의 특성에 따라 트래픽 경로를 분리하거나 격리하는 것이 권장됩니다. 예를 들어, 민감한 서비스나 고가용성이 요구되는 트래픽을 위해 별도의 진입점을 만들거나, 팀별로 게이트웨이를 분리해 각자의 설정을 갖도록 할 수 있습니다.

Istio는 IstioOperator 리소스를 통해 새로운 게이트웨이를 정의할 수 있습니다. 아래는 istioinaction 네임스페이스에 my-user-gateway라는 이름의 새로운 게이트웨이를 생성하는 예시입니다.

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  name: my-user-gateway-install
  namespace: istioinaction
spec:
  profile: empty
  values:
    gateways:
      istio-ingressgateway:
        autoscaleEnabled: false
  components:
    ingressGateways:
    - name: istio-ingressgateway
      enabled: false    
    - name: my-user-gateway
      namespace: istioinaction
      enabled: true
      label:
        istio: my-user-gateway
      k8s:
        service:
          ports:
            - name: tcp
              port: 31400
              targetPort: 31400
              nodePort: 30007

설정 파일을 적용하여 게이트웨이를 설치합니다.

docker exec -it myk8s-control-plane bash

cat <<EOF > my-user-gateway-edited.yaml
(위 내용 동일)
EOF

istioctl install -y -n istioinaction -f my-user-gateway-edited.yaml
exit

설치가 완료되면 다음과 같이 확인할 수 있습니다.

kubectl get IstioOperator -A
kubectl get deploy my-user-gateway -n istioinaction
kubectl get svc my-user-gateway -n istioinaction -o yaml

이제 새로운 게이트웨이를 활용하여 TCP 서비스를 분리해 노출할 수 있습니다. 앞서 만들었던 TCP echo 서비스에 대해 새 게이트웨이를 지정해봅니다.

cat <<EOF | kubectl apply -n istioinaction -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: echo-tcp-gateway
spec:
  selector:
    istio: my-user-gateway
  servers:
  - port:
      number: 31400
      name: tcp-echo
      protocol: TCP
    hosts:
    - "*"
EOF

kubectl apply -f ch4/echo-vs.yaml -n istioinaction
kubectl apply -f ch4/echo.yaml -n istioinaction

게이트웨이 설정 이후, NodePort 30007 포트를 통해 TCP 통신을 테스트할 수 있습니다.

telnet localhost 30007
...
hello Istio    # 입력
*hello Istio*  # 에코 응답

이처럼 게이트웨이 역할을 명확히 분리하면, 서비스별 요구사항에 따라 리소스와 정책을 독립적으로 관리할 수 있어 유연한 운영이 가능합니다.


✅ 게이트웨이 주입 (Gateway injection)

운영 환경에서 모든 사용자가 IstioOperator 리소스를 직접 사용할 수 있도록 허용하는 것은 위험할 수 있습니다. 대신 게이트웨이 주입(Gateway Injection) 기능을 사용하면, 사용자가 미리 정의된 템플릿 기반으로 부분적으로 정의된 Deployment를 배포하고, Istio가 나머지를 자동으로 채워넣도록 구성할 수 있습니다. 이는 사이드카 주입 방식과 유사합니다.

이 방식은 다음과 같은 장점을 가집니다:

  • 사용자는 전체 Istio 설정을 몰라도 자신만의 게이트웨이를 생성 가능
  • 접근 권한을 최소화하면서 팀별 게이트웨이 배포 가능
  • 통일된 구조로 자동 관리가 가능

다음은 게이트웨이 주입을 적용한 Deployment 정의 예시입니다:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-user-gateway-injected
  namespace: istioinaction
spec:
  selector:
    matchLabels:
      ingress: my-user-gateway-injected
  template:
    metadata:
      annotations:
        sidecar.istio.io/inject: "true"             # 1) 주입 활성화
        inject.istio.io/templates: gateway          # 2) gateway 템플릿 사용
      labels:
        ingress: my-user-gateway-injected
    spec:
      containers:
      - name: istio-proxy                           # 3) 반드시 이 이름 사용
        image: auto                                 # 4) 자동 주입용 placeholder

배포 및 리소스 확인은 다음과 같이 진행합니다:

# 게이트웨이 주입 기반 디플로이먼트 적용
cat ch4/my-user-gw-injection.yaml
kubectl apply -f ch4/my-user-gw-injection.yaml

# 리소스 확인
kubectl get deploy,svc,ep my-user-gateway-injected -n istioinaction

이처럼 게이트웨이 주입 방식을 사용하면, 보안성과 분리된 운영 책임을 유지하면서도 유연하게 게이트웨이를 구성할 수 있습니다. 특히 대규모 조직에서 팀별 게이트웨이 운영 정책을 갖추기 위한 기반으로 매우 유용합니다.


✅ 인그레스 게이트웨이 액세스 로그 (Ingress gateway access logs)

서비스 프록시(Envoy)에서 액세스 로그(access log) 는 트래픽 흐름을 이해하고 디버깅할 때 매우 유용한 도구입니다. 이스티오 또한 인그레스 게이트웨이를 포함한 모든 프록시에 대해 액세스 로그를 제공할 수 있습니다.

기본적으로 데모 프로필에서는 인그레스 게이트웨이와 서비스 프록시가 표준 출력 스트림에 액세스 로그를 출력합니다. 따라서 컨테이너 로그를 단순 조회하는 것만으로도 액세스 로그를 확인할 수 있습니다.

kubectl logs -f deploy/istio-ingressgateway -n istio-system

하지만 기본 프로필로 설치된 운영 환경의 이스티오에서는 액세스 로깅이 비활성화되어 있을 수 있습니다. 이 경우 로그가 출력되지 않으며 다음처럼 초기 부팅 로그 외에는 아무 것도 보이지 않습니다:

kubectl stern -n istioinaction -l app=webapp -c istio-proxy

활성화 방법

운영 환경에서도 액세스 로그를 보고 싶다면 accessLogFile 설정을 /dev/stdout 으로 변경해 표준 출력에 출력되도록 만들 수 있습니다.

docker exec -it myk8s-control-plane bash

istioctl install --set meshConfig.accessLogFile=/dev/stdout

# 이후 확인
kubectl get cm -n istio-system istio -o yaml
...
mesh: |-
  accessLogFile: /dev/stdout
...

이후 실제 애플리케이션 요청을 보내면 istio-proxy 컨테이너 로그에 액세스 로그가 나타납니다:

kubectl stern -n istioinaction -l app=webapp -c istio-proxy
[2025-04-14T09:30:47.764Z] "GET /items HTTP/1.1" 200 ...

운영 환경에서는?

운영 환경에서는 수백~수천 개의 워크로드가 있을 수 있기 때문에 모든 워크로드에 대한 액세스 로그를 출력하는 것은 부담이 될 수 있습니다. 더 나은 방식은 특정 워크로드에만 액세스 로그를 켜는 것입니다.

이를 위해 Telemetry API를 사용해 선택적으로 액세스 로그를 활성화할 수 있습니다.

예를 들어 인그레스 게이트웨이에 대해서만 로그를 활성화하는 설정은 다음과 같습니다:

apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
  name: ingress-gateway
  namespace: istio-system
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway   # 특정 파드 선택
  accessLogging:
  - providers:
    - name: envoy                # 기본 로그 프로바이더
    disabled: false              # 로그 활성화

실제 사례

예를 들어 토스에서는 istio-proxy가 gRPC로 직접 access log collector로 로그를 전송하고, 이를 Kafka → Logstash → Elasticsearch 로 연동하는 자체 수집 파이프라인을 운영하고 있습니다:

이처럼 Istio는 기본적인 로그 수집부터, 복잡한 실시간 분석용 파이프라인 구성까지 유연하게 확장 가능합니다.



✅ 게이트웨이 설정 줄이기 (Reducing gateway configuration)

Istio에서는 기본적으로 모든 프록시가 메시 내의 모든 서비스를 알 수 있도록 구성됩니다. 하지만 메시에 포함된 서비스 수가 많을수록 각 프록시가 저장해야 하는 설정 정보가 커지고, 이로 인해 다음과 같은 문제가 발생할 수 있습니다:

  • 설정이 너무 커져 리소스를 과도하게 사용하게 됨
  • 성능 저하
  • 확장성 문제

사이드카 리소스로 설정 줄이기

일반적인 워크로드의 프록시는 Sidecar 리소스를 통해 자신이 관여할 서비스만 알도록 설정 범위를 줄일 수 있습니다. 하지만 이 방법은 게이트웨이에는 적용되지 않습니다.

게이트웨이(예: 인그레스 게이트웨이)는 기본적으로 모든 클러스터 내 서비스에 대한 라우팅 정보를 가지게 되며, 이로 인해 설정이 매우 커질 수 있습니다.


해결 방법: 게이트웨이 설정 잘라내기

Istio에서는 게이트웨이에 필요한 설정만 포함되도록 필터링하는 기능을 지원합니다. 이 기능은 다음 환경 변수를 통해 활성화할 수 있습니다:

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  name: control-plane
spec:
  profile: minimal
  components:
    pilot:
      k8s:
        env:
        - name: PILOT_FILTER_GATEWAY_CLUSTER_CONFIG
          value: "true"
  meshConfig:
    defaultConfig:
      proxyMetadata:
        ISTIO_META_DNS_CAPTURE: "true"
    enablePrometheusMerge: true

이 설정을 적용하면 Istio의 Pilot이 해당 게이트웨이와 연결된 VirtualService 내에서 참조되는 서비스에 대해서만 클러스터 정보를 전파합니다.


적용 확인 예시

설정 적용 전후로 클러스터 수를 비교할 수 있습니다:

# 적용 전: my-user-gateway가 알고 있는 클러스터 수
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/my-user-gateway.istioinaction | wc -l
# 결과 예: 37개

# 설정 변경
KUBE_EDITOR="nano" kubectl edit IstioOperator -n istioinaction installed-state-my-user-gateway-install
...
  pilot:
    k8s:
      env:
      - name: PILOT_FILTER_GATEWAY_CLUSTER_CONFIG
        value: "true"
...

# 게이트웨이 Pod 재시작 후 다시 확인
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/my-user-gateway.istioinaction | wc -l
# 결과: 줄어들었는지 확인

※ 단, 설정 후에도 클러스터 수가 줄지 않는다면 다음을 확인해봐야 합니다:

  • 적용한 IstioOperator 리소스가 실제 게이트웨이 설치에 반영되는지
  • 해당 게이트웨이에 바인딩된 VirtualService 리소스가 존재하는지

기능 플래그 설명

  • PILOT_FILTER_GATEWAY_CLUSTER_CONFIG:
    해당 플래그를 활성화하면 게이트웨이에 연결된 VirtualService에 실제로 등장하는 클러스터만 Pilot이 프록시에 전송하게 됩니다.
    👉 공식 문서 설명



📌 Conclusion

Istio Gateway는 단순한 L7 리버스 프록시 이상의 역할을 수행하며, 클러스터 외부와 내부를 연결하는 중요한 보안 경계입니다.

이번 스터디에서는 실제 환경에서 필요한 설정과 운영 전략을 실습을 통해 익혔습니다.

특히 다음과 같은 관점에서 게이트웨이를 바라보는 것이 중요합니다:

  • 보안: TLS/mTLS를 통해 모든 트래픽을 암호화하고, 인증서를 통해 서비스 간 신뢰를 구축할 수 있습니다.

  • 유연한 라우팅: 다양한 프로토콜과 포트를 지원하며, SNI 기반 다중 가상 호스트 구성까지 가능합니다.

  • 운영 최적화: 게이트웨이 책임 분리, 액세스 로깅 제어, 설정 범위 최소화를 통해 효율적인 운영이 가능합니다.

실무에서는 트래픽 흐름을 명확히 이해하고, 성능 및 보안 요구사항에 맞게 게이트웨이 구성을 설계-적용-검증하는 것이 핵심입니다.

이번 실습을 통해 단순히 게이트웨이 리소스를 배포하는 것에 그치는 것이 아니라, “왜 이런 구성이 필요한가”, “어떤 선택이 실무에서 문제를 예방하는가”를 깊이 고민하게 되었습니다.
Istio Gateway는 단순한 진입점 이상의 의미를 가지며, 서비스 메쉬의 품질과 안정성을 결정짓는 핵심 요소임을 다시 한 번 느꼈습니다.

앞으로도 트래픽 흐름과 보안 아키텍처를 더 명확히 이해하고, 보다 체계적이고 효율적인 게이트웨이 운영 전략을 고민해 나가고자 합니다.

profile
안녕하세요! DevOps 엔지니어 이재찬입니다. 블로그에 대한 피드백은 언제나 환영합니다! 기술, 개발, 운영에 관한 다양한 주제로 함께 나누며, 더 나은 협업과 효율적인 개발 환경을 만드는 과정에 대해 인사이트를 나누고 싶습니다. 함께 여행하는 기분으로, 즐겁게 읽어주시면 감사하겠습니다! 🚀

0개의 댓글