본문 바로가기
WIKI 기술 지식 베이스

이그레스 트래픽 관리하기

원문 보기 위키 갱신

이 가이드에서는 이그레스 트래픽 관리를 예시를 통해 알아볼게요. 클러스터 밖에 있는 대상으로 향하는 트래픽을 시각화하고, 정책을 적용하며, 고급 라우팅 구성을 구현하는 내용이에요.

출처: Linkerd Managing egress traffic

본문

경고

어떤 서비스 메시도 자체만으로 이그레스 트래픽에 대한 강력한 보안 보장을 제공할 수 없어요. 예를 들어 악의적인 행위자는 Linkerd 사이드카(따라서 Linkerd의 이그레스 컨트롤)를 완전히 우회할 수 있어요. 따라서 임의의 애플리케이션이 존재하는 환경에서 이그레스 트래픽을 완전히 제한하려면 보통 더 포괄적인 접근 방식이 필요해요.

이그레스 트래픽 시각화

이그레스 트래픽을 포착하고 그에 정책을 적용하려면 EgressNetwork CRD를 사용할 거예요. 이 CRD는 네임스페이스 범위이며, 전역적으로 구성된 이그레스 네임스페이스에 생성되지 않는 한 로컬 네임스페이스의 클라이언트에 적용돼요. 지금은 egress-test 네임스페이스를 만들고 여기에 단일 EgressNetwork를 추가해 볼게요.

kubectl create ns egress-test
kubectl apply -f - apiVersion: policy.linkerd.io/v1alpha1
kind: EgressNetwork
metadata:
  namespace: egress-test
  name: all-egress-traffic
spec:
  trafficPolicy: Allow
EOF

이것만으로 시스템을 통과하는 이그레스 트래픽을 시각화하기에 충분해요. 그러려면 간단한 curl 컨테이너를 배포하고 클러스터 외부 서비스에 요청을 보내기 시작하면 돼요:

kubectl apply -f - apiVersion: v1
kind: Pod
metadata:
  name: client
  namespace: egress-test
  annotations:
    linkerd.io/inject: enabled
    config.linkerd.io/proxy-metrics-hostname-labels: "true"
spec:
  containers:
  - name: client
    image: curlimages/curl
    command:
      - "sh"
      - "-c"
      - "sleep infinity"
EOF

이제 client 컨테이너에 SSH로 접속해 외부 트래픽 생성을 시작해요:

kubectl -n egress-test exec -it client -c client -- sh
$ while sleep 1; do curl -s https://httpbin.org/get ; done

별도의 셸에서 Linkerd diagnostics 명령을 사용해 트래픽을 시각화할 수 있어요.

linkerd dg proxy-metrics -n egress-test po/client | grep outbound_http_route_request_statuses_total

outbound_http_route_request_statuses_total{
  parent_group="policy.linkerd.io",
  parent_kind="EgressNetwork",
  parent_namespace="egress-test",
  parent_name="all-egress-traffic",
  parent_port="80",
  parent_section_name="",
  route_group="",
  route_kind="default",
  route_namespace="",
  route_name="http-egress-allow",
  hostname="httpbin.org",
  http_status="200",
  error=""
} 697

참고

아웃바운드 메트릭은 기본적으로 호스트 이름을 포함하지 않아요. 자세한 내용과 위 예시처럼 포함하는 방법은 Hostnames in metrics를 참고하세요.

이 원시 메트릭을 보면 parent_kind가 EgressNetwork 유형인 것으로 조회하기만 하면 서로 다른 대상으로 향하는 이그레스 트래픽을 빠르게 식별할 수 있다는 점에 주목하세요. 지금은 모든 트래픽이 허용되며 단순히 관찰만 하고 있어요. EgressNetwork의 기본 트래픽 정책이 Allow로 설정되어 있기 때문에 기본 http 라우트 이름이 http-egress-allow라는 것도 관찰할 수 있어요. 이는 Linkerd 컨트롤러가 자동으로 채우는 플레이스홀더 라우트예요.

메트릭의 호스트 이름

기본적으로 아웃바운드 메트릭은 클러스터 로컬 트래픽과 이그레스 트래픽 모두 hostname 라벨에 호스트 이름을 포함하지 않아요. 이는 많은 수의 개별 대상 호스트 이름을 다루는 워크로드에서 아웃바운드 메트릭의 높은 카디널리티(cardinality)를 방지하기 위한 안전한 기본값이에요.

이것이 워크로드에 문제가 되지 않는다면, 단일 파드 또는 네임스페이스에 각각 config.linkerd.io/proxy-metrics-hostname-labels 어노테이션을 true로 설정해 워크로드별 또는 네임스페이스별로 호스트 이름 메트릭을 활성화할 수 있어요.

호스트 이름 메트릭은 linkerd install의 값을 통해 클러스터 전체에서도 활성화할 수 있어요:

# With a single value
linkerd install --set proxy.metrics.hostnameLabels=true | kubectl apply -f -

# Or ith a values.yaml file
#
# 
proxy:
  metrics:
    hostnameLabels: true

linkerd install --values=values.yaml | kubectl apply -f -

참고

이 페이지의 나머지 예시들은 명확성을 위해 호스트 이름 메트릭이 활성화되어 있다고 가정해요.

이그레스 트래픽 제한

메트릭으로 이그레스 트래픽의 그림을 구성한 뒤에는 그중 일부만 통과시키는 정책을 적용하기 시작할 수 있어요. EgressNetwork를 업데이트해 trafficPolicy를 Deny로 변경해 볼게요:

kubectl patch egressnetwork -n egress-test all-egress-traffic \
  -p '{"spec":{"trafficPolicy": "Deny"}}' --type=merge

이제 client 컨테이너에서 실패한 요청이 관찰되기 시작해야 해요. 또한 메트릭을 보면 다음 결과를 관찰할 수 있어요:

outbound_http_route_request_statuses_total{
  parent_group="policy.linkerd.io",
  parent_kind="EgressNetwork",
  parent_namespace="egress-test",
  parent_name="all-egress-traffic",
  parent_port="80",
  parent_section_name="",
  route_group="",
  route_kind="default",
  route_namespace="",
  route_name="http-egress-deny",
  hostname="httpbin.org",
  http_status="403",
  error=""
} 45

이제 트래픽이 동일한 parent를 대상으로 하지만 라우트 이름이 http-egress-deny인 것을 명확히 관찰할 수 있어요. 또한 http_status는 403 또는 Forbidden이에요. 트래픽 정책을 Deny로 변경함으로써 로컬 네임스페이스에서 발생하는 모든 이그레스 트래픽을 금지한 거예요. 그중 일부를 허용하려면 Gateway API 유형을 사용할 수 있어요. httpbin.org에 대한 트래픽 중에서 /get 엔드포인트를 대상으로 하는 요청만 허용하고 싶다고 가정해 볼게요. 이를 위해 다음 HTTPRoute를 만들어야 해요:

kubectl apply -f - apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: httpbin-get
  namespace: egress-test
spec:
  parentRefs:
    - name: all-egress-traffic
      kind: EgressNetwork
      group: policy.linkerd.io
      namespace: egress-test
      port: 80
  rules:
    - matches:
      - path:
          value: "/get"
EOF

트래픽이 다시 흐르는 것을 볼 수 있고, 메트릭을 보면 이 트래픽이 httpbin-get 라우트를 통해 흐른다는 것을 확인할 수 있을 거예요.

outbound_http_route_request_statuses_total{
  parent_group="policy.linkerd.io",
  parent_kind="EgressNetwork",
  parent_namespace="egress-test",
  parent_name="all-egress-traffic",
  parent_port="80",
  parent_section_name="",
  route_group="gateway.networking.k8s.io",
  route_kind="HTTPRoute",
  route_namespace="egress-test",
  route_name="httpbin-get",
  hostname="httpbin.org",
  http_status="200",
  error=""
} 63

흥미롭게도 client 셸로 돌아가 동일한 서비스로 HTTPS 트래픽을 시작하려고 하면 허용되지 않을 거예요:

~ $ curl -v https://httpbin.org/get
curl: (35) TLS connect error: error:00000000:lib(0)::reason(0)

이는 현재 구성이 평문 HTTP 트래픽만 시스템을 통과시키도록 허용하기 때문이에요. Gateway API TLSRoute를 사용해 HTTPS 트래픽도 추가로 허용할 수 있어요:

kubectl apply -f - apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
  name: tls-egress
  namespace: egress-test
spec:
  hostnames:
  - httpbin.org
  parentRefs:
  - name: all-egress-traffic
    kind: EgressNetwork
    group: policy.linkerd.io
    namespace: egress-test
    port: 443
  rules:
  - backendRefs:
    - kind: EgressNetwork
      group: policy.linkerd.io
      name: all-egress-traffic
EOF

이렇게 하면 문제가 해결되고 외부 서비스로 가는 HTTPS 요청이 성공하는 것이 메트릭에 반영된 것을 볼 수 있어요:

linkerd dg proxy-metrics -n egress-test po/client | grep outbound_tls_route_open_total

outbound_tls_route_open_total{
  parent_group="policy.linkerd.io",
  parent_kind="EgressNetwork",
  parent_namespace="egress-test",
  parent_name="all-egress-traffic",
  parent_port="443",
  parent_section_name="",
  route_group="gateway.networking.k8s.io",
  route_kind="TLSRoute",
  route_namespace="egress-test",
  route_name="tls-egress",
  hostname="httpbin.org"
} 2

이 구성은 httpbin.org에 대한 트래픽만 허용해요. TLS 연결에 대한 정책 결정을 적용하기 위해 프록시는 TLS 세션의 ClientHello에서 SNI 확장 헤더를 파싱해 이를 대상 호스트 이름 식별자로 사용해요. 따라서 client에서 github.com으로 요청을 시작하려고 하면, 현재 정책 구성에 의해 금지되었으므로 프록시가 연결을 미리 닫는 것을 볼 수 있어요:

linkerd dg proxy-metrics -n egress-test po/client | grep outbound_tls_route_close_total

outbound_tls_route_close_total{
  parent_group="policy.linkerd.io",
  parent_kind="EgressNetwork",
  parent_namespace="egress-test",
  parent_name="all-egress-traffic",
  parent_port="443",
  parent_section_name="",
  route_group="",
  route_kind="default",
  route_namespace="",
  route_name="tls-egress-deny",
  hostname="github.com",
  error="forbidden"
} 1

비슷한 방식으로 GRPCRoute와 TCPRoute 같은 다른 Gateway API 라우트 유형을 사용해 EgressNetwork 프리미티브가 포착하는 트래픽을 조정할 수 있어요. 이 모든 트래픽 유형에는 트래픽이 시스템을 통해 어떻게 흐르는지, 어떤 정책 결정이 내려졌는지 설명하는 라우트 기반 메트릭 세트가 함께 제공돼요.

이그레스 트래픽을 클러스터로 다시 리다이렉트

Gateway API 라우트 유형을 사용해 이그레스 트래픽을 모델링하면 몇 가지 더 고급 라우팅 구성을 구현할 수 있어요. 다음 규칙을 적용하고 싶다고 가정해 볼게요:

  • 암호화되지 않은 HTTP 트래픽은 httpbin.org/get과 다른 엔드포인트만 대상으로 할 수 있음
  • 암호화된 HTTPS 트래픽은 모든 대상에 허용됨
  • 기타 모든 암호화되지 않은 HTTP 트래픽은 내부 서비스로 리다이렉트되어야 함

먼저 트래픽이 리다이렉트되어야 할 내부 서비스를 만들어 볼게요:

kubectl apply -f - apiVersion: v1
kind: Service
metadata:
  name: internal-egress
  namespace: egress-test
spec:
  type: ClusterIP
  selector:
    app: internal-egress
  ports:
  - port: 80
    protocol: TCP
    name: one
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: egress-test
  name: internal-egress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: internal-egress
  template:
    metadata:
      labels:
        app: internal-egress
      annotations:
        linkerd.io/inject: enabled
    spec:
      containers:
        - name: legacy-app
          image: buoyantio/bb:v0.0.5
          command: [ "sh", "-c"]
          args:
          - "/out/bb terminus --h1-server-port 80 --response-text 'You cannot go there right now' --fire-and-forget"
          ports:
            - name: http-port
              containerPort: 80
EOF

첫 번째 규칙을 허용하기 위해 이런 형태의 HTTPRoute를 만들어야 해요:

kubectl apply -f - apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: httpbin-get
  namespace: egress-test
spec:
  parentRefs:
    - name: all-egress-traffic
      kind: EgressNetwork
      group: policy.linkerd.io
      namespace: egress-test
      port: 80
  rules:
    - matches:
      - path:
          value: "/get"
EOF

모든 TLS 트래픽을 허용하려면 다음 TLSRoute가 필요해요:

kubectl apply -f - apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
  name: tls-egress
  namespace: egress-test
spec:
  parentRefs:
  - name: all-egress-traffic
    kind: EgressNetwork
    group: policy.linkerd.io
    namespace: egress-test
    port: 443
  rules:
  - backendRefs:
    - kind: EgressNetwork
      group: policy.linkerd.io
      name: all-egress-traffic
EOF

마지막으로 나머지 평문 HTTP 트래픽을 내부 서비스로 리다이렉트하기 위해, 내부 서비스를 커스텀 백엔드로 하는 HTTPRoute를 만들어요:

kubectl apply -f - apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: unencrypted-http
  namespace: egress-test
spec:
  parentRefs:
    - name: all-egress-traffic
      kind: EgressNetwork
      group: policy.linkerd.io
      namespace: egress-test
      port: 80
  rules:
  - backendRefs:
    - kind: Service
      name: internal-egress
      port: 80
EOF

이제 모든 것이 예상대로 동작하는지 확인해 볼게요:

# plaintext traffic goes as expected to the /get path
$ curl  https://httpbin.org/get
{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.11.0",
    "X-Amzn-Trace-Id": "Root=1-674599d4-77a473943844e9e31844b48e"
  },
  "origin": "51.116.126.217",
  "url": "https://httpbin.org/get"
}

# encrypted traffic can target all paths and hosts
$ curl  https://httpbin.org/ip
{
  "origin": "51.116.126.217"
}

# arbitrary unencrypted traffic goes to the internal service
$ curl https://google.com
{
  "requestUID": "in:http-sid:terminus-grpc:-1-h1:80-190120723",
  "payload": "You cannot go there right now"}
}

정리

모든 것을 정리하려면 네임스페이스를 삭제하기만 하면 돼요: kubectl delete ns egress-test.

더 알아보기 (Learn more)