IT/Container&k8s

[k8s] Service Mesh

Hayley Shim 2023. 10. 29. 00:43

안녕하세요. Service Mesh 기술에 대해 이해하기 위해 작성한 글입니다.

Service Mesh

  • MSA에서 마이크로서비스 간의 통신이 Mesh 네트워크 형태를 띄는 것을 말합니다.
  • 서비스가 아니라 서비스의 네트워크 처리를 추상화한 Proxy를 그물처럼 엮은 것입니다.
  • Pod 간 통신 경로에 Proxy를 놓고 트래픽 모니터링이나 트래픽 컨트롤이 가능합니다.
  • 따라서, Service Mesh는 서비스를 구현한 어플리케이션에 구축된 전용 인프라 계층(Infrastructure Layer)이며, 서비스 간의 통신을 처리합니다.
  • 링크를 참고하면 많은 Service Mesh 기술이 있지만 주로 사용되는 예로 Istio, Linkerd, AWS App Mesh 등이 있습니다.
  • 과거에 Spring Cloud Netflix가 대표적인 기술이었으나 Spring Cloud Netflix는 library를 이용해서 Service Mesh를 구현하는 방식으로 코드와의 결합도가 높고 Java 언어만 지원되서 최근 코드 결합도가 낮은 Istio 솔루션이 주로 사용됩니다.

Service Mesh Architecture 패턴

  • Library : 서비스 어플리케이션 안에 Service Mesh가 가져야 할 것과 같은 핵심 기능을 구현하는 라이브러리 코드를 포함하는 방식(Spring Cloud Netflix)
  • Node Agent(=Host Proxy) : 모든 노드(서비스 인스턴스, Host)마다 하나의 에이전트(=프록시)를 배치하여 서비스간 통신을 제어하는 방식
  • Sidecar : 서비스 옆에 경량화 프록시를 Sidecar 패턴으로 배치하여 서비스간 통신을 제어하는 방식( ex) Istio)

주요 기능

  • Service Discovery : 서비스 식별 기능은 분산 환경에서 동적으로 생성, 변경, 삭제되는 서비스 인스턴스의 접속 정보를 자동으로 등록, 삭제, 관리하는 기능을 담당한다.
  • Routing/ Load Balancing : MSA는 많은 서비스들을 보유하게 되며 각 서비스들의 부하를 분산하기 위하여 Load Balancing은 필수적이다. Service Mesh에서 로드밸런싱 기능은 4 계층에서 구현되며, 강력한 트래픽 관리 기능을 제공한다. 최근에는 7계층(어플리케이션 레이어)까지도 지원한다.
  • 트래픽 모니터링 : 요청의 ‘에러율, 레이턴시, 커넥션 개수, 요청 개수’ 등 메트릭 모니터링, 특정 서비스간 혹은 특정 요청 경로로 필터링 → 원인 파악 용이
  • 트래픽 컨트롤 : 트래픽 시프팅(Traffic shifting), 서킷 브레이커(Circuit Breaker), 폴트 인젝션(Fault Injection), 속도 제한(Rate Limit)
    — 트래픽 시프팅(Traffic shifting) : 예시) 99% 기존앱 + 1% 신규앱 , 특정 단말/사용자는 신규앱에 전달하여 단계적으로 적용하는 카니리 배포 가능
    — 서킷 브레이커(Circuit Breaker) : 목적지 마이크로서비스에 문제가 있을 시 접속을 차단하고 출발지 마이크로서비스에 요청 에러를 반환 (연쇄 장애, 시스템 전제 장애 예방)
    — 폴트 인젝션(Fault Injection) : 의도적으로 요청을 지연 혹은 실패를 구현
    — 속도 제한(Rate Limit) : 요청 개수를 제한

Service Mesh 종류

1. Istio

Istio Architecture

Istio Architecture

Data Plane

  • Sidecar로 배포된 일련의 intelligent proxy(Envoy)로 구성됩니다.
  • Proxy는 마이크로서비스 간의 모든 네트워크 통신을 중재하고 제어합니다.
  • 모든 Mesh traffic에 대한 원격 분석(telemetry)을 수집하고 보고합니다.

Control Plane

  • Proxy를 관리하고 구성하여 트래픽을 라우팅합니다.

Components

  • Envoy : Istio는 Envoy Proxy의 확장 버전을 사용합니다. Envoy는 Service Mesh의 모든 서비스에 대한 모든 인바운드 및 아웃바운드 트래픽을 중재하기 위해 C++로 개발된 고성능 Proxy입니다. Envoy Proxy는 데이터 플레인 트래픽과 상호 작용하는 유일한 Istio 구성 요소입니다. Envoy Proxy는 서비스에 대한 Sidecar로 배포되어 Envoy의 많은 내장 기능으로 서비스를 논리적으로 보강합니다.
  • Istiod : Istiod는 서비스 검색, 구성 및 인증서 관리를 제공합니다. Istiod 보안은 기본 제공 ID 및 자격 증명 관리를 통해 강력한 서비스 간 인증 및 최종 사용자 인증을 지원합니다. Istio를 사용하여 서비스 메시에서 암호화되지 않은 트래픽을 업그레이드할 수 있습니다. Istiod는 인증 기관(CA) 역할을 하며 데이터 영역에서 보안 mTLS 통신을 허용하는 인증서를 생성합니다.

2. AWS App Mesh

What is App Mesh

  • AWS App Mesh는 서비스가 여러 유형의 컴퓨팅 인프라에서 서로 쉽게 통신할 수 있도록 애플리케이션 수준 네트워킹을 제공하는 서비스 메시입니다.
  • AWS App Mesh는 관리형 Control Plane이므로 사용자는 이를 실행하기 위해 서버를 설치하거나 관리할 필요가 없습니다. App Mesh의 데이터 영역은 오픈 소스 Envoy Proxy입니다.

AWS App Mesh Components

App Mesh Components

  • Service mesh : 서비스 간의 network traffic에 대한 logical boundry입니다.
  • Virtual services : 가상 라우터를 통해 가상 노드에서 직접 또는 간접적으로 제공되는 실제 서비스의 추상화입니다.
  • Virtual node : ECS 서비스 또는 Kubernetes 배포와 같은 특정 작업 그룹에 대한 논리적 포인터 역할을 합니다. 가상 노드를 생성할 때 작업 그룹의 서비스 검색 이름을 지정해야 합니다.
  • Envoy proxy: 가상 라우터 및 가상 노드에 대해 설정한 App Mesh 서비스 메시 트래픽 규칙을 사용하도록 마이크로서비스 작업 그룹을 구성합니다. 가상 노드, 가상 라우터, 경로 및 가상 서비스를 생성한 후 작업 그룹에 Envoy 컨테이너를 추가합니다.
  • Virtual routers : 메시 내에서 하나 이상의 가상 서비스에 대한 트래픽을 처리합니다.
  • Routes : 가상 라우터와 연결되며 서비스 이름 접두사와 일치하는 트래픽을 하나 이상의 가상 노드로 보냅니다.

AWS App Mesh 기능

  • Traffic Management(서비스 라우팅, 트래픽 제어, Traffic Routing)
  • Load Balancing, 로드밸런싱, 부하 분산, 클라이언트 측 트래픽 정책, Service Discovery
  • Resiliency, 내결함성, 계단식 장애 방지, 서킷브레이크, Failure Recovery(Retry, Circuit Breaker, Timeout)
  • Security, 보안 기능(TLS, 암호화, 인증 및 권한 부여), Mutual Authentication, ID Provider
  • Telemetry, 가시성

App Mesh 실습 — 참고[github, workshop]

# Mesh 생성
aws appmesh create-mesh \
  --mesh-name appmesh-workshop \
  --spec egressFilter={type=DROP_ALL}


# 변수 정의
INT_LOAD_BALANCER=$(jq < cfn-output.json -r '.InternalLoadBalancerDNS');
SPEC=$(cat <<-EOF
  { 
    "serviceDiscovery": {
      "dns": { 
        "hostname": "$INT_LOAD_BALANCER"
      }
    },
    "logging": {
      "accessLog": {
        "file": {
          "path": "/dev/stdout"
        }
      }
    },      
    "listeners": [
      {
        "healthCheck": {
          "healthyThreshold": 3,
          "intervalMillis": 10000,
          "path": "/health",
          "port": 3000,
          "protocol": "http",
          "timeoutMillis": 5000,
          "unhealthyThreshold": 3
        },
        "portMapping": { "port": 3000, "protocol": "http" }
      }
    ]
  }
EOF
); \


# app mesh virual node 생성
aws appmesh create-virtual-node \
  --mesh-name appmesh-workshop \
  --virtual-node-name crystal-lb-vanilla \
  --spec "$SPEC"


# 변수 정의
SPEC=$(cat <<-EOF
  { 
    "provider": {
      "virtualNode": { 
        "virtualNodeName": "crystal-lb-vanilla"
      }
    }
  }
EOF
); \


# app mesh virtual service 생성
# 가상 서비스는 메시의 가상 노드 에서 제공하는 실제 서비스의 추상화
aws appmesh create-virtual-service \
  --mesh-name appmesh-workshop \
  --virtual-service-name crystal.appmeshworkshop.hosted.local \
  --spec "$SPEC"


# 변수 정의
# APPMESH_VIRTUAL_NODE_NAME 환경 변수 : 해당 값은 실제로 이 프록시가 옆에 있는 App Mesh 내부의 가상 노드 이름을 반영합니다. 
# 예시의 경우 mesh/appmesh-workshop/virtualNode/crystal-lb-vanilla
# 인그레스 트래픽을 수신할 수 있도록 Envoy에 전달해야 하는 유일한 필수 환경 변수
ENVOY_REGISTRY="840364872350.dkr.ecr.$AWS_REGION.amazonaws.com";
TASK_DEF_ARN=$(jq < cfn-output.json -r '.CrystalTaskDefinition');
TASK_DEF_OLD=$(aws ecs describe-task-definition --task-definition $TASK_DEF_ARN);
TASK_DEF_NEW=$(echo $TASK_DEF_OLD \
  | jq ' .taskDefinition' \
  | jq --arg ENVOY_REGISTRY $ENVOY_REGISTRY ' .containerDefinitions += 
        [
          {
            "environment": [
              {
                "name": "APPMESH_VIRTUAL_NODE_NAME",
                "value": "mesh/appmesh-workshop/virtualNode/crystal-lb-vanilla"
              }
            ],
            "image": ($ENVOY_REGISTRY + "/aws-appmesh-envoy:v1.11.2.0-prod"),
            "healthCheck": {
              "retries": 3,
              "command": [
                "CMD-SHELL",
                "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"
              ],
              "timeout": 2,
              "interval": 5,
              "startPeriod": 10
            },
            "essential": true,
            "user": "1337",
            "name": "envoy"
          }
        ]' \
  | jq ' .containerDefinitions[0] +=
        { 
          "dependsOn": [ 
            { 
              "containerName": "envoy",
              "condition": "HEALTHY" 
            }
          ] 
        }' \
  | jq ' . += 
        { 
          "proxyConfiguration": {
            "type": "APPMESH",
            "containerName": "envoy",
            "properties": [
              { "name": "IgnoredUID", "value": "1337"},
              { "name": "ProxyIngressPort", "value": "15000"},
              { "name": "ProxyEgressPort", "value": "15001"},
              { "name": "AppPorts", "value": "3000"},
              { "name": "EgressIgnoredIPs", "value": "169.254.170.2,169.254.169.254"}
            ]
          }
        }' \
  | jq ' del(.status, .compatibilities, .taskDefinitionArn, .requiresAttributes, .revision, .registeredBy, .registeredAt) '
); \


TASK_DEF_FAMILY=$(echo $TASK_DEF_ARN | cut -d"/" -f2 | cut -d":" -f1);
echo $TASK_DEF_NEW > /tmp/$TASK_DEF_FAMILY.json && 
# Envoy 사이드카 프록시에 새 작업 정의를 등록
aws ecs register-task-definition \
  --cli-input-json file:///tmp/$TASK_DEF_FAMILY.json


# 변수 정의
CLUSTER_NAME=$(jq < cfn-output.json -r '.EcsClusterName');
TASK_DEF_ARN=$(aws ecs list-task-definitions | \
  jq -r ' .taskDefinitionArns[] | select( . | contains("crystal"))' | tail -1);

# 서비스 업데이트
aws ecs update-service \
  --cluster $CLUSTER_NAME \
  --service crystal-service-lb \
  --task-definition "$(echo $TASK_DEF_ARN)"


# 변수 정의
CLUSTER_NAME=$(jq < cfn-output.json -r '.EcsClusterName');
TASK_DEF_ARN=$(aws ecs list-task-definitions | \
  jq -r ' .taskDefinitionArns[] | select( . | contains("crystal"))' | tail -1);


# 서비스 작업이 실행 상태가 될 때까지 기다림
_list_tasks() {
  aws ecs list-tasks \
    --cluster $CLUSTER_NAME \
    --service crystal-service-lb | \
  jq -r ' .taskArns | @text' | \
    while read taskArns; do 
      aws ecs describe-tasks --cluster $CLUSTER_NAME --tasks $taskArns;
    done | \
  jq -r --arg TASK_DEF_ARN $TASK_DEF_ARN \
    ' [.tasks[] | select( (.taskDefinitionArn == $TASK_DEF_ARN) 
                    and (.lastStatus == "RUNNING" ))] | length'
}
until [ $(_list_tasks) == "3" ]; do
  echo "Tasks are starting ..."
  sleep 10s
done && echo "Tasks started"


# EC2 인스턴스로 세션 관리자 세션 시작
AUTO_SCALING_GROUP=$(jq < cfn-output.json -r '.RubyAutoScalingGroupName');
TARGET_EC2=$(aws ec2 describe-instances \
    --filters Name=tag:aws:autoscaling:groupName,Values=$AUTO_SCALING_GROUP | \
  jq -r ' .Reservations | first | .Instances | first | .InstanceId')
aws ssm start-session --target $TARGET_EC2


# 마이크로서비스를 curl해서 서버 헤더가 있는지 확인
# 이은 그 자체로 Envoy가 요청을 프록싱하고 있다는 확인임
# TARGET_IP=$(dig +short crystal.appmeshworkshop.hosted.local | head -1)
curl -v $TARGET_IP:3000/crystal

 

blog migration project

written in 2023.6.13

https://medium.com/techblog-hayleyshim/k8s-service-mesh-5b96ed2fd873