스터디/Istio

Istio 스터디 2주차 - <Envoy와 Istio Gateway 실습>

황동리 2025. 4. 18. 15:27
반응형

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가지 입니다.

  1. 엔드포인트에 따라 호출할 때 사용한 헤더를 반환 결과 확인
  2. HTTP 요청 지연
  3. 오류 발생 후 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주차 내용은 여기까지 입니다.

 

감사합니다.

반응형