배경
쿠버네티스가 컨테이너 오케스트레이션 도구로 널리 사용됨에 따라, 실무에서는 몇 가지 문제에 직면하게 되었다. 예를 들어, 간단한 웹서비스를 쿠버네티스에 구축할 경우, 사용자는 Ingress, Deployment, Service와 같은 다양한 쿠버네티스 리소스를 정의하고, 필요한 스펙을 설정해야 한다. 더 나아가 오토스케일링을 구현하려면 별도의 설정을 해야 하며, Canary 또는 Blue-Green과 같은 고급 배포 전략을 적용하기 위해서는 추가적인 솔루션을 설치하고 구성해야 하는 번거로움이 있다. 이러한 설정은 현업 개발자에게 상당한 부담을 주게 된다.
또한 서버리스 서비스를 이용하여 간단한 이벤트 기반 서비스를 구축하는 경우, AWS Lambda나 GCP의 Cloud Functions과 같은 서비스를 사용하는데, 이러한 서비스들은 특정 클라우드 플랫폼에 대한 의존성을 갖게 된다.
이러한 문제들을 해결하기 위해 등장한 서버리스 솔루션이 Knative이다.
Knative는 쿠버네티스를 기반으로 동작하는 서버리스 도구로, 배포의 복잡성을 줄여 개발자가 서비스 개발에 집중할 수 있게 도와주고, 특정 클라우드 종속성이 없어 다양한 클라우드 환경에서의 유연성을 향상시킨다.
Knative란?
Knative는 쿠버네티스 위에서 서버리스 애플리케이션을 빌드, 배포 및 관리하기 위한 오픈 소스 도구이다.
Knative의 개발은 Google이 주도하였으며, IBM, Pivotal, Red Hat 등 여러 기업들이 협력하여 공동으로 개발하였다. 2022년 3월 2일부로 CNCF 재단에 Incubating 프로젝트로 등재되어 활발한 커뮤니티 활동이 진행 중이다.
그렇다면 Knative는 왜 서버리스라고 불리는가?
서버리스는 개발자가 인프라 관리에 대해 걱정할 필요 없이 애플리케이션을 배포하고 실행할 수 있다는 의미이다. Knative는 Ingress, Deployment, Service 리소스를 자동으로 생성해 줄 뿐만 아니라 리소스 요청이 있을 때만 실행되고 특정 시간 동안 요청이 없으면 종료된다는 특징이 있다.
Knative는 주로 두 가지 구성 요소로 구성된다.
- Serving: 애플리케이션의 배포, 스케일링, 트래픽 관리를 자동화한다.
- Eventing: 다양한 이벤트 소스와 애플리케이션 사이의 이벤트를 쉽게 연결하고 처리할 수 있다.
참고
이번 글에서는 Serving에 대한 내용을 다루고, 다음 글에서 Eventing에 대한 내용을 다룰 예정이다.
다음은 Serving의 Resource에 대한 그림이다.
그림 참고 : https://knative.dev/docs/serving/
- Service : 모든 작업의 전체 lifecycle을 자동으로 관리한다. 트래픽을 항상 최신 Revision으로 라우팅 되도록 설정할 수 있고, 특정 Revision으로 라우팅 되도록 설정할 수도 있다.
- Route : 서비스로 들어오는 트래픽을 하나 이상의 Revision에 라우팅 한다. 최신 Revision으로 라우팅 하거나 다른 Revision으로 라우팅 되도록 트래픽을 관리할 수도 있다. Route는 직접 업데이트해서는 안되고, 모든 변경 사항은 서비스 컨트롤러에 의해 재설정된다.
- Configuration : Knative Serving으로 배포되는 서비스를 정의한다. Configuration을 수정하면 새로운 Revision이 생성된다. Configuration은 직접 업데이트해서는 안되고, 모든 변경 사항은 서비스 컨트롤러에 의해 재설정된다.
- Revision : 코드와 Configuration의 스냅샷으로 Configuration이 업데이트되면 생성된다. Revision은 들어오는 트래픽에 따라 자동으로 확장 및 축소될 수 있으며, 직접 업데이트해서는 안되고, Configuration 업데이트에 의해 생성되어야 한다.
다음 그림은 Knative Serving의 Diagram이다.
그림 참고: https://knative.dev/docs/serving/architecture/#diagram
Knative Serving의 구성 요소는 다음과 같다.
- Activator : 서비스가 zero- scaling 된 경우 들어오는 요청을 대기시키고, autoscaler와 통신하여 zero scaling 된 서비스를 다시 활성화하고 대기 중인 요청을 전달한다. 또한 방대한 트래픽을 처리하기 위한 요청 버퍼로도 사용된다.
- Autoscaler : 들어오는 요청에 기반하여 Pod 수를 조정한다.
- Controller : Knative 리소스의 상태를 관리한다. 여러 개체를 감시하고 종속 리소스의 수명 주기를 관리하며 리소스 상태를 업데이트한다.
- Net-istio : Istio 서비스 메시의 네트워크 구성을 관리하는 역할을 담당한다.
- Webhook : Knative 리소스에 대한 유효성을 검사하고 변경한다.
- Queue-Proxy : Knative 서비스의 Pod에 있는 사이드카 컨테이너이며, 필요한 경우 Activator와 유사하게 대기열로 작동한다.
본 글에서는 Knative를 구성하고, 실제 어떻게 애플리케이션이 배포되는지 실습을 통해 알아본다.
실습은 Knative에서 제공하는 YAML 파일을 사용하여 구성하고, 네트워크 계층은 Istio를 사용하여 구성한다.
구성 환경
- Amazon EKS v1.27.7
구성 버전
- Knative v1.13.1
- Istio v1.20.3
전제 조건
- EKS Add-on
- AWS LoadBalancer Controller v2.6.1
- External DNS v0.13.6
실습 절차
1. Knative Serving 구성요소 설치2. Knative 네트워크 계층 구성 (Istio)
3. Knative Service 생성(ksvc)
4. Knative 서비스 호출 및 분석
5. Scale to Zero 테스트
1. Knative Serving 구성요소 설치
Knative Serving에 필요한 CRD를 설치한다.
$ kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.13.1/serving-crds.yaml
Knative Serving에 필요한 핵심 구성요소를 설치한다.
$ kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.13.1/serving-core.yaml
정상 설치되었는지 확인한다.
# Knative CRD
$ kubectl get crd | grep knative
certificates.networking.internal.knative.dev 2024-04-09T08:56:51Z
clusterdomainclaims.networking.internal.knative.dev 2024-04-09T08:56:51Z
configurations.serving.knative.dev 2024-04-09T08:56:51Z
domainmappings.serving.knative.dev 2024-04-09T08:56:51Z
images.caching.internal.knative.dev 2024-04-09T08:56:52Z
ingresses.networking.internal.knative.dev 2024-04-09T08:56:51Z
metrics.autoscaling.internal.knative.dev 2024-04-09T08:56:51Z
podautoscalers.autoscaling.internal.knative.dev 2024-04-09T08:56:51Z
revisions.serving.knative.dev 2024-04-09T08:56:51Z
routes.serving.knative.dev 2024-04-09T08:56:51Z
serverlessservices.networking.internal.knative.dev 2024-04-09T08:56:51Z
services.serving.knative.dev 2024-04-09T08:56:51Z
# Knative 서비스
$ kubectl get po -n knative-serving
NAME READY STATUS RESTARTS AGE
activator-7dbd76fdd8-ml76g 1/1 Running 0 23h
autoscaler-649fb5c98f-wbqhj 1/1 Running 0 23h
controller-6856cc8fc-7x5sg 1/1 Running 0 23h
net-istio-controller-7b8ff5fffc-slg56 1/1 Running 0 23h
net-istio-webhook-796694d98c-7qlzl 1/1 Running 0 23h
webhook-9cfc4b9d5-m76ft 1/1 Running 0 23h
2. Knative 네트워크 계층 구성 (Istio)
Knative 네트워크 계층에서는 Contour, Kourier 및 Istio와 같은 여러 네트워킹 계층을 지원한다.
해당 글에서는 Istio를 사용한다.
다음 명령을 통해 Istio를 설치한다.
$ kubectl apply -l knative.dev/crd-install=true -f https://github.com/knative/net-istio/releases/download/knative-v1.13.1/istio.yaml
$ kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-v1.13.1/istio.yaml
$ kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-v1.13.1/net-istio.yaml
설치 후 Istio의 Ingress gateway 서비스가 정상 설치되었는지 확인한다.
$ kubectl --namespace istio-system get service istio-ingressgateway
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
istio-ingressgateway LoadBalancer 172.20.227.200 <None> 15021:31017/TCP,80:30500/TCP,443:31103/TCP 9d
Istio의 Ingress gateway 서비스를 수정하여 AWS LoadBalancer 생성 및 Route53에 도메인 등록 및 맵핑이 되게 한다.
$ kubectl edit svc -n istio-system istio-ingressgateway
apiVersion: v1
kind: Service
metadata:
annotations:
# Knative에서 사용할 와일드카드 레코드 입력
# A, CNAME 레코드 구성해야 함
external-dns.alpha.kubernetes.io/hostname: '*.knative.example.com'
# Istio Ingress gateway에서 사용할 LoadBalancer 정보 입력
service.beta.kubernetes.io/aws-load-balancer-attributes: load_balancing.cross_zone.enabled=true
service.beta.kubernetes.io/aws-load-balancer-internal: "true"
service.beta.kubernetes.io/aws-load-balancer-name: istio-ingressgateway
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-subnets: <서브넷 입력>
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: stickiness.enabled=false
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/load-balancer-source-ranges: 0.0.0.0/0
#... 생략
:wq
실제 AWS 웹 콘솔에서 LoadBalancer 생성 및 Route53에 레코드 생성 및 LoadBalancer에 맵핑이 정상으로 되었는지 확인한다.
- AWS Istio LoadBalancer 설정
- AWS Route53 설정
이 전 과정이 전부 정상으로 설정되었으면, Knative가 해당 도메인을 사용하도록 설정한다.
$ kubectl patch configmap/config-domain \
--namespace knative-serving \
--type merge \
--patch '{"data":{"knative.example.com":""}}'
3. Knative Service 생성 (ksvc)
Knative 구성이 완료되었으면 Knative Service(ksvc)를 생성한다.
다음 YAML 형식의 Knative Service 리소스를 배포한다.
해당 애플리케이션은 호출하면 "Go Sample v1"라는 메세지 응답을 주는 애플리케이션이다.
$ kubectl apply -f - <<EOF
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: helloworld-go
namespace: default
spec:
template:
spec:
containers:
- image: ghcr.io/knative/helloworld-go:latest
env:
- name: TARGET
value: "Go Sample v1"
EOF
배포한 Knative Service 컨테이너 스펙에 맞게 Deployment, Service가 생성된 걸 확인할 수 있다.
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 80/TCP 119d
helloworld-go ExternalName <none> knative-local-gateway.istio-system.svc.cluster.local 80/TCP 14m
helloworld-go-00001 ClusterIP 172.20.198.225 <none> 80/TCP,443/TCP 14m
helloworld-go-00001-private ClusterIP 172.20.30.178 <none> 80/TCP,443/TCP,9090/TCP,9091/TCP,8022/TCP,8012/TCP 14m 443/TCP 136d
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
helloworld-go-00001-deployment 1/1 1 1 14m
Knative Service 이름으로 Route 정보를 확인한다.
$ kubectl get route helloworld-go -oyaml
apiVersion: serving.knative.dev/v1
kind: Route
metadata:
annotations:
serving.knative.dev/creator: kubernetes-admin
serving.knative.dev/lastModifier: kubernetes-admin
#...생략
spec:
traffic:
- configurationName: helloworld-go
latestRevision: true
percent: 100
#...생략
마지막 Revision으로 100% 트래픽을 전달한다는 내용을 확인할 수 있다.
Revision과 configurations 내용을 확인해 보면 동일한 내용이고, spec 내용을 보면 배포된 컨테이너에 대한 정보가 들어가 있는 것을 확인할 수 있다.
$ kubectl get revisions.serving.knative.dev helloworld-go-00001 -oyaml
$ kubectl get configurations.serving.knative.dev helloworld-go -oyaml
apiVersion: serving.knative.dev/v1
kind: Revision
metadata:
annotations:
serving.knative.dev/creator: kubernetes-admin
serving.knative.dev/routes: helloworld-go
#... 생략
name: helloworld-go-00001
namespace: default
spec:
containerConcurrency: 0
containers:
- env:
- name: TARGET
value: Go Sample v1
image: ghcr.io/knative/helloworld-go:latest
name: user-container
readinessProbe:
successThreshold: 1
tcpSocket:
port: 0
resources: {}
enableServiceLinks: false
timeoutSeconds: 300
#... 생략
4. Knative 서비스 호출 및 분석
앞서 배포한 Knative 서비스(ksvc)로 생성된 Istio 리소스에 의해 트래픽이 전달되는데, 이 과정을 확인해 보자.
우선, ksvc 리소스를 조회해 보면 URL정보를 확인하여 웹브라우저에서 접근할 수 있다.
$ kubectl get ksvc
NAME URL LATESTCREATED LATESTREADY READY REASON
helloworld-go http://helloworld-go.default.knative.example.com helloworld-go-00002 helloworld-go-00002 True
웹 브라우저에서 다음과 같은 메세지를 확인할 수 있다.
실제로 웹브라우저 호출 시 어떻게 Pod로 전달되는지 확인해 보자.
Knative에서 사용하는 Gateway 설정을 확인해 보면 다음과 같이 Istio의 gateway를 사용하는 걸 확인할 수 있다.
$ kubectl get gateway -n knative-serving knative-ingress-gateway -oyaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
#... 생략
name: knative-ingress-gateway
namespace: knative-serving
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- '*'
port:
name: http
number: 80
protocol: HTTP
ksvc를 배포하면서 생성된 Istio의 VirtualService를 확인해 보면 다음과 같이 Gateway로 들어오는 트래픽 중 Route53에 등록한 도메인을 호출하면 "helloworld-go-00002"라는 서비스로 향하는 것을 확인할 수 있다.
$ kubectl get virtualservices.networking.istio.io helloworld-go-ingress -oyaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
#...생략
name: helloworld-go-ingress
namespace: default
#...생략
spec:
gateways:
- knative-serving/knative-ingress-gateway
- knative-serving/knative-local-gateway
hosts:
- helloworld-go.default
- helloworld-go.default.knative.example.com
- helloworld-go.default.svc
- helloworld-go.default.svc.cluster.local
http:
#...생략
- match:
- authority:
prefix: helloworld-go.default.knative.example.com
gateways:
- knative-serving/knative-ingress-gateway
retries: {}
route:
- destination:
host: helloworld-go-00002.default.svc.cluster.local
port:
number: 80
headers:
request:
set:
Knative-Serving-Namespace: default
Knative-Serving-Revision: helloworld-go-00002
weight: 100
"helloworld-go-00002" 서비스에 대한 엔드포인트를 추적해 보면 knative의 activator로 연결되는 것을 확인할 수 있다.
$ kubectl get ep helloworld-go-00002
NAME ENDPOINTS AGE
helloworld-go-00002 10.10.2.239:8012,10.10.2.239:8112 32m
$ kubectl get po -o wide -A | grep 10.10.2.239
knative-serving activator-7dbd76fdd8-gdmbd 1/1 Running 0 4h58m 10.10.2.239
activator의 로그를 확인해 보면 최초 호출 시 다음과 같은 메세지를 확인할 수 있다.
$ kubectl logs -n knative-serving deploy/activator
#... 생략
{"severity":"INFO","timestamp":"2024-04-22T14:57:25.767965902Z","logger":"activator","caller":"net/throttler.go:331","message":"Updating Revision Throttler with: clusterIP = <nil>, trackers = 1, backends = 1","commit":"41769de","knative.dev/controller":"activator","knative.dev/pod":"activator-7dbd76fdd8-gdmbd","knative.dev/key":"default/helloworld-go-00002"}
{"severity":"INFO","timestamp":"2024-04-22T14:57:25.768006717Z","logger":"activator","caller":"net/throttler.go:323","message":"Set capacity to 2147483647 (backends: 1, index: 0/1)","commit":"41769de","knative.dev/controller":"activator","knative.dev/pod":"activator-7dbd76fdd8-gdmbd","knative.dev/key":"default/helloworld-go-00002"}
5. Scale to Zero 테스트
Knative에서 0으로 scaling 되는 설정은 Knative Pod Autooscaler(KPA)를 사용해야 활성화할 수 있고 해당 설정은 "기본값"으로 되어 있다. 만약 변경하고 싶다면 다음과 같이 config-autoscaler의 ConfigMap에서 "enable-scale-to-zero" 값을 false로 변경하면 된다.
apiVersion: v1
kind: ConfigMap
metadata:
name: config-autoscaler
namespace: knative-serving
data:
enable-scale-to-zero: "false"
Knative 특징 중 리소스 요청이 있을 때만 실행되고 특정 시간 동안 요청이 없으면 종료된다고 앞서 언급했다.
그럼 실제로 얼마나 요청이 없어야 종료되는지 확인해 보자.
실제 ksvc를 배포 후 아무 호출을 안 한 뒤 종료되는 시간을 체크한다.
$ kubectl get po -w
NAME READY STATUS RESTARTS AGE
helloworld-go-00003-deployment-5c7c76c7df-6l4pj 2/2 Running 0 2s
# 종료
helloworld-go-00003-deployment-5c7c76c7df-6l4pj 2/2 Terminating 0 62s
약 "60초" 후에 종료되는 것을 확인할 수 있다. 이 값은 기본값이다.
종료 간격은 다음 설정을 통해 변경할 수 있다.
- ConfigMap은 전역설정, ksvc는 Revision별 설정이다.
# 전역 설정
apiVersion: v1
kind: ConfigMap
metadata:
name: config-autoscaler
namespace: knative-serving
data:
stable-window: "60s"
---
# Revision별 설정
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: helloworld-go
namespace: default
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/window: "60s"
ksvc 설정에서 "30s"로 변경 후 호출이 없으면 30초 후에 종료되는지 확인해 보자. 다음 ksvc설정을 배포한다.
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: helloworld-go
namespace: default
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/window: "30s"
다음과 같이 30초 후에 종료되는 것을 확인할 수 있다.
$ kubectl get po -w
NAME READY STATUS RESTARTS AGE
helloworld-go-00004-deployment-59c66548f7-bv9jd 2/2 Running 0 2s
helloworld-go-00004-deployment-59c66548f7-bv9jd 2/2 Terminating 0 32s
웹브라우저를 통해 종료된 Pod를 다시 호출하면 생성되는지 확인한다.
$ kubectl get po -w
NAME READY STATUS RESTARTS AGE
# 30초 호출 없으면 Scale to Zero
helloworld-go-00004-deployment-59c66548f7-bv9jd 2/2 Running 0 2s
helloworld-go-00004-deployment-59c66548f7-bv9jd 2/2 Terminating 0 32s
helloworld-go-00004-deployment-59c66548f7-bv9jd 1/2 Terminating 0 60s
helloworld-go-00004-deployment-59c66548f7-bv9jd 0/2 Terminating 0 62s
helloworld-go-00004-deployment-59c66548f7-bv9jd 0/2 Terminating 0 62s
helloworld-go-00004-deployment-59c66548f7-bv9jd 0/2 Terminating 0 62s
helloworld-go-00004-deployment-59c66548f7-bv9jd 0/2 Terminating 0 62s
# 웹브라우저 호출 시 Pod 생성
helloworld-go-00004-deployment-59c66548f7-gvf7m 0/2 Pending 0 0s
helloworld-go-00004-deployment-59c66548f7-gvf7m 0/2 Pending 0 0s
helloworld-go-00004-deployment-59c66548f7-gvf7m 0/2 ContainerCreating 0 0s
helloworld-go-00004-deployment-59c66548f7-gvf7m 1/2 Running 0 1s
helloworld-go-00004-deployment-59c66548f7-gvf7m 2/2 Running 0 1s
댓글