Cloud Tech

EKS VPC CNI 네트워크 최적화 설정과 Kubeflow에서의 istio 구성

Hanhorang31 2024. 10. 20. 02:12
 

Overview

EKS 내 VPC CNI 네트워킹 모범사례를 실습하고, Kubeflow에서의 Istio 구성을 확인하겠습니다.

 

EKS 네트워킹 모범사례 정리

M/L 및 데이터 워크로드 기반의 EKS 프로젝트(DoEKS) 에서 AWS VPC CNI고려사항을 정리하고 실습하겠습니다.

 

Networking for Data | Data on EKS

VPC and IP Considerations

awslabs.github.io

 

 

VPC와 IP 고려 사항

1. EKS 클러스터에서의 Warm Pool 유지

AWS 각 인스턴스 유형은 최대 네트워크 인터페이스 수, 네트워크 인터페이스 당 최대 개인 IP를 지원합니다.

aws ec2 describe-instance-types \
    --filters "Name=instance-type,Values=c5.*" \
    --query "InstanceTypes[].{ \
        Type: InstanceType, \
        MaxENI: NetworkInfo.MaximumNetworkInterfaces, \
        IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
    --output table
   
---------------------------------------
|        DescribeInstanceTypes        |
+----------+----------+---------------+
| IPv4addr | MaxENI   |     Type      |
+----------+----------+---------------+
|  30      |  8       |  c5.4xlarge   |
|  30      |  8       |  c5.12xlarge  |
|  15      |  4       |  c5.xlarge    |
|  30      |  8       |  c5.9xlarge   |
|  50      |  15      |  c5.metal     |
|  50      |  15      |  c5.24xlarge  |
|  15      |  4       |  c5.2xlarge   |
|  10      |  3       |  c5.large     |
|  50      |  15      |  c5.18xlarge  |
+----------+----------+---------------+

EKS 파드가 생성됨에 따라 Kubelet 이 Pod 추가 요청을 받으면 CNI 바이너리는 ipamd에 사용가능한 IP 주소를 쿼리하고 제공하며, CNI 바이너리는 호스트와 Pod 네트워크에 연결합니다.

IP 주소 풀이 고갈되면 플러그인은 자동으로 ENI를 추가로 연결하고 추가 ENI의 보조 IP 주소 세트를 할당합니다.

다만, 파드의 높은 이탈률 또는 대규모 확장 기간 동안 이러한 EC2 API 호출은 속도가 제한될 수 있으며, 이로 인해 Pod 프로비저닝이 지연되고 워크로드 실행이 지연될 수 있기에 파드 오토스케일링 과정이 많은 경우 사전에 웜 풀을 설정하여 네트워크 연결 지연을 줄이고, 안전성이 증가시킬 수 있습니다.

EC2 API 실패 제한으로 파드에 IP가 붙지 못하는 경우 아래 로그가 발생합니다. Failed to create pod sandbox: rpc error: code = Unknown desc = failed to set up sandbox container "xxxxxxxxxxxxxxxxxxxxxx" network for pod "test-pod": networkPlugin cni failed to set up pod test-pod_default" network: add cmd: failed to assign an IP address to container
tree /var/log/aws-routed-eni
---
├── ebpf-sdk.log # ebpf SDK 로그
├── egress-v6-plugin.log # 이그래스 트래픽 관련 로그 파일 
├── ipamd.log # eni IP 주소 할당, 해제, 그리고 오류 등과 관련된 이벤트와 상태 변경 로그 파일 
├── network-policy-agent.log # 네트워크 폴리스 정책 로그
└── plugin.log # 파드의 네트워크 인터페이스 설정, 트래픽 관리, 오류 처리 등과 관련된 이벤트와 상태 변경 로그 파일**


# eni IP 주소 할당, 해제, 그리고 오류 등과 관련된 이벤트와 상태 변경 로그 파일
cat /var/log/aws-routed-eni/ipamd.log 

# 파드의 네트워크 인터페이스 설정, 트래픽 관리, 오류 처리 등과 관련된 이벤트와 상태 변경 로그 파일
cat /var/log/aws-routed-eni/plugin.log 

EKS VPC CNI는 데몬셋으로 파드가 배포되며 WARM_ENI_TARGET옵션을 통해 웜풀 수를 설정할 수 있습니다.

kubectl edit daemonset aws-node -n kube-system

값 수정시 aws-node 파드가 재시작되며 AWS 콘솔에서 네트워크 인터페이스가 추가된 것을 확인할 수 있습니다.

 

 

2. 서브넷이 제한되어 IP 공간이 제한되어 있는 경우 Secondary CIDR를 사용하자

위 서브넷에 IP가 부족한 경우 새로운 서브넷을 추가하여 파드를 배포시킬 수 있습니다. 가이드의 경우경우 VPC에 100.64.0.0/16, 100.65.0.0/16, 를 추가한 100.66.0.0/16다음(최대 CIDR 크기임) 해당 CIDR로 새 서브넷을 만들었습니다.

구성 설정은 새로운 서브넷을 생성하고 라우터 테이블을 연결시킨 다음 VPC CNI 옵션을 설정하고, ENI를 배포합니다.

# 기존 서브넷 라우팅 테이블 확인 
SNET1=$(aws ec2 describe-subnets --filters Name=cidr-block,Values=192.168.0.0/19 --query 'Subnets[].SubnetId' --output text)
RTASSOC_ID=$(aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=$SNET1 --query 'RouteTables[].RouteTableId' --output text)

# 위 라우팅 테이블에 새 서브넷을 연결
aws ec2 associate-route-table --route-table-id $RTASSOC_ID --subnet-id $CGNAT_SNET1
aws ec2 associate-route-table --route-table-id $RTASSOC_ID --subnet-id $CGNAT_SNET2
aws ec2 associate-route-table --route-table-id $RTASSOC_ID --subnet-id $CGNAT_SNET3

https://archive.eksworkshop.com/beginner/160_advanced-networking/secondary_cidr/eniconfig_crd/

 

이제 VPC CNI 옵션을 설정합시다.

kubectl set env ds aws-node -n kube-system AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true
  • 수정시 파드가 새로 배포됩니다.

이제 Secondary CIDR ENI Config 설정을 배포합니다.

cat <<EOF >pod-netconfig.template
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
 name: \${AZ}
spec:
 subnet: \${SUBNET_ID}
 securityGroups: [ \${NETCONFIG_SECURITY_GROUPS} ]
EOF

kubectl get eniconfig 

이제 기존 노드에 annotation을 설정하거나 새로운 노드를 배포하면 Secondary CIDR에 파드가 배포됨을 확인할 수 있습니다.

kubectl annotate node <nodename>.<region>.compute.internal k8s.amazonaws.com/eniConfig=<ENIConfig-name-for-az>

콘솔과의 배포 동작은 필자의 이전 블로그 글(CUSTOM Network를 통한 IP 확장하기) 을 확인해주세요.

https://hanhorang31.github.io/post/aews-eks-vpc-1/

 

[AEWS] EKS VPC CNI Deep Dive | HanHoRang Tech Blog

EKS VPC 와 EKS Workshop Netwroking 실습

hanhorang31.github.io

 

 

애플리케이션 고려 사항

DNS 최적화

EKS 에서는 클러스터 내부 DNS 파드인 CoreDNS 파드가 기본적으로 배포됩니다.

각 파드의 DNS정보를 확인하면 다음과 같이 CoreDNS에 IP를 확인합니다.

cat /etc/resolv.conf
nameserver 10.100.0.10
search namespace.svc.cluster.local svc.cluster.local cluster.local ec2.internal
options ndots:5

search에 나열된 도메인 목록을 차례대로 붙여가며 추가로 DNS 서버 (정확히는 CoreDNS)를 호출한다.

다만, 불필요한 DNS 요청이 많아지면 coredns 과부하가 발생하여 성능 지연이 있을 수 있습니다.

여기서 애플리케이션이 클러스터의 다른 포드와 통신하지 않는 경우 "2"와 같은 낮은 값으로 설정하여 최적화를 시켜줄 수 있습니다.

apiVersion: v1
kind: Pod
metadata:
  namespace: default
  name: dns-example
spec:
  containers:
    - name: test
      image: nginx
  dnsConfig:
    options:
      - name: ndots
        value: "2"

 

InterAZ 네트워크

파드가 여러 가용 영역(AZ)에 분산되어 있는 경우, 파드간 통신시 네트워크 I/O 전면에서 비용이 추가로 많이 발생할 수 있습니다. 예를 들어 Spark, 머신 러닝으로 파드 간 통신이 많아지는 경우 같은 AZ로 설정하여 비용과 파드간 대기시간을 감소 시킬 수 있습니다.

 

동일한 AZ에 워크로드를 공동 배치하는 경우 다음의 장점이 있습니다.

  • AZ 간 트래픽 비용 절감
  • 실행자/파드간 네트워크 대기 시간 감소
spec:
  executor:
    affinity:
      podAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
              matchExpressions:
              - key: sparkoperator.k8s.io/app-name
                operator: In
                values:
                - <<spark-app-name>>
          topologyKey: topology.kubernetes.io/zone
          ...
  • labelSelector: Spark 애플리케이션의 이름(sparkoperator.k8s.io/app-name: spark-example)에 따라 Pod를 매칭하여 배치하도록 설정합니다.
  • topologyKey: topology.kubernetes.io/zone: AZ를 기반으로 동일한 Zone에서 Pod가 스케줄링되도록 설정합니다.

위 장점이 있지만 AZ가 단일 지점으로 변하게 되어 가용성 측면에서 고려가 필요합니다.

일부 발표에서는 해당 옵션을 소개하지만 해당 옵션을 적용하지 않은 곳이 있었습니다.

 

 

Kubeflow로 확인하는 네트워크 구성 확인

Kubeflow는 엔드투엔드 ML 워크플로를 활성화하기 위해 단일 Kubernetes 클러스터에 함께 배포되는 도구, 프레임워크 및 서비스 모음입니다. kubeflow 의 아키텍처와 구성은 필자의 이전 블로그 글을 확인해주세요.

https://hanhorang31.github.io/post/kubeflow-on-aws/

 

[AEWS] Kubeflow on AWS | HanHoRang Tech Blog

EKS에서 Kubeflow addon 사용하기

hanhorang31.github.io

 

특히 Kubeflow는 엔드투엔드 인증 및 액세스 제어를 위해 Istio를 함께 배포하여 사용합니다.

  • 강력한 ID 기반 인증 및 승인을 통해 Kubeflow 배포에서 파드 서비스 간 통신을 보호합니다.
  • OAUTH2 제어를 통한 인증과 istio gateway 를 통한 액세스 제어
  • 클러스터 수신 및 송신을 포함하여 배포 내 트래픽에 대한 자동 측정항목, 로그 및 추적합니다.

 

Kubeflow 파드 통신 과정 이해

아래 다이어그램은 사용자 요청이 kubelfow의 서비스와 상호작용하는 방식을 보여줍니다.

사용자가 Central 대시보드를 통해 액세스할 수 있는 노트북 UI를 통해 새 노트북 서버 생성을 요청하는 프로세스를 안내합니다.

  1. 사용자 인증 요청: 사용자가 웹 서비스에 접근하려고 할 때, 그 요청은 먼저 '인증 프록시'를 통과합니다. 이 프록시는 클라우드 서비스의 IAM(Identity and Access Management) 같은 SSO(Single Sign-On) 서비스 제공자나, 온프레미스 환경에서는 Active Directory/LDAP 같은 서비스와 통신하여 사용자를 인증합니다.
  2. JWT 헤더 토큰 추가: 사용자가 성공적으로 인증되면, Istio Gateway가 요청을 수정하여 사용자의 신원을 담고 있는 JWT(Json Web Token) 헤더 토큰을 추가합니다. 이 토큰은 서비스 메시 내의 모든 요청에 포함되어 사용자의 신원을 전달합니다.
  3. RBAC 정책 적용: 요청이 들어오면, Istio의 RBAC(Role-Based Access Control) 정책이 적용되어 사용자가 서비스와 요청한 네임스페이스에 접근할 수 있는지 검증합니다. 사용자가 접근 권한이 없으면, 오류 응답이 반환됩니다.
  4. 컨트롤러로의 요청 전달: 접근 권한이 확인되면, 요청은 적절한 컨트롤러(여기서는 노트북 컨트롤러)로 전달됩니다.
  5. 노트북 컨트롤러의 권한 검증 및 노트북 생성: 노트북 컨트롤러는 Kubernetes의 RBAC을 사용하여 권한을 검증하고, 사용자가 요청한 네임스페이스 내에 노트북 파드를 생성합니다.

 

Terraform 을 통한 Kubeflow 배포

kubeflow 배포는 테라폼으로 구성된 깃 프로젝트를 참고하여 배포하였습니다.

위 프로젝트는 테라폼을 통해 EKS 인프라를 구성하고 kubeflow를 배포해줍니다.

kubeflow가 설치되는 EKS 버전은 1.28 입니다. 또한, kubeflow 의 데이터 저장소를 AWS EFS, FsX에 설정할 수 있고, 노드 스케일링을 Karpenter 로 구성할 수 있습니다.

테라폼 백엔드 설정으로 s3 객체를 생성하고, 리전과 카펜터 노드 범위 을 설정해주세요.

# git clone 
git clone https://github.com/aws-samples/amazon-eks-machine-learning-with-terraform-and-kubeflow.git

# amazon-eks-machine-learning-with-terraform-and-kubeflow/eks-cluster/terraform/aws-eks-cluster-and-nodegroup/main.tf 

# main.tf 
# line 7 
provider "aws" {
  region                   = "ap-northeast-2"
  alias = "seoul"
  shared_credentials_files = [var.credentials]
  profile                  = var.profile
}

# variables.tf
# line 174
variable "system_instances" {
  description = "List of instance types for system nodes."
  type = list(string)
  default = [
    "t3a.large",
    "t3a.xlarge",
    "t3a.2xlarge",
    "m5.large", 
    "m5.xlarge", 
    "m5.2xlarge", 
    "m5.4xlarge", 
    "m5a.large", 
    "m5a.xlarge", 
    "m5a.2xlarge", 
    "m5a.4xlarge"
  ]
}

이어서 테라폼을 통해 EKS와 kubeflow를 프로비저닝합니다.

# S3 버킷을 통한 테라폼 백엔드설정 
cd ~/amazon-eks-machine-learning-with-terraform-and-kubeflow
./eks-cluster/utils/s3-backend.sh S3_BUCKET
#  ./eks-cluster/utils/s3-backend.sh horangflow

# Terraform 배포 
terraform init 
cd ~/amazon-eks-machine-learning-with-terraform-and-kubeflow/eks-cluster/terraform/aws-eks-cluster-and-nodegroup
terraform apply -var="profile=default" -var="region=ap-northeast-2" -var="cluster_name=ml-eks-cluster" -var='azs=["ap-northeast-2a", "ap-northeast-2c"]' -var="import_path=s3://S3_BUCKET/ml-platform"
# terraform apply -var="profile=default" -var="region=ap-northeast-2" -var="cluster_name=horang" -var='azs=["ap-northeast-2a", "ap-northeast-2c"]' -var="import_path=s3://horangflow/ml-platform" -auto-approve

# 설치 파드 확인 
kubectl get pods -A   
  • 카펜터는 ECR 이미지 버전 이슈로 배포되지 않았습니다. 필요한 경우 이미지 태그를 확인하여 별도 배포해주세요.

 

 

kubeflow 대시보드 접근

istio-ingressgateway 을 통해서 kubeflow dashboard 에 접근하기 위해서는 포트포워딩에 대시보드 FQDN을 입력해야 합니다.

# kubeflow 대시보드 접근
sudo kubectl port-forward svc/istio-ingressgateway -n ingress 443:443 

# DNS 접근 도메인 등록 
sudo vi /etc/hosts
# 아래 도메인 수정 
127.0.0.1 	istio-ingressgateway.ingress.svc.cluster.local

# 아래 도메인 접근
https://istio-ingressgateway.ingress.svc.cluster.local 

# 패스워드 확인 
terraform output static_password  
  • Email 은 user@example.com 로 설정하고, 패스워드는 terraform output static_password 를 통해 확인할 수 있습니다.

 

Istio 동작 확인

배포한 kubeflow에서 동작하는 istio 동작을 확인하겠습니다.

# 네임스페이스 라벨 확인, 사이드카 주입
kubectl get namespace kubeflow --show-labels

#istio 파드 확인
kubectl describe pods notebook-controller-deployment-8555f678d4-rsrv2  -n kubeflow

kubectl exec -it  notebook-controller-deployment-8555f678d4-rsrv2  -n kubeflow -c istio-proxy -- sh

# 라우터 확인
netstat -anr
# 포트 확인 
ss -ltp
  • envoy Proxy가 포트 15001~15020포트를 사용하고 있는 것을 확인할 수 있고, 포트에 따라 트래픽이 라우팅된 것을 확인할 수 있습니다.

Enovy 프록시 설정은 istio-proxy 파드 내에서 확인할 수 있습니다.

아래 명령어를 통해 엔드포인트, 라우팅을 비롯한 설정을 확인할 수 있습니다.

kubectl exec -it  notebook-controller-deployment-8555f678d4-rsrv2  -n kubeflow -c istio-proxy -- sh

cat /etc/istio/proxy/envoy-rev.json

 

더보기
prxoy 설정

$ cat /etc/istio/proxy/envoy-rev.json
{
  "node": {
    "id": "sidecar~192.168.110.237~notebook-controller-deployment-8555f678d4-rsrv2.kubeflow~kubeflow.svc.cluster.local",
    "cluster": "notebook-controller-deployment.kubeflow",
    "locality": {
      "region": "ap-northeast-2"
      ,
      "zone": "ap-northeast-2a"
    },
    "metadata": {"ANNOTATIONS":{"istio.io/rev":"default","kubectl.kubernetes.io/default-container":"manager","kubectl.kubernetes.io/default-logs-container":"manager","kubernetes.io/config.seen":"2024-10-19T08:31:11.792487894Z","kubernetes.io/config.source":"api","prometheus.io/path":"/stats/prometheus","prometheus.io/port":"15020","prometheus.io/scrape":"true","sidecar.istio.io/interceptionMode":"REDIRECT","sidecar.istio.io/status":"{\"initContainers\":[\"istio-validation\"],\"containers\":[\"istio-proxy\"],\"volumes\":[\"workload-socket\",\"credential-socket\",\"workload-certs\",\"istio-envoy\",\"istio-data\",\"istio-podinfo\",\"istio-token\",\"istiod-ca-cert\"],\"imagePullSecrets\":null,\"revision\":\"default\"}","traffic.sidecar.istio.io/excludeInboundPorts":"15020","traffic.sidecar.istio.io/includeInboundPorts":"*","traffic.sidecar.istio.io/includeOutboundIPRanges":"*"},"APP_CONTAINERS":"manager","CLUSTER_ID":"Kubernetes","ENVOY_PROMETHEUS_PORT":15090,"ENVOY_STATUS_PORT":15021,"INSTANCE_IPS":"192.168.110.237","INTERCEPTION_MODE":"REDIRECT","ISTIO_PROXY_SHA":"7b292c7175692c822148b64005a731eb00365508","ISTIO_VERSION":"1.20.2","LABELS":{"control-plane":"notebook-controller","security.istio.io/tlsMode":"istio","service.istio.io/canonical-name":"notebook-controller-deployment","service.istio.io/canonical-revision":"latest"},"MESH_ID":"cluster.local","NAME":"notebook-controller-deployment-8555f678d4-rsrv2","NAMESPACE":"kubeflow","NODE_NAME":"ip-192-168-124-178.ap-northeast-2.compute.internal","OWNER":"kubernetes://apis/apps/v1/namespaces/kubeflow/deployments/notebook-controller-deployment","PILOT_SAN":["istiod.istio-system.svc"],"PLATFORM_METADATA":{"aws_availability_zone":"ap-northeast-2a","aws_instance_id":"i-0fb65dc15525d07b1","aws_region":"ap-northeast-2"},"POD_PORTS":"[\n]","PROXY_CONFIG":{"binaryPath":"/usr/local/bin/envoy","concurrency":2,"configPath":"./etc/istio/proxy","controlPlaneAuthPolicy":"MUTUAL_TLS","discoveryAddress":"istiod.istio-system.svc:15012","drainDuration":"45s","proxyAdminPort":15000,"serviceCluster":"istio-proxy","statNameLength":189,"statusPort":15020,"terminationDrainDuration":"5s","tracing":{"zipkin":{"address":"zipkin.istio-system:9411"}}},"SERVICE_ACCOUNT":"notebook-controller-service-account","WORKLOAD_NAME":"notebook-controller-deployment"}
  },
  "layered_runtime": {
      "layers": [
          {
            "name": "global config",
            "static_layer": {"envoy.deprecated_features:envoy.config.listener.v3.Listener.hidden_envoy_deprecated_use_original_dst":true,"envoy.reloadable_features.http_reject_path_with_fragment":false,"overload.global_downstream_max_connections":"2147483647","re2.max_program_size.error_level":"32768"}
          },
          {
              "name": "admin",
              "admin_layer": {}
          }
      ]
  },
  "bootstrap_extensions": [
    {
      "name": "envoy.bootstrap.internal_listener",
      "typed_config": {
        "@type":"type.googleapis.com/udpa.type.v1.TypedStruct",
        "type_url": "type.googleapis.com/envoy.extensions.bootstrap.internal_listener.v3.InternalListener",
        "value": {
          "buffer_size_kb": 64
        }
      }
    }
  ],
  "stats_config": {
    "use_all_default_tags": false,
    "stats_tags": [
      {
        "tag_name": "cluster_name",
        "regex": "^cluster\\.((.+?(\\..+?\\.svc\\.cluster\\.local)?)\\.)"
      },
      {
        "tag_name": "tcp_prefix",
        "regex": "^tcp\\.((.*?)\\.)\\w+?$"
      },
      {
        "regex": "_rq(_(\\d{3}))$",
        "tag_name": "response_code"
      },
      {
        "tag_name": "response_code_class",
        "regex": "_rq(_(\\dxx))$"
      },
      {
        "tag_name": "http_conn_manager_listener_prefix",
        "regex": "^listener(?=\\.).*?\\.http\\.(((?:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      },
      {
        "tag_name": "http_conn_manager_prefix",
        "regex": "^http\\.(((?:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      },
      {
        "tag_name": "listener_address",
        "regex": "^listener\\.(((?:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
      },
      {
        "tag_name": "mongo_prefix",
        "regex": "^mongo\\.(.+?)\\.(collection|cmd|cx_|op_|delays_|decoding_)(.*?)$"
      },
      {
        "regex": "(cache\\.(.+?)\\.)",
        "tag_name": "cache"
      },
      {
        "regex": "(component\\.(.+?)\\.)",
        "tag_name": "component"
      },
      {
        "regex": "(tag\\.(.+?);\\.)",
        "tag_name": "tag"
      },
      {
        "regex": "(wasm_filter\\.(.+?)\\.)",
        "tag_name": "wasm_filter"
      },
      {
        "tag_name": "authz_enforce_result",
        "regex": "rbac(\\.(allowed|denied))"
      },
      {
        "tag_name": "authz_dry_run_action",
        "regex": "(\\.istio_dry_run_(allow|deny)_)"
      },
      {
        "tag_name": "authz_dry_run_result",
        "regex": "(\\.shadow_(allowed|denied))"
      }
    ],
    "stats_matcher": {
      "inclusion_list": {
        "patterns": [
          {
          "prefix": "reporter="
          },
          {
          "prefix": "cluster_manager"
          },
          {
          "prefix": "listener_manager"
          },
          {
          "prefix": "server"
          },
          {
          "prefix": "cluster.xds-grpc"
          },
          {
          "prefix": "wasm"
          },
          {
          "suffix": "rbac.allowed"
          },
          {
          "suffix": "rbac.denied"
          },
          {
          "suffix": "shadow_allowed"
          },
          {
          "suffix": "shadow_denied"
          },
          {
          "safe_regex": {"regex":"vhost\\.*\\.route\\.*"}
          },
          {
          "prefix": "component"
          },
          {
          "prefix": "istio"
          }
        ]
      }
    }
  },
  "admin": {
    "access_log": [
      {
        "name": "envoy.access_loggers.file",
        "typed_config": {
          "@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog",
          "path": "/dev/null"
        }
      }
    ],
    "profile_path": "/var/lib/istio/data/envoy.prof",
    "address": {
      "socket_address": {
        "address": "127.0.0.1",
        "port_value": 15000
      }
    }
  },
  "dynamic_resources": {
    "lds_config": {
      "ads": {},
      "initial_fetch_timeout": "0s",
      "resource_api_version": "V3"
    },
    "cds_config": {
      "ads": {},
      "initial_fetch_timeout": "0s",
      "resource_api_version": "V3"
    },
    "ads_config": {
      "api_type": "GRPC",
      "set_node_on_first_message_only": true,
      "transport_api_version": "V3",
      "grpc_services": [
        {
          "envoy_grpc": {
            "cluster_name": "xds-grpc"
          }
        }
      ]
    }
  },
  "static_resources": {
    "clusters": [
      {
        "name": "prometheus_stats",
        "type": "STATIC",
        "connect_timeout": "0.250s",
        "lb_policy": "ROUND_ROBIN",
        "load_assignment": {
          "cluster_name": "prometheus_stats",
          "endpoints": [{
            "lb_endpoints": [{
              "endpoint": {
                "address":{
                  "socket_address": {
                    "protocol": "TCP",
                    "address": "127.0.0.1",
                    "port_value": 15000
                  }
                }
              }
            }]
          }]
        }
      },
      {
        "name": "agent",
        "type": "STATIC",
        "connect_timeout": "0.250s",
        "lb_policy": "ROUND_ROBIN",
        "load_assignment": {
          "cluster_name": "agent",
          "endpoints": [{
            "lb_endpoints": [{
              "endpoint": {
                "address":{
                  "socket_address": {
                    "protocol": "TCP",
                    "address": "127.0.0.1",
                    "port_value": 15020
                  }
                }
              }
            }]
          }]
        }
      },
      {
        "name": "sds-grpc",
        "type": "STATIC",
        "typed_extension_protocol_options": {
          "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": {
           "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions",
           "explicit_http_config": {
            "http2_protocol_options": {}
           }
          }
        },
        "connect_timeout": "1s",
        "lb_policy": "ROUND_ROBIN",
        "load_assignment": {
          "cluster_name": "sds-grpc",
          "endpoints": [{
            "lb_endpoints": [{
              "endpoint": {
                "address":{
                  "pipe": {
                    "path": "./var/run/secrets/workload-spiffe-uds/socket"
                  }
                }
              }
            }]
          }]
        }
      },
      {
        "name": "xds-grpc",
        "type" : "STATIC",
        "connect_timeout": "1s",
        "lb_policy": "ROUND_ROBIN",
        "load_assignment": {
          "cluster_name": "xds-grpc",
          "endpoints": [{
            "lb_endpoints": [{
              "endpoint": {
                "address":{
                  "pipe": {
                    "path": "./etc/istio/proxy/XDS"
                  }
                }
              }
            }]
          }]
        },
        "circuit_breakers": {
          "thresholds": [
            {
              "priority": "DEFAULT",
              "max_connections": 100000,
              "max_pending_requests": 100000,
              "max_requests": 100000
            },
            {
              "priority": "HIGH",
              "max_connections": 100000,
              "max_pending_requests": 100000,
              "max_requests": 100000
            }
          ]
        },
        "upstream_connection_options": {
          "tcp_keepalive": {
            "keepalive_time": 300
          }
        },
        "max_requests_per_connection": 1,
        "typed_extension_protocol_options": {
          "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": {
           "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions",
           "explicit_http_config": {
            "http2_protocol_options": {}
           }
          }
        }
      }
      
      ,
      {
        "name": "zipkin",
        "type": "STRICT_DNS",
        "respect_dns_ttl": true,
        "dns_lookup_family": "V4_ONLY",
        "dns_refresh_rate": "30s",
        "connect_timeout": "1s",
        "lb_policy": "ROUND_ROBIN",
        "load_assignment": {
          "cluster_name": "zipkin",
          "endpoints": [{
            "lb_endpoints": [{
              "endpoint": {
                "address":{
                  "socket_address": {"address": "zipkin.istio-system", "port_value": 9411}
                }
              }
            }]
          }]
        }
      }
      
      
    ],
    "listeners":[
      {
        "address": {
          "socket_address": {
            "protocol": "TCP",
            "address": "0.0.0.0",
            "port_value": 15090
          }
        },
        "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",
                  "codec_type": "AUTO",
                  "stat_prefix": "stats",
                  "route_config": {
                    "virtual_hosts": [
                      {
                        "name": "backend",
                        "domains": [
                          "*"
                        ],
                        "routes": [
                          {
                            "match": {
                              "prefix": "/stats/prometheus"
                            },
                            "route": {
                              "cluster": "prometheus_stats"
                            }
                          }
                        ]
                      }
                    ]
                  },
                  "http_filters": [{
                    "name": "envoy.filters.http.router",
                    "typed_config": {
                      "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
                    }
                  }]
                }
              }
            ]
          }
        ]
      },
      {
        "address": {
           "socket_address": {
             "protocol": "TCP",
             "address": "0.0.0.0",
             "port_value": 15021
           }
        },
        "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",
                  "codec_type": "AUTO",
                  "stat_prefix": "agent",
                  "route_config": {
                    "virtual_hosts": [
                      {
                        "name": "backend",
                        "domains": [
                          "*"
                        ],
                        "routes": [
                          {
                            "match": {
                              "prefix": "/healthz/ready"
                            },
                            "route": {
                              "cluster": "agent"
                            }
                          }
                        ]
                      }
                    ]
                  },
                  "http_filters": [{
                    "name": "envoy.filters.http.router",
                    "typed_config": {
                      "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
                    }
                  }]
                }
              }
            ]
          }
        ]
      }
    ]
  }
  ,
  "tracing": {
    "http": {
      "name": "envoy.tracers.zipkin",
      "typed_config": {
        "@type": "type.googleapis.com/envoy.config.trace.v3.ZipkinConfig",
        "collector_cluster": "zipkin",
        "collector_endpoint": "/api/v2/spans",
        "collector_endpoint_version": "HTTP_JSON",
        "trace_id_128bit": true,
        "shared_span_context": false
      }
    }
  }
  
  
}

 

Istio에서 Envoy 프록시의 구성을 확인하는 과정에서, 핵심적으로 확인해야 할 몇 가지 중요한 설정이 있습니다. 이 설정들은 Istio 및 Envoy 프록시의 동작 방식, 네트워크 정책, 트래픽 라우팅, 모니터링, 로깅 등에 중요한 역할을 합니다. 로그가 복잡하니 ChatGPT를 통해 분석을 권합니다.

핵심적으로 확인해야 할 부분

  1. 트래픽 가로채기 모드(sidecar.istio.io/interceptionMode): REDIRECT 모드로 설정되어 있는지 확인합니다.
  2. 제어 플레인 통신(discoveryAddress, controlPlaneAuthPolicy): Istio 제어 플레인과의 통신이 정상적으로 설정되어 있는지 확인합니다.
  3. 포트 필터링(traffic.sidecar.istio.io/excludeInboundPorts, includeInboundPorts): Inbound 및 Outbound 트래픽 필터링 설정을 확인합니다.
  4. TLS 인증 설정(MUTUAL_TLS): Istio와 Envoy 간 상호 TLS 인증이 활성화되어 있는지 확인합니다.
  5. 메트릭 및 트레이싱 설정(prometheus_stats, zipkin): Prometheus 및 Zipkin으로 메트릭과 트레이싱 데이터가 전송되도록 설정되어 있는지 확인합니다.

이 설정들이 올바르게 되어 있는지 확인함으로써 Istio에서 Envoy 프록시가 올바르게 동작하고, 네트워크 트래픽을 가로채고, 모니터링 및 보안 정책을 적용하는지 알 수 있습니다.

istioctl 를 통해 파드별 Envoy 동기화를 확인할 수 있습니다.

# Mac M1, istioctl 설치
brew install istioctl

# proxy 확인
istioctl proxy-status
  • 프록시 상태는 SYNCED로 모두 정상입니다. istiod 컨트롤 플레인과 각 사이드카 프록시 간의 네트워크 설정이 동기화되어 있습니다.
  • CDS (Cluster Discovery Service): Envoy의 클러스터 구성 정보가 Istio 컨트롤 플레인과 동기화되었는지를 나타냅니다. SYNCED는 동기화가 잘 이루어졌음을 의미합니다.
  • LDS (Listener Discovery Service): Envoy의 리스너 구성 정보가 Istio 컨트롤 플레인과 동기화되었는지를 나타냅니다. SYNCED로 표시되어 리스너 설정이 문제없이 동기화되었음을 보여줍니다.
  • EDS (Endpoint Discovery Service): 프록시가 사용하는 엔드포인트 정보가 동기화되었는지 나타냅니다. 여기서도 SYNCED 상태입니다.
  • RDS (Route Discovery Service): Envoy의 라우트 구성 정보가 동기화되었는지 나타냅니다. SYNCED로 되어 있어 라우트 구성이 정상적으로 이루어졌음을 알 수 있습니다.
  • ECDS (Extension Config Discovery Service): 확장 구성이 적용되었는지 여부를 나타냅니다. 대부분 NOT SENT로 표시된 것은 확장 구성을 사용하지 않는다는 의미입니다.
 
#istio CRD 확인 
kubectl get Gateway,VirtualService,DestinationRule -A

각 객체를 확인하여 istio 통신 구조를 확인하겠습니다.

ISTIO CRD 객체와 기능 설명은 필자의 이전 블로그 글(Istio On EKS) 을 참고해주세요.

 

Istio On EKS

Overview EKS에서 서비스 매시를 활용하기 위해 ISTIO를 학습한 내용을 정리하겠습니다. 학습 내용은 CloudNet@ 가시다님께서 진행하시는 스터디(AEWS2) 내용을 참고하였습니다. 좋은 자료 공유해주시는

hanhorang.tistory.com

 

  1. Gateway: Gateway는 클러스터 외부의 트래픽이 쿠버네티스 클러스터 내부의 서비스로 들어오거나 나가는 방식을 제어합니다. 즉, 외부 트래픽을 어떻게 쿠버네티스 서비스로 라우팅할지 정의합니다. 이를 통해 특정 호스트, 포트, 프로토콜 등을 기준으로 트래픽을 관리할 수 있습니다.
  2. VirtualService: 트래픽 라우팅 규칙을 정의하여 서비스 간의 트래픽 흐름을 제어합니다. 이 객체는 URI, HTTP 메소드 등과 같은 요청 속성을 기반으로 트래픽을 어떤 방향으로 보낼지 결정하는 데 사용됩니다. VirtualService는 한 개 이상의 Gateway에 연결될 수 있으며, 특정 서비스나 여러 서비스의 버전(예: A/B 테스팅, 카나리 배포 등)으로 트래픽을 분할하는 기능을 제공합니다.
  3. DestinationRule: 이름처럼, 트래픽의 최종 목적지를 정의하는 정책입니다. 이 정책에는 로드 밸런싱 설정, 연결 풀 설정, 아웃바운드 연결 타임아웃 등이 포함될 수 있습니다. 이러한 정책은 변수로 지정되어 VirutalService에서 트래픽을 라우팅하는 데 사용됩니다.

 

Gateway

apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  annotations:
    meta.helm.sh/release-name: istio-ingress
    meta.helm.sh/release-namespace: ingress
  creationTimestamp: "2024-10-19T11:28:15Z"
  generation: 1
  labels:
    app.kubernetes.io/managed-by: Helm
  name: ingress-gateway
  namespace: ingress
  resourceVersion: "4173"
  uid: 8e646a19-23a1-4426-906d-1e8d3954eff0
spec:
  selector:
    app: istio-ingressgateway
  servers:
  - hosts:
    - '*'
    port:
      name: https-8443
      number: 8443
      protocol: HTTPS
    tls:
      credentialName: gateway-cert
      mode: SIMPLE
  - hosts:
    - '*'
    port:
      name: http-8080
      number: 8080
      protocol: HTTP

모든 호스트(x) 에서 들어오는 HTTP, HTTPS 트래픽을 수신하도록 설정되어 있습니다. HTTPS 트래픽은 gateway-cert 인증서를 통해 처리됩니다.

또한, tls 설정 모드가 SIMPLE 모드로 서버 측에서만 TLS 인증서를 사용하여 트래픽을 암호화합니다. 이는 클라이언트에 대한 별도 인증이 없으며, 서버가 제공하는 인증서를 통해 클라이언트와 서버 간 통신이 암호화됨을 뜻합니다.

 

 

VirtualService

 

kubectl get virtualservice dex -n auth -o yaml
kubectl get virtualservice oauth2-proxy -n auth -o yaml
kubectl get virtualservice ingress-gateway-health-check -n ingress -o yaml
kubectl get virtualservice centraldashboard -n kubeflow -o yaml
kubectl get virtualservice katib-ui -n kubeflow -o yaml
kubectl get virtualservice kubeflow-notebooks-webapp -n kubeflow -o yaml
kubectl get virtualservice metadata-grpc -n kubeflow -o yaml
kubectl get virtualservice ml-pipeline-ui -n kubeflow -o yaml
kubectl get virtualservice profiles-kfam -n kubeflow -o yaml
kubectl get virtualservice tensorboards-web-app-tensorboards-web-app -n kubeflow -o yaml
kubectl get virtualservice volumes-web-app-virtual-service -n kubeflow -o yaml
 
기능
YAML 필드
설정 옵션
설명
Istio Gateway와 연계
gateways
gateways: - ingress/ingress-gateway
VirtualService가 Istio Gateway와 연계되어 외부 트래픽을 수신하도록 설정합니다. 이 필드는 Gateway와의 연결을 정의합니다.
HTTP 라우팅
http.match.uri
- match: - uri: exact: /authservice/logout redirect: uri: /oauth2/sign_out
특정 URI 패턴에 매칭되는 트래픽을 라우팅합니다. 예를 들어, /dex/로 시작하는 모든 요청을 매칭하여 처리합니다.
URI 리다이렉션과 재작성
http.redirect, http.rewrite
- match: - uri: exact: /authservice/logout redirect: uri: /oauth2/sign_out - match: - uri: prefix: /katib/ rewrite: uri: /katib/
특정 URI로 들어오는 요청을 다른 URI로 리다이렉션하거나 URI를 재작성하여 백엔드 서비스로 전달합니다.
헤더 조작
headers.request.add
headers: request: add: x-forwarded-prefix: /jupyter
요청 헤더에 새로운 헤더를 추가하거나 수정할 수 있습니다. 여기서는 x-forwarded-prefix: /jupyter 헤더를 추가합니다.
백엔드 서비스로 트래픽 전달
route.destination
route: - destination: host: dex.auth.svc.cluster.local port: number: 5556
트래픽을 지정된 백엔드 서비스로 전달합니다. 여기서는 dex.auth.svc.cluster.local 서비스의 5556 포트로 트래픽을 라우팅합니다.
직접 응답
directResponse
http: - directResponse: status: 200 match: - method: exact: GET port: 8080 uri: exact: /healthcheck
백엔드 서비스로 라우팅하지 않고, Istio가 직접 응답을 반환합니다. 주로 헬스체크 같은 간단한 응답에 사용됩니다.
타임아웃 설정
timeout
http: - match: - uri: prefix: /pipeline rewrite: uri: /pipeline route: - destination: host: ml-pipeline-ui.kubeflow.svc.cluster.local port: number: 80 timeout: 300s
요청 처리에 대한 최대 대기 시간을 설정합니다. 여기서는 300초(5분) 동안 요청에 대한 응답을 기다립니다.
  • Istio Gateway와 연계: 모든 VirtualService는 ingress-gateway를 통해 외부에서 들어오는 트래픽을 수신합니다.
  • HTTP 라우팅: dex(OAUTH) URI 트래픽을 매칭하고 지정된 dex 서비스로 라우팅합니다.
  • URI 리다이렉션과 재작성: /authservice/logout으로 요청이 들어오면 /oauth2/sign_out으로 리다이렉션됩니다. 또한, /katib/로 시작하는 요청을 /katib/로 재작성하여 백엔드로 전달합니다.
  • 헤더 추가: juypter 로 들어오는 요청에 대해 헤더를 추가하여 jupyter 파드로 전달합니다. 이전 Juypter 접속시 권한이 없어 설정할 부분을 헤더 추가를 통해 해결한 부분입니다.
  • 직접응답: ingress 게이트웨이 자체 healthcheck 를 istio를 통해 200 코드를 전달합니다.
  • 타임아웃 설정: ml-pipeline-ui 요청에 대해 5분 타임아웃을 설정되어 있습니다. 머신 러닝을 고려한 작업으로 타임아웃 설정입니다.

 

DestinationRule

kubectl get destinationrule ml-pipeline-visualizationserver -n kubeflow-user-example-com -o yaml
kubectl get destinationrule kubeflow-notebooks-webapp -n kubeflow -o yaml
kubectl get destinationrule metadata-grpc-service -n kubeflow -o yaml
kubectl get destinationrule ml-pipeline -n kubeflow -o yaml
kubectl get destinationrule ml-pipeline-minio -n kubeflow -o yaml
kubectl get destinationrule ml-pipeline-mysql -n kubeflow -o yaml
kubectl get destinationrule ml-pipeline-ui -n kubeflow -o yaml
kubectl get destinationrule ml-pipeline-visualizationserver -n kubeflow -o yaml
kubectl get destinationrule tensorboards-web-app -n kubeflow -o yaml
kubectl get destinationrule volumes-web-app -n kubeflow -o yaml
DestinationRule

머신 러닝으로 통신하는 목적지에 대해 옵션 (ITSTIO_MUTUAL) 을 통해 상호 TLS을 적용합니다.

이를 통해 Zero Trust 를 설정하여 istio가 없는 통신간 내부 또는 외부에서의 접근에 있어서 모두 허용하지 못하도록 합니다.

trafficPolicy:
  tls:
    mode: ISTIO_MUTUAL

이전 필자의 블로그 글에서 내부에서 접근하여 확인한다면 다음과 같이 접속이 안되는 것을 확인할 수 있습니다.

 

 

리소스 삭제

AWS 리소스와 Kubeflow 삭제는 다음의 명령어를 통해 일괄 삭제가 가능합니다.

# 자원 삭제 
terraform destory -var="profile=default" -var="region=ap-northeast-2" -var="cluster_name=ml-eks-cluster" -var='azs=["ap-northeast-2a", "ap-northeast-2c"]' -var="import_path=s3://S3_BUCKET/ml-platform"
# terraform destroy  -var="profile=default" -var="region=ap-northeast-2" -var="cluster_name=horang" -var='azs=["ap-northeast-2a", "ap-northeast-2c"]' -var="import_path=s3://horangflow/ml-platform" 

 

 

참고

KANS 3기

https://kangbk0120.github.io/articles/2022-03/ignore-istio-dex

https://github.com/kserve/kserve/blob/master/docs/samples/istio-dex/README.md

https://bongjasee.tistory.com/m/22

https://velog.io/@seokbin/Kubeflow-V1.4-설치-및-초기-설정User-추가-CORSP