비(非)Kubernetes 워크로드를 메시에 추가하기
비(非)Kubernetes 워크로드를 메시에 추가하기 (Adding non-Kubernetes workloads to your mesh)
이 가이드에서는 메시 확장(mesh expansion)의 한 예를 따라 해봐요. 예시 비-Kubernetes 워크로드를 설정·구성해서 Linkerd 메시에 추가하는 과정을 안내합니다.
본문
이 가이드에서는 다음을 어떻게 하는지 알아볼 거예요.
- Kubernetes 클러스터 밖의 가상 또는 물리 머신에 Linkerd 프록시 설치하기
- 트래픽이 프록시를 통해 라우팅되도록 네트워크 규칙 구성하기
- 메시에 외부 워크로드 등록하기
- 트래픽 패턴을 실습하고 외부 워크로드에 영향을 주는 인가 정책 적용하기
워크로드 아이덴티티를 생성하기 위해 아이덴티티 메커니즘으로 SPIRE를 사용할 거예요.
사전 준비 (Prerequisites)
다음이 필요합니다.
- 정상 동작하는 Linkerd 설치와 신뢰 앵커(trust anchor)
- 상승된 권한이 있는 클러스터. 로컬 개발에는 kind나 k3d를 사용할 수 있어요.
- 물리 또는 가상 머신
- 머신에 iptables 규칙을 수정할 수 있는 NET_CAP 권한
- 머신에서 메시의 모든 pod로 가는 IP 연결성
- 머신이 클러스터 내 Kubernetes 워크로드의 DNS 이름을 해석할 수 있도록 동작하는 DNS 설정
현재 신뢰 앵커와 키 가져오기
클러스터 경계를 넘어 상호 TLS를 사용하려면, 클러스터 밖 머신과 클러스터가 공유 신뢰 앵커를 가져야 합니다. 이 튜토리얼에서는 Linkerd 배포의 신뢰 앵커 인증서와 비밀 키에 접근할 수 있고, 그것을 ca.key와 ca.crt라는 파일에 두었다고 가정할게요.
머신에 SPIRE 설치하기
Linkerd의 프록시는 보통 Linkerd 컨트롤 플레인의 identity 컴포넌트에서 TLS 인증서를 얻습니다. 자신의 신원을 증명(attest)할 때 각 pod에 제공되는 Kubernetes Service Account 토큰을 사용하죠.
우리의 외부 워드로드는 Kubernetes 밖에 있으므로 Service Account 토큰이라는 개념이 존재하지 않습니다. 그래서 클러스터 밖 리소스의 아이덴티티를 만들기 위해 SPIFFE 프레임워크와 그 구현인 SPIRE를 사용합니다. 따라서 메시 확장에서는 Linkerd 프록시가 Linkerd의 identity 서비스 대신 SPIRE에서 직접 인증서를 얻도록 구성해요. SPIFFE의 마법은 이런 인증서가 클러스터에서 Linkerd가 생성한 인증서와 호환된다는 점입니다.
프로덕션에서는 이미 SPIFFE 위에 구축한 자체 identity 인프라가 있어서 외부 머신의 프록시가 사용할 수 있을 수도 있어요. 하지만 이 튜토리얼에서는 머신에 최소한의 SPIRE 환경을 설치하고 설정하는 과정을 안내해 드릴게요. 먼저 SPIRE GitHub 릴리스 페이지에서 다운로드해 SPIRE를 설치해야 합니다. 예를 들면:
`wget https://github.com/spiffe/SPIRE/releases/download/v1.8.2/SPIRE-1.8.2-linux-amd64-musl.tar.gz
tar zvxf SPIRE-1.8.2-linux-amd64-musl.tar.gz
cp -r SPIRE-1.8.2/. /opt/SPIRE/
`
그다음 머신에 SPIRE 서버를 구성해야 합니다:
`cat >/opt/SPIRE/server.cfg server {
bind_address = "127.0.0.1"
bind_port = "8081"
trust_domain = "root.linkerd.cluster.local"
data_dir = "/opt/SPIRE/data/server"
log_level = "DEBUG"
ca_ttl = "168h"
default_x509_svid_ttl = "48h"
}
plugins {
DataStore "sql" {
plugin_data {
database_type = "sqlite3"
connection_string = "/opt/SPIRE/data/server/datastore.sqlite3"
}
}
KeyManager "disk" {
plugin_data {
keys_path = "/opt/SPIRE/data/server/keys.json"
}
}
NodeAttestor "join_token" {
plugin_data {}
}
UpstreamAuthority "disk" {
plugin_data {
cert_file_path = "/opt/SPIRE/certs/ca.crt"
key_file_path = "/opt/SPIRE/certs/ca.key"
}
}
}
EOL
`
이 파일은 SPIRE 서버를 구성합니다. Linkerd로 설치할 때 쓴 루트 인증서와 키가 /opt/SPIRE/certs 디렉터리에 있다고 가정해요.
추가로 SPIRE 에이전트도 구성해야 합니다:
`cat >/opt/SPIRE/agent.cfg agent {
data_dir = "/opt/SPIRE/data/agent"
log_level = "DEBUG"
trust_domain = "root.linkerd.cluster.local"
server_address = "localhost"
server_port = 8081
# Insecure bootstrap is NOT appropriate for production use but is ok for
# simple testing/evaluation purposes.
insecure_bootstrap = true
}
plugins {
KeyManager "disk" {
plugin_data {
directory = "/opt/SPIRE/data/agent"
}
}
NodeAttestor "join_token" {
plugin_data {}
}
WorkloadAttestor "unix" {
plugin_data {}
}
}
EOL
`
이제 서버를 시작하고 워크로드에 대한 등록 정책을 제공해야 합니다. 서버는 인증서를 발급하는 컴포넌트예요. 먼저 SPIRE 서버를 시작하고 정상인지 확인하세요:
`SPIRE-server run -config ./server.cfg &&
SPIRE-server healthcheck
`
이제 에이전트를 등록하고 실행해야 합니다. 에이전트는 SPIRE 서버에 질의해 워크로드를 증명(인증)합니다.
`AGENT_TOKEN=$(SPIRE-server token generate -spiffeID spiffe://root.linkerd.cluster.local/agent -output json | jq -r '.value')
SPIRE-agent run -config ./agent.cfg -joinToken "$AGENT_TOKEN" &
SPIRE-agent healthcheck
Agent is healthy.
`
서버와 에이전트가 모두 실행된 후에는 워크로드에 대한 등록 정책을 제공해야 합니다. 단순하게, 루트 UID로 실행되는 어떤 프로세스든 미리 정의된 SPIFFE 아이덴티티를 할당하는 간단한 등록 정책을 만들 수 있어요.
`SPIRE-server entry create -parentID spiffe://root.linkerd.cluster.local/agent \
-spiffeID spiffe://root.linkerd.cluster.local/external-workload -selector unix:uid:$(id -u root)
Entry ID : ac5e2354-596a-4059-85f7-5b76e3bb53b3
SPIFFE ID : spiffe://root.linkerd.cluster.local/external-workload
Parent ID : spiffe://root.linkerd.cluster.local/agent
TTL : 3600
Selector : unix:uid:0
`
외부 워크로드를 메시에 등록하기
Linkerd가 외부 워크로드를 알고 트래픽을 라우팅하려면 몇 가지 정보를 제공해야 합니다. 이는 클러스터에 존재해야 하는 ExternalWorkload CRD로 수행됩니다. 하나 만들어 볼게요:
`machine_IP=
kubectl --context=west apply -f -
apiVersion: workload.linkerd.io/v1beta1
kind: ExternalWorkload
metadata:
name: external-workload
namespace: mixed-env
labels:
location: vm
app: legacy-app
workload_name: external-workload
spec:
meshTLS:
identity: "spiffe://root.linkerd.cluster.local/external-workload"
serverName: "external-workload.cluster.local"
workloadIPs:
- ip: $machine_IP
ports:
- port: 80
name: http
status:
conditions:
- type: Ready
status: "True"
lastTransitionTime: "2024-01-24T11:53:43Z"
EOF
`
이렇게 하면 Kubernetes 밖에 사는 워크로드를 발견하는 데 사용되는 ExternalWorkload 리소스가 생성됩니다. Service 객체는 Pod를 선택하는 것과 같은 방식으로 이 리소스를 선택할 수 있어요. 자세한 내용은 나중에 다룰게요.
머신에 Linkerd 프록시 설치하기
머신에 Linkerd 프록시를 설치하고 실행해야 합니다. 보통 프록시는 Kubernetes에서 컨테이너로 실행됩니다. 컨테이너 자체에는 Kubernetes 환경에서 identity를 부트스트래핑하는 데 특화된 추가 장치가 들어 있어요. 외부 환경에서는 이 기능이 필요 없으므로 프록시 바이너리만 가져오면 됩니다:
`LINKERD_VERSION=edge-26.9.3
mkdir /opt/linkerd-proxy && cd /opt/linkerd-proxy
id=$(docker create cr.l5d.io/linkerd/proxy:$LINKERD_VERSION)
docker cp $id:/usr/lib/linkerd/linkerd2-proxy ./linkerd-proxy
docker rm -v $id
`
프록시 구성하고 실행하기
트래픽이 프록시를 통과하도록 머신의 네트워크 구성을 설정해야 합니다. 다음 iptables 규칙을 추가하면 됩니다:
`PROXY_INBOUND_PORT=4143
PROXY_OUTBOUND_PORT=4140
PROXY_USER_UID=$(id -u root)
# default inbound and outbound ports to ignore
INBOUND_PORTS_TO_IGNORE="4190,4191,4567,4568"
OUTBOUND_PORTS_TO_IGNORE="4567,4568"
iptables -t nat -N PROXY_INIT_REDIRECT
# ignore inbound ports
iptables -t nat -A PROXY_INIT_REDIRECT -p tcp --match multiport --dports $INBOUND_PORTS_TO_IGNORE -j RETURN
# redirect all incoming traffic to proxy's inbound port
iptables -t nat -A PROXY_INIT_REDIRECT -p tcp -j REDIRECT --to-port $PROXY_INBOUND_PORT
iptables -t nat -A PREROUTING -j PROXY_INIT_REDIRECT
# outbound rules
iptables -t nat -N PROXY_INIT_OUTPUT
# ignore proxy user
iptables -t nat -A PROXY_INIT_OUTPUT -m owner --uid-owner $PROXY_USER_UID -j RETURN
# ignore loopback
iptables -t nat -A PROXY_INIT_OUTPUT -o lo -j RETURN
# ignore outbound ports
iptables -t nat -A PROXY_INIT_OUTPUT -p tcp --match multiport --dports $OUTBOUND_PORTS_TO_IGNORE -j RETURN
# redirect all outgoing traffic proxy's outbound port
iptables -t nat -A PROXY_INIT_OUTPUT -p tcp -j REDIRECT --to-port $PROXY_OUTBOUND_PORT
iptables -t nat -A OUTPUT -j PROXY_INIT_OUTPUT
iptables-save -t nat
`
이 규칙들은 트래픽이 프록시를 거쳐 올바르게 라우팅되도록 보장합니다. 이제 이것이 끝났으니, 올바른 환경 변수를 설정하고 프록시를 실행해야 해요:
`export LINKERD2_PROXY_IDENTITY_SERVER_ID="spiffe://root.linkerd.cluster.local/external-workload"
export LINKERD2_PROXY_IDENTITY_SERVER_NAME="external-workload.cluster.local"
export LINKERD2_PROXY_POLICY_WORKLOAD="{\"ns\":\"mixed-env\", \"external_workload\":\"external-workload\"}"
export LINKERD2_PROXY_DESTINATION_CONTEXT="{\"ns\":\"mixed-env\", \"nodeName\":\"my-vm\", \"external_workload\":\"external-workload\"}"
export LINKERD2_PROXY_DESTINATION_SVC_ADDR="linkerd-dst-headless.linkerd.svc.cluster.local.:8086"
export LINKERD2_PROXY_DESTINATION_SVC_NAME="linkerd-destination.linkerd.serviceaccount.identity.linkerd.cluster.local"
export LINKERD2_PROXY_POLICY_SVC_NAME="linkerd-destination.linkerd.serviceaccount.identity.linkerd.cluster.local"
export LINKERD2_PROXY_POLICY_SVC_ADDR="linkerd-policy.linkerd.svc.cluster.local.:8090"
export LINKERD2_PROXY_IDENTITY_SPIRE_SOCKET="unix:///tmp/spire-agent/public/api.sock"
export LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS=`cat /opt/SPIRE/crts/ca.crt`
./linkerd-proxy
`
머신에서 애플리케이션 워크로드 시작하기
이제 머신에서 프록시가 실행 중이니, 클러스터 안에서 접근할 수 있는 또 다른 워크로드를 그 위에서 시작할 수 있어요. 이 애플리케이션을 프록시가 사용하는 것과 다른 사용자 계정으로 실행해야 한다는 점을 기억하세요. bb 유틸리티를 사용해 워크로드를 흉내 내볼게요:
`docker run -p 80:80 buoyantio/bb:v0.0.5 terminus \
--h1-server-port 80 \
--response-text hello-from-external-vm
`
머신과 주고받는 암호화된 트래픽 보내기
이제 모든 것이 실행 중이니, 클러스터 내 워크로드에서 머신으로 트래픽을 보낼 수 있어요. 먼저 클라이언트를 클러스터의 워크로드로 만들어 볼게요:
`kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: client
namespace: mixed-env
---
apiVersion: v1
kind: Pod
metadata:
name: client
namespace: mixed-env
annotations:
linkerd.io/inject: enabled
spec:
volumes:
- name: shared-data
emptyDir: {}
containers:
- name: client
image: docker.io/curlimages/curl:latest
command:
- "sh"
- "-c"
- >
while true; do
sleep 3600;
done
serviceAccountName: client
EOF
`
머신과 클러스터 내 워크로드 둘 다를 선택하는 서비스도 만들 수 있어요:
`kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: legacy-app
namespace: mixed-env
spec:
type: ClusterIP
selector:
app: legacy-app
ports:
- port: 80
protocol: TCP
name: one
---
apiVersion: v1
kind: Service
metadata:
name: legacy-app-cluster
namespace: mixed-env
spec:
type: ClusterIP
selector:
app: legacy-app
location: cluster
ports:
- port: 80
protocol: TCP
name: one
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: mixed-env
name: legacy-app
spec:
replicas: 1
selector:
matchLabels:
app: legacy-app
template:
metadata:
labels:
app: legacy-app
location: cluster
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 hello-from-$POD_NAME --fire-and-forget"
ports:
- name: http-port
containerPort: 80
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
EOF
`
이제 클라이언트 pod에 ssh로 접속해 트래픽이 클러스터 내 워크로드와 머신 사이에서 로드밸런싱되는 것을 관찰할 수 있어요:
`kubectl exec -c client --stdin --tty client -n mixed-env -- sh
while sleep 1; do curl -s http://legacy-app.mixed-env.svc.cluster.local:80/who-am-i| jq .; done
{
"requestUID": "in:http-sid:terminus-grpc:-1-h1:80-571813026",
"payload": "hello-from-external-workload"
}
{
"requestUID": "in:http-sid:terminus-grpc:-1-h1:80-599832807",
"payload": "hello-from-legacy-app-d4446455b-2fgcr"
}
{
"requestUID": "in:http-sid:terminus-grpc:-1-h1:80-634437030",
"payload": "hello-from-external-workload"
}
{
"requestUID": "in:http-sid:terminus-grpc:-1-h1:80-667578518",
"payload": "hello-from-external-workload"
}
`
마찬가지로 머신에서 클러스터로 트래픽을 보낼 수도 있어요:
`while sleep 1; do curl -s http://legacy-app-cluster.mixed-env.svc.cluster.local:80/who-am-i| jq .; done
# You should start seeing responses from the in-cluster workload.
{
"requestUID": "in:http-sid:terminus-grpc:-1-h1:80-824112662",
"payload": "hello-from-legacy-app-6bb4854789-x4wbw"
}
{
"requestUID": "in:http-sid:terminus-grpc:-1-h1:80-858574572",
"payload": "hello-from-legacy-app-6bb4854789-x4wbw"
}
{
"requestUID": "in:http-sid:terminus-grpc:-1-h1:80-895218927",
"payload": "hello-from-legacy-app-6bb4854789-x4wbw"
}
`
머신과 함께 인가 정책 사용하기
머신에서 실행되는 프록시의 아이덴티티가 Kubernetes 서비스 어카운트에 묶이지는 않지만, 인가 정책을 정의하는 데 쓸 수 있는 증명된 아이덴티티는 여전히 있습니다. 우리의 클러스터 내 워크로드에 도달할 수 있는 트래픽의 종류를 제한해 볼게요. Server 리소스를 만들어 보세요:
`kubectl apply -f -
apiVersion: policy.linkerd.io/v1beta2
kind: Server
metadata:
name: in-cluster-endpoint
namespace: mixed-env
annotations:
config.linkerd.io/default-inbound-policy: "deny"
spec:
podSelector:
matchLabels:
app: legacy-app
port: http
proxyProtocol: HTTP/1
EOF
`
이제 머신에서 클러스터 내 워크로드를 대상으로 하면 응답을 더 이상 받지 못한다는 것을 관찰할 수 있어요. 기본 정책이 deny이기 때문입니다. 머신의 SPIFFE id를 허용하는 정책을 만들어 트래픽을 명시적으로 허용하면 고칠 수 있어요:
`kubectl apply -f -
apiVersion: policy.linkerd.io/v1beta2
kind: Server
metadata:
name: in-cluster-endpoint
namespace: mixed-env
annotations:
config.linkerd.io/default-inbound-policy: "deny"
spec:
podSelector:
matchLabels:
app: legacy-app
port: http
proxyProtocol: HTTP/1
apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
name: in-cluster-endpoint-authn
namespace: mixed-env
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: in-cluster-endpoint
requiredAuthenticationRefs:
- name: in-cluster-endpoint-mtls
kind: MeshTLSAuthentication
group: policy.linkerd.io
---
apiVersion: policy.linkerd.io/v1alpha1
kind: MeshTLSAuthentication
metadata:
name: in-cluster-endpoint-mtls
namespace: mixed-env
spec:
identities:
- "spiffe://root.linkerd.cluster.local/external-workload"
EOF
`
이 정책을 적용하면 머신에서 클러스터 내 워크로드로의 트래픽이 허용되는 것을 관찰할 수 있어요. 마찬가지로 Server 객체의 externalWorkloadSelector 필드를 사용해 외부 워크로드 객체에 정책을 붙일 수도 있습니다.
끝입니다
축하해요! 비-Kubernetes 워크로드를 Linkerd로 메시화했고, 그것과 클러스터의 meshed pod 사이에 안전하고 신뢰할 수 있는 통신을 시연했습니다.
더 알아보기 (Learn more)
- 메시 확장(Mesh expansion) 개념 문서
- SPIRE/SPIFFE 공식 문서