CloudNet@ 가시다님이 진행하는 Istio 스터디 1기 - 2주차 정리 내용 입니다.
앞서 진행했던 주차를 보고 오시면 이해가 빠릅니다.
2025.04.07 - [스터디/Istio] - Istio 스터디 1주차 - <서비스 메시와 Istio>
2025.04.09 - [스터디/Istio] - Istio 스터디 1주차 - <Istio 실습해보기>
2025.04.16 - [스터디/Istio] - Istio 스터디 2주차 - <Envoy란?>
2025.04.18 - [스터디/Istio] - Istio 스터디 2주차 - <Envoy와 Istio Gateway 실습>
이번에는 실제로 Envoy의 설정 내용을 알아보고 실습을 해보도록 하겠습니다.
📌 Envoy 설정 알아보기
우선 Envoy는 JSON/YAML 형식의 설정 파일로 구성이 됩니다.
아래의 예시를 통해 Envoy 설정을 확인해보겠습니다.
static_resources: listeners: # (1) 리스너 정의 - name: httpbin-demo address: socket_address: { address: 0.0.0.0, port_value: 15001 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager # (2) HTTP 필터 typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http http_filters: - name: envoy.filters.http.router route_config: # (3) 라우팅 규칙 name: httpbin_local_route virtual_hosts: - name: httpbin_local_service domains: ["*"] # (4) 와일드카드 가상 호스트 routes: - match: { prefix: "/" } route: auto_host_rewrite: true cluster: httpbin_service # (5) 클러스터로 라우팅 clusters: - name: httpbin_service # (6) 업스트림 클러스터 connect_timeout: 5s type: LOGICAL_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: httpbin endpoints: - lb_endpoints: - endpoint: address: socket_address: address: httpbin port_value: 8000
✅ static_resources:
- Envoy가 부팅 시 고정으로 로드하는 리소스들을 정의
- listeners(수신 포트 설정), clusters(백엔드 서비스 정보)
✅ listeners:
- 클라이언트로부터 요청을 수신할 포트 정의
- Envoy가 바인딩될 IP 및 포트, 그리고 요청을 처리할 필터 체인 설정
✅ filter_chains:
- 수신된 요청을 어떻게 처리할지 결정하는 필터 목록
✅ filters:
- 리스너가 요청을 처리할 때 사용할 네트워크 필터 정의
✅ typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
- 위 타입으로 정의한 HttpConnectionManager는 HTTP 요청을 수신, 라우팅, 응답하는 역할을 합니다.
✅ stat_prefix: ingress_http
- Envoy Metrics에서 사용될 접두어
ex) ingress_http.downstream_rq_total✅ http_filters:
- HTTP 요청을 처리할 필터 체인
✅ route_config:
- 요청에 대한 라우팅 규칙 정의
ex) 어떤 URL/도메인 조합의 요청을 어떤 클러스터로 보낼지 결정✅ virtual_hosts:
- HTTP 요청을 도메인 기준으로 묶어 라우팅할 수 있도록 정의
- domains: ["*"]: 모든 호스트에 대해 요청 허용
- match: { prefix: "/" }: 모든 URI에 대해 매칭
- route.cluster: 매칭된 요청을 httpbin_service라는 클러스터로 전달
- auto_host_rewrite: true: 요청의 Host 헤더를 클러스터의 도메인으로 자동 변경
✅ Clusters:
- 위 route 에서 참조하는 벡엔드 클러스터를 정의 해줍니다.
✅ name:
- 백엔드 클러스터 이름 정의
✅ connect_timeout:
- 클러스터에 연결 시도할 때 타임아웃 시간 정의
- ex) connect_timeout: 5s -> 5초 동안 응답이 없으면 실패 처리
✅ type:
- 백엔드 서버의 위치를 어떻게 식별하고 연결할지 결정
- ex) address.socket_address.address: 에 정의된 내용이 IP가 아닌 도메인 이기 때문에, 예시 YAML 코드에서는 type을 LOGICAL_DNS로 정의
✅ dns_lookup_family: V4_ONLY
- DNS 해석 시 IPv4 주소만 사용
✅ lb_policy: ROUND_ROBIN
- round_robin 방식으로 요청 처리
✅ load_assignment:
- 클러스터가 연결할 실제 엔드포인트(서버)의 주소를 지정
✅ cluster_name: httpbin
- load_assignment이 속한 클러스터의 이름입니다. name과 반드시 일치하지 않아도 되지만, 관례상 동일하게 맞춥니다.
✅ endpoints: → lb_endpoints: → endpoint:
- 실제 백엔드 주소를 계층적으로 정의
socket_address: address: httpbin port_value: 8000
- httpbin:8000 에 요청을 보냅니다.
- 이 httpbin은 DNS로 이름 해석 가능한 서비스명 or 도메인이어야 합니다.
🛠️ 정리
- 위 설정 파일은 들어오는 트래픽이 연결할 수 있는 리스너를 만들고, 모든 트래픽을 httpbin 클러스터로 라우팅 한다.
- 또한 사용할 로드 밸런싱 설정과 커넥션 타임아웃 종류도 지정
- 해당 프록시를 호출 시, 요청이 httpbin 서비스로 라우팅
Envoy의 구성 YAML 파일에 대해 알아보았습니다.
위 설정 파일은 완전히 정적인 설정 파일 입니다.
Envoy 실습 하기에 앞서 xDS API를 이용해 어떻게 동적으로 설정하는지 알아보겠습니다.
🛠️ Envoy 동적 설정을 위한 xDS API 알아보기
✅ xDS 란?
xDS는 Envoy가 Control Plane으로 부터 설정을 실시간으로 받아오도록 설계된 API 집합 입니다.
ex) LDS, RDS, CDS, EDS 등 이 존재하여 공통된 접미사 DS(Discovery Service) 앞에 x가 붙는 것 입니다.
이 API들은 보통 gRPC 기반의 streaming 방식으로 동작하여 설정이 변경되면 Envoy가 즉시 적용할 수 있도록 해줍니다.
이제 API의 종류에 대해 설명해보도록 하겠습니다.
🖥️ Listener discovery service (LDS)
LDS는 리스너 정보를 동적으로 가져오는 역할을 하는 API 입니다.
즉, Envoy가 외부 요청을 수신할 리스너(포트, IP) 설정을 동적으로 가져오게끔 도와주는 API 입니다.
ex) Envoy가 8080에서 요청을 받을지 443에서 받을 지 결정
🖥️ Route discovery service (RDS)
RDS는 Listener를 통해 들어온 traffic을 어떤 클러스터로 라우팅이 될지 정의해주는 API 입니다.
ex) /productpage 페이지로 요청이 들어오면 productpage-v1 POD 집합으로 요청을 보내줍니다.
🖥️ Cluster discovery service (CDS)
CDS는 Envoy가 요청을 보낼 수 있는 클러스터 단위 서비스 목록을 제공합니다.
서비스 목록이라 함은, K8s 클러스터에 존재하는 Service를 의미합니다.
🖥️ Endpoint discovery service (EDS)
EDS는 Cluster안에 실제 요청이 전달될 IP:PORT 정보(=Endpoint) 들을 동적으로 제공합니다.
ex) POD가 수평 확장되면 EDS를 통해 새로운 POD IP 목록을 Envoy에 실시간으로 전달 해줍니다.
🖥️ Secret discovery service (SDS)
SDS는 Envoy가 사용하는 TLS 인증서 키, CA 등의 보안 자격 정보를 실시간으로 갱신해주는 API 입니다.
ex)
- Cert-Manager 또는 Istio를 사용해 mTLS 인증서 자동 갱신
- Envoy는 필요 시 재요청하거나 자동으로 적용
🖥️ Aggregated discovery service (ADS)
ADS는 위 모든 LDS/RDS/CDS/EDS/SDS 요청을 하나의 gRPC 연결로 통합 관리해주는 API 입니다.
하나의 gRPC 연결로 LDS -> RDS -> CDS -> EDS 순서로 처리를 합니다.
이제 실습을 해보도록 하겠습니다
📌 기본 실습 진행

기본 실습 내용은 총 3가지 입니다.
- 엔드포인트에 따라 호출할 때 사용한 헤더를 반환 결과 확인
- HTTP 요청 지연
- 오류 발생 후 retry 하도록 설정
1. 엔드포인트에 따라 호출할 때 사용한 헤더를 반환 결과 확인
🛠️ 실습을 위해 도커 이미지를 받아줍니다.
$ docker pull envoyproxy/envoy:v1.19.0
$ docker pull curlimages/curl
$ docker pull mccutchen/go-httpbin
🛠️ httpbin 서비스 실행
$ docker run -d -e PORT=8000 --name httpbin mccutchen/go-httpbin
🛠️ curl 컨테이너로 httpbin 호출
$ docker run -it --rm --link httpbin curlimages/curl curl -X GET http://httpbin:8000/headers
결과
{
"headers": {
"Accept": [
"*/*"
],
"Host": [
"httpbin:8000"
],
"User-Agent": [
"curl/8.13.0"
]
}
}
그러면 요청을 할 때 사용한 헤더들이 나오게 됩니다.
🛠️ 이제 Envoy 프록시를 실행하고 help 명령어를 사용하여 설정 파일을 적용하는 방법을 확인해줍니다.
$ docker run -it --rm envoyproxy/envoy:v1.19.0 envoy --help
결과
-c <string>, --config-path <string> # 설정 파일을 전달
Path to configuration file
🛠️ 아래의 설정 파일 내용을 Envoy에 적용 시켜보도록 하겠습니다.
admin:
address:
socket_address: { address: 0.0.0.0, port_value: 15000 }
static_resources:
listeners:
- name: httpbin-demo
address:
socket_address: { address: 0.0.0.0, port_value: 15001 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
http_filters:
- name: envoy.filters.http.router
route_config:
name: httpbin_local_route
virtual_hosts:
- name: httpbin_local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
auto_host_rewrite: true
cluster: httpbin_service
clusters:
- name: httpbin_service
connect_timeout: 5s
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 8000
- 위 설정 파일을 보면 15001 포트로 받는 트래픽을 httpbin 도메인의 8000 포트로 전달 하도록 설정이 되어있습니다.
설정대로 동작을 하는지 확인해보겠습니다.
🛠️ Envoy 실행
$ docker run --name proxy --link httpbin envoyproxy/envoy:v1.19.0 --config-yaml "$(cat /root/istio-in-action/book-source-code-master/ch3/simple.yaml)"
정상적으로 실행이 되었으면 이제 curl 컨테이너를 사용하여 프록시 호출을 해보겠습니다.
🛠️ 프록시 호출
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15001/headers
결과
{
"headers": {
"Accept": [
"*/*"
],
"Host": [
"httpbin"
],
"User-Agent": [
"curl/8.13.0"
],
"X-Envoy-Expected-Rq-Timeout-Ms": [
"15000"
],
"X-Forwarded-Proto": [
"http"
],
"X-Request-Id": [
"3fd55f78-4b23-4428-adbb-54351c74df20"
]
}
}
결과를 보면, 프록시에 curl 요청을 하였지만 httpbin 으로 트래픽이 라우팅 된 것을 확인 할 수 있습니다.
또한, 새로운 헤더도 추가되었습니다.
- X-Request-Id
- X-Envoy-Expected-Rq-Timeout-Ms
X-Request-Id는 클러스터 내 다양한 요청 사이의 관계를 파악하고 요청을 처리하기 위해 거처 가는 여러 서비스(즉, 여러 홉)를 추적하는데 활용됩니다.
X-Envoy-Expected-Rq-Timeout-Ms는 업스트림 서비스에 대한 힌트로, 요청이 15,000ms 후에 타임아웃 된다고 알려주고 있습니다.
2. HTTP 요청 지연
위에서 사용했던 프록시 설정에서 약간만 변경 하면 됩니다.
- match: { prefix: "/" }
route:
auto_host_rewrite: true
cluster: httpbin_service
timeout: 1s # 여기서 timeout 추가
🛠️ 앞서 생성했던 프록시 삭제 후 다시 실행
$ docker rm -f proxy
$ docker run --name proxy --link httpbin envoyproxy/envoy:v1.19.0 --config-yaml "$(cat /root/istio-in-action/book-source-code-master/ch3/simple_change_timeout.yaml)"
🛠️ 타임아웃 설정 변경 확인
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15001/headers
결과
{
"headers": {
"Accept": [
"*/*"
],
"Host": [
"httpbin"
],
"User-Agent": [
"curl/8.13.0"
],
"X-Envoy-Expected-Rq-Timeout-Ms": [
"1000" # 1000ms = 1s 변경 확인
],
"X-Forwarded-Proto": [
"http"
],
"X-Request-Id": [
"1ec6116b-da79-4886-9ae1-6254b13bb964"
]
}
}
변경이 정상적으로 된 것을 알 수 있습니다.
3. 오류 발생 후 retry 하도록 설정
마지막으로 httpbin 요청을 일부로 실패시켜 Envoy가 어떻게 요청을 자동으로 재시작하는지 보도록 하겠습니다.
먼저 retry_policy를 사용하도록 설정 파일을 업데이트 해줍니다.
- match: { prefix: "/" }
route:
auto_host_rewrite: true
cluster: httpbin_service
retry_policy:
retry_on: 5xx # 5xx 일때 재시도
num_retries: 3 # 재시도 횟수
이제 실습 해보도록 하겠습니다.
🛠️ 앞서 생성했던 프록시 삭제 후 다시 실행
$ docker rm -f proxy
$ docker run -p 15000:15000 --name proxy --link httpbin envoyproxy/envoy:v1.19.0 --config-yaml "$(cat /root/istio-in-action/book-source-code-master/ch3/simple_retry.yaml)"
🛠️ http 로그 레벨 info -> debug로 변경
$ docker run -it --rm --link proxy curlimages/curl curl -X POST http://proxy:15000/logging?http=debug
🛠️ /stats/500 경로로 프록시를 호출 : 이 경로로 httphbin 호출하면 오류가 발생
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15001/status/500
🛠️ 호출이 끝났는데 아무런 응답도 보이지 않는다. 엔보이 Admin API에 확인
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15000/stats | grep retry
결과
cluster.httpbin_service.circuit_breakers.default.rq_retry_open: 0
cluster.httpbin_service.circuit_breakers.high.rq_retry_open: 0
cluster.httpbin_service.retry.upstream_rq_500: 3
cluster.httpbin_service.retry.upstream_rq_5xx: 3
cluster.httpbin_service.retry.upstream_rq_completed: 3
cluster.httpbin_service.retry_or_shadow_abandoned: 0
cluster.httpbin_service.upstream_rq_retry: 3
cluster.httpbin_service.upstream_rq_retry_backoff_exponential: 3
cluster.httpbin_service.upstream_rq_retry_backoff_ratelimited: 0
cluster.httpbin_service.upstream_rq_retry_limit_exceeded: 1
cluster.httpbin_service.upstream_rq_retry_overflow: 0
cluster.httpbin_service.upstream_rq_retry_success: 0
vhost.httpbin_local_service.vcluster.other.upstream_rq_retry: 0
vhost.httpbin_local_service.vcluster.other.upstream_rq_retry_limit_exceeded: 0
vhost.httpbin_local_service.vcluster.other.upstream_rq_retry_overflow: 0
vhost.httpbin_local_service.vcluster.other.upstream_rq_retry_success: 0
API 호출로 확인해보면, Envoy는 업스트림 클러스터 httpbin을 호출 할 때, 500 응답을 받았습니다.
그리고 Envoy는 요청을 재시도 했으며, cluster.httpbin_service.upstream_rq_retry: 3 앞서 설정 파일에서 설정한 대로 3번 retry를 하였습니다.
Admin API를 사용하여 추가적인 정보 확인
아래 명령어들을 사용하면 추가적인 정보를 확인 할 수 있습니다.
# 머신상의 인증서
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15000/certs
# 엔보이에 설정한 클러스터
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15000/clusters
# 엔보이 설정 덤프
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15000/config_dump
# 엔보이에 설정한 리스너
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15000/listeners
# 로깅 설정 확인 가능
$ docker run -it --rm --link proxy curlimages/curl curl -X POST http://proxy:15000/logging
# 로깅 설정 편집 가능
$ docker run -it --rm --link proxy curlimages/curl curl -X POST http://proxy:15000/logging?http=debug
# 엔보이 통계
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15000/stats
# 엔보이 통계(프로메테우스 레코드 형식)
$ docker run -it --rm --link proxy curlimages/curl curl -X GET http://proxy:15000/stats/prometheus
이렇게 간단한 실습을 통해서 Envoy가 프록시로서 어떻게 동작을 하는지 알아보았습니다.
그러면 Envoy는 왜 Istio에 적합한 지에 대해서도 알아보겠습니다.
📌 Envoy는 왜 Istio에 적합할까?
Istio는 마이크로서비스 아키텍처 환경에서 서비스 간 트래픽 제어, 보안 정책 적용, 관찰성 확보를 가능하게 해주는 서비스 메시 입니다.
그리고 그 중심에는 Envoy Proxy가 존재합니다.
따라서 Envoy는 Istio의 대부분 기능에서 핵심 역할을 수행합니다.
하지만, Envoy 혼자서는 완벽한 서비스 메시를 구성할 수 없습니다.
이를 위해서 Envoy를 보조하고 구성 정보를 전달해주는 컨트롤 플레인이 필요합니다.
여기서 등장하는 것이 바로 istiod 입니다.
🧭 Istio 아키텍처에서 Envoy의 위치
Envoy는 정적 설정 없이도 동적으로 구성될 수 있습니다.
그 이유는 Istio가 Envoy의 설정을 xDS API를 통해 실시간으로 전달하기 때문입니다.

위 그림을 보면,
1. 사용자가 VirtualService, DestinationRule 등을 설정
2. istiod는 Kubernetes API 로부터 설정을 읽음
3. xDS API (LDS, RDS, CDS, EDS 등)를 통해 Envoy에게 구성 정보를 전달
4. Envoy는 해당 정보를 기반으로 라우팅, 로드밸런싱, 서비스 디스커버리 수행
🔎 이 처럼 Envoy는 Kubernetes 내부 서비스 목록을 직접 조회하지 않고,
Istio가 Kubernetes의 서비스 정보를 수집하고, Envoy에게 추상화된 구성만을 전달합니다.
📊 관찰성과 텔레메트리 수집
또한 Envoy는 다양한 메트릭, 로그, 트레이스 정보를 수집할 수 있는 기능을 내장하고 있습니다.
하지만 이 정보들을 외부 시스템으로 전달하려면 추가 설정이 필요합니다.

위 그림 처럼,
- Istio는 Prometheus, Grafana, Jaeger, Zipkin 등과 통합 설정을 자동으로 구성
- Envoy는 수집한 데이터를 지정된 수집 엔진으로 전송
- 이를 통해 사용자는 트래픽 흐름, 지연 시간, 오류율 등을 시각화 하여 확인 가능
🔐 TLS 종료와 보안 구성
마지막으로 서비스 간 통신을 암호화(TLS)하려면 인증서 관리가 필요합니다.
Envoy는 TLS를 처리할 수 있지만, 인증서 생성·서명·로테이션을 스스로 하지 않습니다.

- Istio는 자동 인증서 관리 기능(Mutual TLS)을 제공하며,
- istiod는 CA 역할을 하며, Envoy에 적절한 인증서를 동적으로 전달합니다.
- 이를 통해 서비스 간 통신은 안전하게 암호화되고 인증됩니다.
이와 같이 Istio의 구성 요소와 Envoy는 강력한 서비스 메시를 구현하는데 함께 기여를 합니다.
따라서 Envoy를 Istio 프록시라고 부르게 되었습니다.
✅ 요약
- Envoy는 애플리케이션이 애플리케이션 수준의 동작에 사용할 수 있는 프록시입니다.
- Envoy는 이스티오의 데이터 플레인입니다.
- Envoy는 클라우드 신뢰성 문제(네트워크 장애, 토폴로지 변경, 탄력성)를 일관되고 정확하게 해결하는 데 도움을 줄 수 있습니다.
- Envoy는 런타임 제어를 위해 동적 API를 사용합니다(Istio는 이를 사용합니다).
- Envoy는 애플리케이션 사용 및 프록시 내부에 대한 강력한 지표와 정보를 많이 노출합니다.
🎯 Istio Ingress Gateway 실습
외부 클라이언트의 트래픽을 Istio Ingress Gateway를 통해 클러스터 내부 서비스로 안전하고 유연하게 라우팅하는 실습을 해보도록 하겠습니다.
📌 세부 목표 분석
1. 클러스터 진입 지점 정의
- Ingress Gateway를 통해 외부에서 오는 트래픽이 클러스터 안으로 들어올 수 있는 경로(entry point) 를 설정합니다.
2. Ingress 트래픽을 내부 배포 서비스로 라우팅
- 외부 요청을 클러스터 내부의 특정 서비스 또는 파드로 전달하는 라우팅 설정을 실습합니다 (예: VirtualService 사용).
3. 보안 설정 (HTTPS, x.509 인증서 등)
- 인입(Ingress) 트래픽을 TLS 등으로 암호화하고 인증서를 이용해 보호하는 방법도 학습합니다.
4. 비 HTTP(S) 트래픽 (TCP 등)의 처리
- 단순한 HTTP뿐 아니라, TCP 기반의 트래픽도 Istio를 통해 라우팅 및 관리할 수 있다는 점을 실습합니다.
✅ 실습 환경 준비
🛠️ kind를 사용하여 k8s 클러스터를 생성
# 실습에 필요한 파일 다운로드
$ git clone https://github.com/AcornPublishing/istio-in-action
$ cd istio-in-action/book-source-code-master
# kind 사용하여 k8s 클러스터 설치
$ kind create cluster --name myk8s --image kindest/node:v1.23.17 --config - <30000-30007/tcp, 127.0.0.1:34917->6443/tcp myk8s-control-plane
# 노드에 기본 툴 설치
$ 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'
🛠️ istio 1.17.8 설치
# myk8s-control-plane 진입 후 설치 진행
$ docker exec -it myk8s-control-plane bash
# 코드 파일 마운트 확인
$ tree /istiobook/ -L 1
# 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
결과
1.17.8
# 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
🛠️ 실습을 위한 네임스페이스 설정 및 필요한 서비스 NodePort로 변경
# 실습을 위한 네임스페이스 설정
$ 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}]}}'
🛠️ 접속 테스트용 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 IngressGateway 개념 및 동작 확인
Istio에는 네트워크 인그레스 포인트 역할을 하는 IngressGateway가 존재합니다.
여기서 Ingress란?
클러스터 외부에서 내부로 들어오는 트래픽을 뜻 합니다.
그리고 아래의 그림처럼 Istio의 IngressGateway는 외부 트래픽을 수신하여 내부 서비스로 라우팅 하는 역할을 합니다.

이제 실제로 어떻게 동작을 하고 있는지 확인해보겠습니다.
🛠️ 설치된 istio gateway 확인
$ kubectl get pod -n istio-system -l app=istio-ingressgateway
결과
NAME READY STATUS RESTARTS AGE
istio-ingressgateway-996bc6bb6-nwdjb 1/1 Running 0 3h9m
🛠️ proxy 상태 확인
$ docker exec -it myk8s-control-plane istioctl proxy-status
결과
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
istio-ingressgateway-996bc6bb6-nwdjb.istio-system Kubernetes SYNCED SYNCED SYNCED NOT SENT NOT SENT istiod-7df6ffc78d-frvkk 1.17.8
🛠️ proxy 설정 확인
$ 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
결과
ADDRESS PORT MATCH DESTINATION
0.0.0.0 15021 ALL Inline Route: /healthz/ready*
0.0.0.0 15090 ALL Inline Route: /stats/prometheus*
결과를 보면 현재 route 경로와 사용되는 포트를 확인 할 수 있습니다.
🛠️ route 경로와 매핑되는 도메인 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system
결과
NAME DOMAINS MATCH VIRTUAL SERVICE
* /stats/prometheus*
* /healthz/ready*
이제 아래의 그림처럼, Gateway 리소스를 사용하여 개방하고 싶은 포트와 그 포트에서 허용할 가상 호스트를 지정해보겠습니다.

🛠️ 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) 이 포트의 호스트
이제 위 Gateway 리소스 파일을 적용시켜보도록 하겠습니다.
🛠️ istiod 로그 열어두기
$ kubectl logs -f -n istio-system -l app=istiod

🛠️ Gateway 리소스 적용 후 로그 확인
$ kubectl -n istioinaction apply -f /root/istio-in-action/book-source-code-master/ch4/coolstore-gw.yaml

보면 새롭게 로그가 추가 된 것을 확인 할 수 있습니다.
로그 내용을 보면, coolstore-gateway가 추가되고 LDS API를 사용하여 Gateway 리소스에서 정의한 80 포트로 리스닝하도록 설정
RDS API를 사용하여 istio-ingressgateway에게 라우팅 설정을 전달하고 있습니다.
🛠️ 이제 다시 istio proxy-status 확인
$ docker exec -it myk8s-control-plane istioctl proxy-status
결과
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
istio-ingressgateway-996bc6bb6-nwdjb.istio-system Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-frvkk 1.17.8
결과를 보면 이전에는 RDS 가 NOT SENT 상태였는데, SYNCED 된 것을 확인할 수 있습니다.
🛠️ 리스너, 라우트 설정 확인
$ 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
0.0.0.0 15021 ALL Inline Route: /healthz/ready*
0.0.0.0 15090 ALL Inline Route: /stats/prometheus*
$ docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/istio-ingressgateway.istio-system
결과
NAME DOMAINS MATCH VIRTUAL SERVICE
http.8080 * /* 404
* /stats/prometheus*
* /healthz/ready*
리스너, 라우트 결과를 보면 새롭게 추가 된 것을 확인 할 수 있습니다.
그런데 갑자기 8080 포트가 튀어 나왔는데, http.8080 정보의 의미를 확인하겠습니다.
$ kubectl get svc -n istio-system istio-ingressgateway -o jsonpath="{.spec.ports}" | jq
결과
[
{
"name": "status-port",
"nodePort": 31633,
"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
}
]
결과를 보면, 외부에서 80 포트(=NodePort 30000)를 사용할 경우, 내부의 8080 포트(Envoy)로 트래픽을 전달하겠다는 의미입니다.
앞서 Gateway 리소스를 생성할 때 나왔던 로그를 보면 80 포트와 매핑된 VirtualHost가 없다고 나옵니다.
이제 80 포트와 매핑되는 가상 호스트를 설정해보도록 하겠습니다.
🖥️ VirtualSerivce 란?
앞서 진행했던 내용은 Istio Gateway 설정을 했습니다.
특정 포트를 노출하고, 그 포트에서 특정 프로토콜을 예상하고, 그 포트/프로토콜 쌍에서 서빙할 특정 호스트를 정의 하였습니다.
이제 트래픽이 게이트웨이로 들어오면 서비스 메시 내 특정 서비스로 가져올 방법이 필요합니다.
해당 방법이 바로 VirtualService 리소스 입니다.
🛠️ 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
위 VS(VirtualService)의 내용을 보면, 아래와 같습니다.
- VS를 적용할 게이트웨이 정의
- 라우팅 시켜줄 호스트와 포트 정의
이제 실제로 적용해보도록 하겠습니다.
🛠️ VS 적용
$ kubectl apply -n istioinaction -f /root/istio-in-action/book-source-code-master/ch4/coolstore-vs.yaml
그리고 적용한 로그를 확인해보면 아래와 같이 추가된 것을 확인 할 수 있습니다.

$ 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": "webapp.istioinaction.io:80",
"domains": [
"webapp.istioinaction.io" #1 비교할 도메인
],
"routes": [
{
"match": {
"prefix": "/"
},
"route": { #2 라우팅 할 곳
"cluster": "outbound|80||webapp.istioinaction.svc.cluster.local",
"timeout": "0s",
"retryPolicy": {
"retryOn": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes",
"numRetries": 2,
"retryHostPredicate": [
{
"name": "envoy.retry_host_predicates.previous_hosts",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate"
}
}
],
"hostSelectionRetryMaxAttempts": "5",
"retriableStatusCodes": [
503
]
},
"maxGrpcTimeout": "0s"
},
"metadata": {
"filterMetadata": {
"istio": {
"config": "/apis/networking.istio.io/v1alpha3/namespaces/istioinaction/virtual-service/webapp-vs-from-gw"
}
}
},
"decorator": {
"operation": "webapp.istioinaction.svc.cluster.local:80/*"
}
}
],
"includeRequestAttemptCount": true
}
],
"validateClusters": false,
"ignorePortInHostMatching": true
}
]
결과를 보면 virtualhost의 도메인과, 라우트 할 서비스의 도메인 경로가 나와있습니다.
이제 동작을 위해서 실제 애플리케이션을 배포 해보겠습니다.
🛠️ 애플리케이션 배포
$ kubectl apply -f /istio-in-action/book-source-code-master/services/catalog/kubernetes/catalog.yaml -n istioinaction
$ kubectl apply -f /istio-in-action/book-source-code-master/services/webapp/kubernetes/webapp.yaml -n istioinaction
🛠️ 프록시 설정 확인
$ docker exec -it myk8s-control-plane istioctl proxy-status
결과
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
catalog-6cf4b97d-rk98q.istioinaction Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-frvkk 1.17.8
istio-ingressgateway-996bc6bb6-nwdjb.istio-system Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-frvkk 1.17.8
webapp-7685bcb84-m58jg.istioinaction Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-frvkk 1.17.8
결과를 보면, catlog, webapp 파드 앞에도 istio-proxy가 생긴 것을 확인할 수 있습니다.
🛠️ cluster의 도메인 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/istio-ingressgateway.istio-system | egrep 'TYPE|istioinaction'
결과
SERVICE FQDN PORT SUBSET DIRECTION TYPE DESTINATION RULE
catalog.istioinaction.svc.cluster.local 80 - outbound EDS
webapp.istioinaction.svc.cluster.local 80 - outbound EDS
🛠️ cluster의 엔드포인트 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/istio-ingressgateway.istio-system | egrep 'ENDPOINT|istioinaction'
결과
ENDPOINT STATUS OUTLIER CHECK CLUSTER
10.10.0.12:3000 HEALTHY OK outbound|80||catalog.istioinaction.svc.cluster.local
10.10.0.13:8080 HEALTHY OK outbound|80||webapp.istioinaction.svc.cluster.local
결과에 나온 엔드포인트를 보면, 실제로 POD의 IP인 것을 확인 할 수 있습니다.
Istio를 사용하면 VS(VirtualService)에서 라우트할 destination의 호스트를 Service 명으로 지정을 해도,
실제로는 직접 POD의 IP로 트래픽을 전송하게 됩니다.
🛠️ netshoot 파드 내부에서 catalog, webapp 접속 확인
$ kubectl exec -it netshoot -- curl -s http://catalog.istioinaction/items/1 | jq
결과
{
"id": 1,
"color": "amber",
"department": "Eyewear",
"name": "Elinor Glasses",
"price": "282.00"
}
$ kubectl exec -it netshoot -- curl -s http://webapp.istioinaction/api/catalog/items/1 | jq
결과
{
"id": 1,
"color": "amber",
"department": "Eyewear",
"name": "Elinor Glasses",
"price": "282.00"
}
이제 외부에서 호출 시도를 해보겠습니다.
$ curl http://localhost:30000/api/catalog -v
결과
* Host localhost:30000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:30000...
* connect to ::1 port 30000 from ::1 port 38780 failed: Connection refused
* Trying 127.0.0.1:30000...
* Connected to localhost (127.0.0.1) port 30000
> GET /api/catalog HTTP/1.1
> Host: localhost:30000
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< date: Thu, 17 Apr 2025 07:11:17 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host localhost left intact
$ 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"
},
{
"id": 2,
"color": "cyan",
"department": "Clothing",
"name": "Atlas Shirt",
"price": "127.00"
},
{
"id": 3,
"color": "teal",
"department": "Clothing",
"name": "Small Metal Shoes",
"price": "232.00"
},
{
"id": 4,
"color": "red",
"department": "Watches",
"name": "Red Dragon Watch",
"price": "232.00"
}
]
결과의 차이를 보면, Gateway와 VirtualService에서 정의한 Host 헤더의 설정이 일치 할 때는 결과 값이 정상적으로 나오고,
불 일치 할 때는 결과 값이 404 로 나오는 것을 확인 할 수 있습니다.
이제 게이트웨이 트래픽 보안에 대해 알아보겠습니다
Istio 사용하여 트래픽 보안
트래픽 보호는 클라이언트가 통신하려는 서버의 진위를 가리는 것에서 시작할 수 있다.
자신이 그 서비스라고 주장하는 서비스가 정말 통신하려는 서비스가 맞는지 검증하는 것이다.
또한 누군가가 도청하는 일을 막고 싶으므로 트래픽을 암호화해야 합니다.
Istio의 게이트웨이 구현을 사용하면 들어오는 TLS/SSL 트래픽을 종료하고, 백엔드 서비스로 전달하고, TLS가 아닌 트래픽을 적절한 TLS 포트로 리다이렉트하고, mTLS를 구현할 수 있습니다.
🔒 HTTP traffic with TLS 실습

위 그림 처럼 Istio Gateway에 TLS 인증서를 설정하면, 외부에서 오는 트래픽을 자동으로 암호화 해서 안전하게 내부로 전달하도록 해보겠습니다.
🛠️ istio-ingressgateway 가 인증서와 키를 사용하도록 설정하기 위해 먼저 인증서/키를 쿠버네티스 시크릿으로 생성
$ 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
$ kubectl get secret -n istio-system
결과
NAME TYPE DATA AGE
.......
webapp-credential kubernetes.io/tls 2 25s
🛠️ Istio 게이트웨이 리소스가 인증서와 키 사용할 수 있도록 리소스 내용 추가
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: coolstore-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80 #1 HTTP 트래픽 허용
name: http
protocol: HTTP
hosts:
- "webapp.istioinaction.io"
- port:
number: 443 #2 HTTPS 트래픽 허용
name: https
protocol: HTTPS
tls:
mode: SIMPLE #3 보안 연결
credentialName: webapp-credential #4 TLS 인증서가 들어 있는 쿠버네티스 시크릿 이름
hosts:
- "webapp.istioinaction.io"
🛠️ 게이트웨이 설정 적용
$ kubectl apply -f /root/istio-in-action/book-source-code-master/ch4/coolstore-gw-tls.yaml -n istioinaction
🛠️ Secret 리소스가 Envoy 프록시에 적용되었는지 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/istio-ingressgateway.istio-system
결과
RESOURCE NAME TYPE STATUS VALID CERT SERIAL NUMBER NOT AFTER NOT BEFORE
default Cert Chain ACTIVE true 135624833228815846167305474128694379500 2025-04-14T03:57:41Z 2025-04-13T03:55:41Z
kubernetes://webapp-credential Cert Chain ACTIVE true 1049106 2041-06-29T12:49:32Z 2021-07-04T12:49:32Z
ROOTCA CA ACTIVE true 236693590886065381062210660883183746411 2035-04-11T03:57:31Z 2025-04-13T03:57:31Z
🛠️ 이전과 동일하게 호출 테스트 진행
$ 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
인증서를 적용하고 난 후에는 이전과 동일하게 호출하면 실패하는 것을 확인할 수 있습니다.
🛠️ 인증서를 지정하고 호출 테스트 진행
$ 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
인증서를 지정을 했는데도 실패를 했습니다.
원인은 서버 인증서가 발급된 도메인 "webapp.istioinaction.io"로 호출하지 않아서 입니다.
🛠️ 도메인 변경 후 다시 호출 테스트 진행
$ echo "127.0.0.1 webapp.istioinaction.io" | sudo tee -a /etc/hosts
$ curl -v https://webapp.istioinaction.io:30005/api/catalog \
--cacert ch4/certs/2_intermediate/certs/ca-chain.cert.pem
결과
* Host webapp.istioinaction.io:30005 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.1
* Trying 127.0.0.1:30005...
* Connected to webapp.istioinaction.io (127.0.0.1) port 30005
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* CAfile: ch4/certs/2_intermediate/certs/ca-chain.cert.pem
* CApath: /etc/ssl/certs
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
* subject: C=US; ST=Denial; L=Springfield; O=Dis; CN=webapp.istioinaction.io
* start date: Jul 4 12:49:32 2021 GMT
* expire date: Jun 29 12:49:32 2041 GMT
* common name: webapp.istioinaction.io (matched)
* issuer: C=US; ST=Denial; O=Dis; CN=webapp.istioinaction.io
* SSL certificate verify ok.
* Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 1: Public key type RSA (4096/152 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://webapp.istioinaction.io:30005/api/catalog
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: webapp.istioinaction.io:30005]
* [HTTP/2] [1] [:path: /api/catalog]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
> GET /api/catalog HTTP/2
> Host: webapp.istioinaction.io:30005
> User-Agent: curl/8.5.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 200
< content-length: 357
< content-type: application/json; charset=utf-8
< date: Thu, 17 Apr 2025 08:31:16 GMT
< x-envoy-upstream-service-time: 167
< server: istio-envoy
<
* Connection #0 to host webapp.istioinaction.io left intact
[{"id":1,"color":"amber","department":"Eyewear","name":"Elinor Glasses","price":"282.00"},{"id":2,"color":"cyan","department":"Clothing","name":"Atlas Shirt","price":"127.00"},{"id":3,"color":"teal","department":"Clothing","name":"Small Metal Shoes","price":"232.00"},{"id":4,"color":"red","department":"Watches","name":"Red Dragon Watch","price":"232.00"}](
정상적으로 호출이 되는 것을 확인 할 수 있습니다.
HTTP redirect to HTTPS 실습
모든 트래픽을 항상 TLS를 쓰도록 강제하고 HTTP 트래픽을 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"
🛠️ 수정된 Gateway 설정 적용 후 HTTP 호출 테스트
$ kubectl apply -f ch4/coolstore-gw-tls-redirect.yaml
$ curl -v http://webapp.istioinaction.io:30000/api/catalog
결과
* Host webapp.istioinaction.io:30000 was resolved.
* IPv6: (none)
* 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.5.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< location: https://webapp.istioinaction.io:30000/api/catalog
< date: Thu, 17 Apr 2025 08:45:56 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host webapp.istioinaction.io left intact
301 에러가 나오고 해당 에러는 클라이언트에게 HTTPS 버전으로 호출을 요청하는 내용 입니다.
HTTP traffic with mutual TLS 실습

mTLS는 클라이언트와 서버가 서로 믿을 수 있는지 확인하는 보안 통신 방식 입니다.
🔒 TLS vs mTLS 차이
항목 | TLS | mTLS (mutual TLS) |
인증 방향 | 서버 → 클라이언트 | 서버 ↔ 클라이언트 (양방향) |
사용 예시 | HTTPS 웹사이트 접속 | 서비스 간 보안 통신, API 인증 등 |
목적 | 서버 신뢰성 검증 | 서버와 클라이언트 상호 인증 + 암호화 |
Istio-ingressgateway가 mTLS 커넥션에 참여하도록 구성하려면, 클라이언트의 인증서를 검증하는 데 사용할 수 있도록 CA 인증서 집합을 제공해야합니다.
✅ 실습 목표
클라이언트가 Istio Gateway에 접근할 때 서버 인증서만 확인하는 TLS가 아니라, 클라이언트도 인증서를 제시하도록(mTLS) 구성하여 서버가 클라이언트의 신원을 검증할 수 있도록 설정하는 것입니다.
tls.key = 서버 비공개 키
tls.crt = 서버 인증서
ca.crt = 신뢰할 CA 인증서 체인
🛠️ 적절한 CA 인증서 체인으로 istio-ingressgateway-ca-cert 시크릿을 구성
# 인증서 파일들 확인
$ 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 describe secrets -n istio-system webapp-credential-mtls
결과
Name: webapp-credential-mtls
Namespace: istio-system
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
ca.crt: 4025 bytes
tls.crt: 1923 bytes
tls.key: 1675 bytes
🛠️ CA 인증서 체인의 위치를 가리키도록 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
🛠️ Secret 리소스가 Envoy 프록시에 적용되었는지 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config secret deploy/istio-ingressgateway.istio-system
결과
RESOURCE NAME TYPE STATUS VALID CERT SERIAL NUMBER NOT AFTER NOT BEFORE
default Cert Chain ACTIVE true 330104555039918527066958644715789120925 2025-04-19T00:48:35Z 2025-04-18T00:46:35Z
kubernetes://webapp-credential Cert Chain ACTIVE true 1049106 2041-06-29T12:49:32Z 2021-07-04T12:49:32Z
kubernetes://webapp-credential-mtls Cert Chain ACTIVE true 1049106 2041-06-29T12:49:32Z 2021-07-04T12:49:32Z
ROOTCA CA ACTIVE true 31128635175786742820350531621523917090 2035-04-15T02:23:03Z 2025-04-17T02:23:03Z
kubernetes://webapp-credential-mtls-cacert CA ACTIVE true 1049106 2041-06-29T12:49:29Z 2021-07-04T12:49:29Z
🛠️ 호출 테스트 1 : (호출실패) 클라이언트 인증서 없음 - SSL 핸드섀이크가 성공하지 못하여 거부됨
$ curl -v https://webapp.istioinaction.io:30005/api/catalog \
--cacert ch4/certs/2_intermediate/certs/ca-chain.cert.pem
결과
>
* TLSv1.3 (IN), TLS alert, unknown (628):
* OpenSSL SSL_read: OpenSSL/3.0.13: error:0A00045C:SSL routines::tlsv13 alert certificate required, errno 0
* Failed receiving HTTP2 data: 56(Failure when receiving data from the peer)
* Connection #0 to host webapp.istioinaction.io left intact
curl: (56) OpenSSL SSL_read: OpenSSL/3.0.13: error:0A00045C:SSL routines::tlsv13 alert certificate required, errno 0
🛠️ 호출 테스트 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
결과
* Connection #0 to host webapp.istioinaction.io left intact
[{"id":1,"color":"amber","department":"Eyewear","name":"Elinor Glasses","price":"282.00"},{"id":2,"color":"cyan","department":"Clothing","name":"Atlas Shirt","price":"127.00"},{"id":3,"color":"teal","department":"Clothing","name":"Small Metal Shoes","price":"232.00"},{"id":4,"color":"red","department":"Watches","name":"Red Dragon Watch","price":"232.00"}](
Serving multiple virtual hosts with TLS 실습
- Istio의 ingress-gateway는 동일한 HTTPS 포트(443)에서 자체 인증서와 비밀 키가 있는 여러 가상호스트를 서빙할 수 있습니다.
- 이를 위해 동일 포트 및 프로토콜에 여러 항목을 추가합니다.
- 예를 들어 자체 인증서와 키 쌍이 있는 webapp.istioinaction.io 와 catalog.istioinaction.io 서비스를 둘 다 추가합니다.
- HTTPS로 서빙하는 가상 호스트를 여럿 기술하고 있는 Istio Gateway 리소스는 다음과 같습니다.
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" # webapp 도메인
- port:
number: 443
name: https-catalog
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: catalog-credential
hosts:
- "catalog.istioinaction.io" # 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
🛠️ Gateway로 노출한 catalog 서비스용 VS(VirtualService) 생성
$ kubectl apply -f ch4/catalog-vs.yaml -n istioinaction
🛠️ 도메인 질의를 위한 임시 도메인 설정
$ echo "127.0.0.1 catalog.istioinaction.io" | sudo tee -a /etc/hosts
🛠️ 호출테스트 1 - webapp.istioinaction.io
$ curl -v https://webapp.istioinaction.io:30005/api/catalog \
--cacert ch4/certs/2_intermediate/certs/ca-chain.cert.pem
결과
* Connection #0 to host webapp.istioinaction.io left intact
[{"id":1,"color":"amber","department":"Eyewear","name":"Elinor Glasses","price":"282.00"},{"id":2,"color":"cyan","department":"Clothing","name":"Atlas Shirt","price":"127.00"},{"id":3,"color":"teal","department":"Clothing","name":"Small Metal Shoes","price":"232.00"},{"id":4,"color":"red","department":"Watches","name":"Red Dragon Watch","price":"232.00"}](
🛠️ 호출테스트 2 - catalog.istioinaction.io (cacert 경로가 ch4/certs2/* 임에 유의)
$ curl -v https://catalog.istioinaction.io:30005/items \
--cacert ch4/certs2/2_intermediate/certs/ca-chain.cert.pem
결과
[
{
"id": 1,
"color": "amber",
"department": "Eyewear",
"name": "Elinor Glasses",
"price": "282.00"
},
{
"id": 2,
"color": "cyan",
"department": "Clothing",
"name": "Atlas Shirt",
"price": "127.00"
},
{
"id": 3,
"color": "teal",
"department": "Clothing",
"name": "Small Metal Shoes",
"price": "232.00"
},
{
"id": 4,
"color": "red",
"department": "Watches",
"name": "Red Dragon Watch",
"price": "232.00"
}
* Connection #0 to host catalog.istioinaction.io left intact
Exposing TCP ports on an istio gateway 실습
✅ 실습 목적
TCP 기반 에코 서버를 Kubernetes에 배포하고, Istio의 L4(TCP) 트래픽 라우팅 테스트를 위한 서비스를 구성 해보도록 하겠습니다.
🛠️ 적용할 Deploy, Service 구성 확인
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
🛠️ Deploy, Service 적용
$ kubectl apply -f ch4/echo.yaml -n istioinaction
🛠️ istio-ingressgateway 서비스 내용 추가
$ 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")]}'
결과
{"name":"tcp","nodePort":30006,"port":31400,"protocol":"TCP","targetPort":31400}
🛠️ 게이트웨이 내용 확인 및 적용
$ 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
🛠️ 에코 서비스로 라우팅 하기 위한 VS 내용 확인 및 적용
$ 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
NAME GATEWAYS HOSTS AGE
catalog-vs-from-gw ["coolstore-gateway"] ["catalog.istioinaction.io"] 44m
tcp-echo-vs-from-gw ["echo-tcp-gateway"] ["*"] 6s
webapp-vs-from-gw ["coolstore-gateway"] ["webapp.istioinaction.io"] 3h59m
🛠️ telnet을 사용하여 TCP 포트로 라우팅 되는지 확인
$ apt install -y inetutils-telnet
$ telnet localhost 30006
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Welcome, you are connected to node myk8s-control-plane.
Running on Pod tcp-echo-deployment-584f6d6d6b-d5nxb.
In namespace istioinaction.
With IP address 10.10.0.14.
Service default.
hello istio!
hello istio!
Traffic routing with SNI passthrough 실습
✅ 실습 목표
이번엔 istio-ingressgateway에서 트래픽을 종료하지 않고 SNI 호스트네임에 따라 TCP 트래픽을 라우팅 하는 것을 해보겠습니다.
🛠️ Gateway 설정 확인
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: sni-passthrough-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 31400 #1 HTTP 포트가 아닌 특정 포트 열기
name: tcp-sni
protocol: TLS
hosts:
- "simple-sni-1.istioinaction.io" #2 이 호스트를 포트와 연결
tls:
mode: PASSTHROUGH #3 통과 트래픽으로 처리
🛠️ 애플리케이션에서 TLS 적용하기 위해 앞서 설정해둔 TLS 설정 종료
$ kubectl delete gateway echo-tcp-gateway -n istioinaction
🛠️ 신규 앱, 게이트웨이 배포
$ kubectl apply -f ch4/sni/simple-tls-service-1.yaml -n istioinaction
$ kubectl apply -f ch4/sni/passthrough-sni-gateway.yaml -n istioinaction
🛠️ VS 에서 라우팅 규칙 설정
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: simple-sni-1-vs
spec:
hosts:
- "simple-sni-1.istioinaction.io"
gateways:
- sni-passthrough-gateway
tls:
- match: #1 특정 포트와 호스트의 비교 부분
- port: 31400
sniHosts:
- simple-sni-1.istioinaction.io
route:
- destination: #2 트래픽이 일치하는 경우 라우팅 목적지
host: simple-tls-service-1
port:
number: 80 #3 서비스 포트로 라우팅
🛠️ VS 적용
$ kubectl apply -f ch4/sni/passthrough-sni-vs-1.yaml -n istioinaction
🛠️ 호출 테스트에 앞서 도메인 등록
$ echo "127.0.0.1 simple-sni-1.istioinaction.io" | sudo tee -a /etc/hosts
🛠️ 호출 테스트
$ curl https://simple-sni-1.istioinaction.io:30006/ \
--cacert ch4/sni/simple-sni-1/2_intermediate/certs/ca-chain.cert.pem
결과
{
"name": "simple-tls-service-1",
"uri": "/",
"type": "HTTP",
"ip_addresses": [
"10.10.0.15"
],
"start_time": "2025-04-18T05:03:30.931118",
"end_time": "2025-04-18T05:03:30.931301",
"duration": "183.08µs",
"body": "Hello from simple-tls-service-1!!!",
"code": 200
}
좀 더 명확하게 알아보기 위해 인증서가 다르고 SNI 호스트에 기반해 라우팅을 하는 두 번째 서비스를 배포 해보겠습니다.
# 두 번째 서비스 배포
cat ch4/sni/simple-tls-service-2.yaml
kubectl apply -f ch4/sni/simple-tls-service-2.yaml -n istioinaction
# gateway 설정 업데이트
cat ch4/sni/passthrough-sni-gateway-both.yaml
kubectl apply -f ch4/sni/passthrough-sni-gateway-both.yaml -n istioinaction
# VirtualService 설정
cat ch4/sni/passthrough-sni-vs-2.yaml
kubectl apply -f ch4/sni/passthrough-sni-vs-2.yaml -n istioinaction
# 호출테스트2
echo "127.0.0.1 simple-sni-2.istioinaction.io" | sudo tee -a /etc/hosts
curl https://simple-sni-2.istioinaction.io:30006 \
--cacert ch4/sni/simple-sni-2/2_intermediate/certs/ca-chain.cert.pem
결과
{
"name": "simple-tls-service-2",
"uri": "/",
"type": "HTTP",
"ip_addresses": [
"10.10.0.16"
],
"start_time": "2025-04-18T05:05:12.594585",
"end_time": "2025-04-18T05:05:12.594858",
"duration": "273.559µs",
"body": "Hello from simple-tls-service-2!!!",
"code": 200
}
2주차 내용은 여기까지 입니다.
감사합니다.
'스터디 > Istio' 카테고리의 다른 글
Istio 스터디 3주차 - <Resilience> (0) | 2025.04.25 |
---|---|
Istio 스터디 3주차 - <Traffic Control> (0) | 2025.04.23 |
Istio 스터디 2주차 - <Envoy 란?> (0) | 2025.04.16 |
Istio 스터디 1주차 - <Istio 실습 해보기> (1) | 2025.04.09 |
Istio 스터디 1주차 - <서비스 메시와 Istio> (0) | 2025.04.07 |