네트워크 정책과 CRD
- 19.1 네트워크 정책 기초
- 19.2 네트워크 정책 심화
- 19.3 CRD
- 19.4 커스텀 컨트롤러
- 19.5 Operator Framework
보안의 마지막 파트야. 네트워크 정책으로 파드 간 트래픽을 제어하는 방법부터 시작해서, CRD와 커스텀 컨트롤러로 Kubernetes를 확장하는 방법, 그리고 이걸 하나로 묶는 Operator Framework까지 살펴보자.
먼저 네트워킹과 보안의 기본 개념부터 잡고 가자. 웹 서버, API 서버, DB 서버가 있는 간단한 구성을 생각해봐. 사용자가 포트 80에서 웹 서버에 요청을 보내고, 웹 서버는 포트 5000에서 API 서버에 요청을 보내고, API 서버는 포트 3306에서 DB 서버의 데이터를 가져오는 흐름이야.
여기서 **인그레스(Ingress)**와 **이그레스(Egress)**라는 두 가지 트래픽 유형이 있어. 웹 서버 관점에서 사용자로부터 들어오는 트래픽이 인그레스, API 서버로 나가는 요청이 이그레스야. 인그레스와 이그레스를 정의할 때는 트래픽이 시작되는 방향만 봐. 응답으로 돌아오는 트래픽은 신경 쓰지 않아도 돼.
Kubernetes에서는 기본적으로 모든 파드가 클러스터 내 다른 모든 파드와 통신할 수 있어. 모든 허용(All Allow) 규칙이 기본이야. 그래서 프런트엔드 웹 파드, API 파드, DB 파드가 있을 때 기본적으로 셋 다 서로 통신할 수 있지.
프런트엔드 웹 서버가 DB 서버에 직접 통신하지 못하게 막고 싶다면? **네트워크 정책(Network Policy)**을 쓰면 돼. API 서버에서만 DB 서버로의 트래픽을 허용하는 정책을 만드는 거야.
네트워크 정책은 파드, 레플리카 세트, 서비스와 마찬가지로 Kubernetes 네임스페이스의 오브젝트야. 레이블과 셀렉터로 파드에 연결해. 파드에 레이블을 지정하고 네트워크 정책의 podSelector 필드에 같은 레이블을 사용하는 거지. 정책이 적용되면 지정된 규칙에 맞는 트래픽만 허용하고 나머지는 전부 차단해. 정책이 없는 파드는 여전히 All Allow 상태야.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-policy
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
name: api-pod
ports:
- protocol: TCP
port: 3306
policyTypes에서 Ingress인지 Egress인지 지정하고, ingress 섹션의 from에서 레이블과 셀렉터로 API 파드를 지정하고, ports에서 3306을 허용해.
한 가지 중요한 건, 모든 네트워크 솔루션이 네트워크 정책을 지원하는 건 아니야. Kube-router, Calico, Romana, Weave-net은 지원하지만, Flannel은 지원하지 않아. 네트워크 정책을 지원하지 않는 솔루션에서도 정책을 만들 수는 있지만 적용되지 않아. 오류 메시지도 안 나오니까 주의해야 해. 항상 네트워크 솔루션 문서를 확인해봐.
기본 네트워크 정책을 이해했으니, 이제 좀 더 세밀하게 구성하는 방법을 알아보자. 작은 YAML 차이가 엄청난 보안 차이를 만들 수 있어서 정확히 이해해야 해.
기본 시나리오는 같아. DB 파드를 보호해서 API 파드를 제외한 다른 파드에서는 포트 3306으로 접근하지 못하게 하려는 거야. 항상 보호하려는 파드(DB 파드)의 관점에서 생각해야 해. DB 파드 관점에서 API 파드로부터 들어오는 트래픽은 인그레스야. 그럼 DB가 쿼리 결과를 돌려보내는 건? 별도 규칙이 필요 없어. 들어오는 트래픽을 허용하면 그에 대한 응답은 자동으로 허용되거든. 어떤 유형의 규칙을 만들지 결정할 때는 요청이 시작되는 방향만 신경 쓰면 돼.
인그레스 규칙의 from 섹션에서 쓸 수 있는 셀렉터가 세 가지 있어.
첫째, podSelector로 레이블을 기준으로 파드를 선택할 수 있어. 둘째, namespaceSelector로 레이블을 기준으로 네임스페이스를 선택할 수 있어. 클러스터에 같은 레이블을 가진 API 파드가 여러 네임스페이스에 있을 때, prod 네임스페이스의 API 파드만 허용하고 싶으면 namespaceSelector를 추가해. 이 기능을 쓰려면 네임스페이스에 먼저 레이블을 설정해둬야 해. namespaceSelector만 쓰고 podSelector를 빼면 해당 네임스페이스의 모든 파드가 접근할 수 있게 돼. 셋째, ipBlock으로 IP 주소 범위를 지정할 수 있어. 클러스터 외부의 백업 서버처럼 Kubernetes 파드가 아닌 서버에서 접근을 허용할 때 쓰는 거야.
여기서 가장 중요한 부분이야. 이 셀렉터들을 어떻게 조합하느냐에 따라 AND 연산이 되기도 하고 OR 연산이 되기도 해.
from 섹션에서 각 항목 앞에 대시(-)를 붙이면 별개의 규칙이 돼. 이건 OR 연산이야. 하나라도 매칭되면 트래픽이 허용돼.
from:
- podSelector:
matchLabels:
name: api-pod
- namespaceSelector:
matchLabels:
name: prod
- ipBlock:
cidr: 192.168.5.10/32
이러면 세 가지 중 하나라도 충족하면 통과해. API 파드 레이블이 일치하는 모든 네임스페이스의 파드, 또는 prod 네임스페이스의 모든 파드, 또는 해당 IP에서 오는 트래픽이 전부 허용돼.
반면에 podSelector와 namespaceSelector를 같은 항목 안에 넣으면(namespaceSelector 앞에 대시를 안 붙이면) AND 연산이야. 둘 다 만족해야 통과해.
from:
- podSelector:
matchLabels:
name: api-pod
namespaceSelector:
matchLabels:
name: prod
이러면 prod 네임스페이스에 있으면서 동시에 api-pod 레이블이 일치하는 파드만 허용돼. 대시 하나 차이로 보안이 완전히 달라지니까 이 구분을 확실히 이해하고 있어야 해.
이그레스 규칙도 보자. DB 파드에서 외부 백업 서버로 데이터를 푸시하는 에이전트가 있다면, 트래픽이 DB 파드에서 외부로 나가는 거니까 이그레스 규칙이 필요해. policyTypes에 Egress를 추가하고, egress 섹션을 만들어. from 대신 to를 쓰는 게 유일한 차이점이야. to 아래에서도 podSelector, namespaceSelector, ipBlock 같은 셀렉터를 쓸 수 있어.
egress:
- to:
- ipBlock:
cidr: 192.168.5.10/32
ports:
- protocol: TCP
port: 80
하나의 네트워크 정책에 인그레스와 이그레스 규칙을 동시에 정의할 수도 있어. policyTypes에 둘 다 추가하고 각각의 섹션을 작성하면 돼.
네트워크 정책이 파드 간 통신을 제어하는 거였다면, 이제부터는 Kubernetes 자체를 확장하는 이야기야. 사용자 정의 리소스를 이해하려면 먼저 Kubernetes에서 리소스와 컨트롤러가 어떻게 동작하는지 알아야 해. 배포(Deployment)를 만들면 Kubernetes는 배포 오브젝트를 etcd에 저장해. 근데 실제로 파드를 만드는 건 배포 컨트롤러야. 컨트롤러는 백그라운드에서 돌아가면서 리소스의 상태를 지속적으로 모니터링하고, 배포를 생성하거나 업데이트하거나 삭제하면 그에 맞는 변경을 수행해. 배포 컨트롤러는 Kubernetes에 내장되어 있으니까 따로 만들 필요가 없지.
이제 재미있는 걸 해보자. 배포를 만들듯이, 예를 들어 항공권 객체를 만들고 싶다고 해보자. apiVersion은 flights.com/v1, kind는 FlightTicket, spec에는 출발지, 도착지, 매수 같은 속성을 넣는 거야. 이 객체를 만들면 항공권 리소스가 생성되고, 나열하면 전부 보이고, 삭제하면 예약이 취소되는 식으로 동작하길 원해.
etcd에 객체를 저장하고 조회하는 것만으로는 실제 항공권이 예약되지 않아. 실제로 항공편 예약 API를 호출해서 항공권을 예약하려면 컨트롤러가 필요해. 항공권 컨트롤러가 항공권 리소스의 생성, 업데이트, 삭제를 감시하면서 예약 API를 호출하는 거지. 이렇게 우리가 만든 항공권 객체가 커스텀 리소스이고, 예약 API를 호출하는 컨트롤러가 커스텀 컨트롤러야.
지금 바로 Kubernetes에서 FlightTicket을 만들려고 하면 오류가 나. flights.com/v1 버전에 FlightTicket이라는 리소스가 없다고. Kubernetes API에서 이런 리소스를 허용하도록 먼저 알려줘야 하거든. 이게 **CRD(Custom Resource Definition)**야.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: flighttickets.flights.com
spec:
scope: Namespaced
group: flights.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
from:
type: string
to:
type: string
number:
type: integer
minimum: 1
maximum: 10
names:
kind: FlightTicket
singular: flightticket
plural: flighttickets
shortNames:
- ft
scope는 이 객체가 네임스페이스에 속하는지 여부를 정의해. Pod처럼 네임스페이스 범위로 할 수도 있고, Node처럼 클러스터 범위로 할 수도 있어. group은 API 버전에서 제공하는 API 그룹이야. versions에서 버전 이름을 지정하고, served는 API 서버를 통해 제공할지, storage는 etcd에 저장할 버전인지를 결정해. 여러 버전이 있으면 하나만 storage로 표시할 수 있어. schema는 spec 섹션에서 지정할 수 있는 모든 매개변수를 정의하는 거야. 필드 타입과 유효성 검사도 넣을 수 있어. 예를 들어 number 아래에 minimum이나 maximum을 지정하면 범위를 벗어나는 값이 들어올 때 리소스 생성이 거부돼. names에서는 kind, singular, plural, shortNames를 지정해. plural은 kubectl api-resources에 표시되고, shortNames를 설정하면 kubectl get ft 같이 축약해서 쓸 수 있어.
kubectl create -f flightticket-crd.yaml로 CRD를 만들면, 이제 FlightTicket 객체를 생성하고 조회하고 삭제할 수 있어. 하지만 CRD만으로는 etcd에 데이터를 저장하고 CRUD만 가능할 뿐, 실제로 뭔가를 하지는 않아. 이 리소스에 대해 실제 작업을 수행하려면 커스텀 컨트롤러가 필요해.
CRD로 리소스 타입을 정의하고 객체를 만들었지만, 그것만으로는 etcd에 데이터가 저장될 뿐 아무 일도 안 일어나. 실제로 뭔가를 하려면 컨트롤러가 필요해.
컨트롤러는 루프에서 실행되는 프로세스야. Kubernetes 클러스터를 지속적으로 모니터링하면서 특정 객체(이 경우 항공권 같은 커스텀 리소스)의 생성, 수정, 삭제 이벤트를 감시하는 거야. 이벤트가 발생하면 거기에 맞는 작업을 수행해. 예를 들어 항공권 리소스가 생성되면 항공편 예약 API를 호출하고, 삭제되면 예약 취소 API를 호출하는 식으로.
코드를 작성하는 방법은 여러 가지가 있어. Python 같은 언어로도 할 수 있지만, API 호출 비용이 클 수 있고 자체 큐와 캐싱 메커니즘을 직접 만들어야 해서 어려워. Go 언어로 Kubernetes Go 클라이언트를 사용하면 Shared Informer 같은 라이브러리가 캐싱과 큐 메커니즘을 제공해줘서 컨트롤러를 올바르게 빌드하기 훨씬 쉬워.
시작하는 방법은, GitHub에 sample-controller라는 리포지토리가 있어. 이걸 클론하고 controller.go 파일을 커스텀 로직으로 수정한 다음 빌드하고 실행하면 돼. Go가 설치되어 있어야 하고.
git clone https://github.com/kubernetes/sample-controller.git
cd sample-controller
# controller.go 커스터마이즈
go build -o sample-controller .
./sample-controller -kubeconfig=$HOME/.kube/config
실행하면 컨트롤러가 kubeconfig 파일을 사용해서 Kubernetes API에 인증하고, 커스텀 리소스의 변경을 감시하면서 필요한 작업을 수행해.
컨트롤러가 준비되면 배포 방법을 결정해야 해. 매번 직접 빌드하고 실행하는 대신, Docker 이미지로 패키징한 다음 Kubernetes 클러스터 안에서 Pod이나 Deployment로 실행할 수 있어. 이 방식이 더 편하고 권장되는 방법이야.
시험에서는 커스텀 컨트롤러를 직접 만드는 문제는 나오지 않을 거야. 코딩 지식이 필요하니까. 하지만 CRD를 작성하거나, 기존에 있는 컨트롤러와 함께 작업하는 문제는 나올 수 있으니까 개념은 알아두는 게 좋아.
CRD와 커스텀 컨트롤러를 따로 만들고 배포하는 방식을 봤는데, 이 두 엔티티를 하나로 패키징해서 단일 엔티티로 배포할 수 있어. 이게 Operator Framework야.
Operator를 배포하면 내부적으로 CRD와 커스텀 리소스를 생성하고, 커스텀 컨트롤러도 Deployment로 배포해. 다 알아서 해주는 거지.
Operator는 이 두 가지를 합친 것 이상의 역할을 해. 보통 사람이 하는 운영 작업을 자동화하는 거야. 애플리케이션 설치, 백업 생성, 재해 발생 시 백업 복원, 문제 해결, 버전 업그레이드 같은 것들. 실제 사용 사례를 보면 이해가 쉬워.
가장 대표적인 게 etcd Operator야. Kubernetes 안에서 etcd 클러스터를 배포하고 관리하는 데 쓰여. EtcdCluster CRD가 있고, 이 리소스를 감시하는 커스텀 컨트롤러가 etcd를 배포해. 단순히 배포만 하는 게 아니라 etcd 클러스터의 백업을 만드는 백업 오퍼레이터, 백업을 복원하는 복원 오퍼레이터 같은 추가 코드도 포함하고 있어.
다양한 애플리케이션의 Operator는 **OperatorHub(operatorhub.io)**에서 찾을 수 있어. etcd, MySQL, PostgreSQL, Prometheus, Grafana, Argo CD, Istio, Redis, MongoDB 같은 유명한 애플리케이션들의 Operator가 이미 만들어져 있어. 설치도 간단한데, 먼저 **OLM(Operator Lifecycle Manager)**을 설치하고, 그 다음 Operator를 설치하면 끝이야. 복잡한 애플리케이션 배포가 두세 단계로 줄어드는 거지.
시험에서는 Operator 자체에 대한 질문은 나오지 않을 거야. CKA 커리큘럼에서는 주로 CRD를 다루니까. Operator는 이런 게 있다는 정도로 알아두면 충분해.
정리
19장 읽고 기억할 거 세 가지:
- 네트워크 정책의 AND vs OR: from 섹션에서 대시(
-) 하나 차이로 셀렉터가 OR 연산(별개 규칙)이 되기도 하고 AND 연산(동일 규칙 내 조합)이 되기도 한다. 이 구분이 보안에 직접적인 영향을 준다. - CRD = API 확장, 컨트롤러 = 실제 동작: CRD는 Kubernetes API에 새 리소스 타입을 등록하는 것일 뿐이고, 실제 비즈니스 로직을 수행하려면 커스텀 컨트롤러가 반드시 필요하다.
- Operator = CRD + 커스텀 컨트롤러 + 운영 자동화: Operator Framework는 둘을 하나로 패키징해서 배포하고, 백업/복원/업그레이드 같은 Day-2 운영까지 자동화한다. OperatorHub에서 검증된 Operator를 가져다 쓸 수 있다.