CNI는 컨테이너 간 네트워킹을 관리하기 위한 표준 인터페이스이다. 주로 컨테이너 오케스트레이션 플랫폼에서 사용한다. 컨테이너 오케스트레이션 플랫폼이라하면 Docker swarn, 아마존의 ECS 그리고 지금 공부하고 있는 Kubernetes 등이 있다.
하지만 사실 CNI는 컨테이너 런타임 레이어에 해당한다. 즉 도커 컨테이너와 관련이 있다는 것이다.
CNI는 다양한 컨테이너 오케스트레이션들과 다양한 컨테이너 런타임에서 중복을 피하기 위해 네트워크 플러그인과 컨테이너 실행 사이에 공통 인터페이스가 필요하기 때문에 만들어졌다.
하나의 클러스터 내에 파드들은 각각 ip를 가지고 있다. 이 ip는 로컬 ip로 접근하여 파드끼리 접근이 가능하다. 하지만 파드는 stateless하므로 ip도 바뀔 수 있다.
이 때 CNI가 파드끼리의 네트워크 라우팅을 해준다.
컨테이너가 생성되면 ADD, DELETE, CHECK 등 CNI 규격에 맞는 명령어를 실행하여 파드끼리 어디로 요청을 보낼지 설정해준다.
또 CNI는 어떤 요청을 받아들이고 어떤 요청을 거부할지 정할 수 있게 된다. 이를 Kubernetes 구성 요소에서는 NetworkPolicy라고 하는데, 만약 단순히 생각하자면 이건 AWS의 Security Group의 역할로 보인다.
하지만 다시 한번 잘 생각해보자.
Pod1과 Pod2라는 두 개의 파드가 있고 Pod1은 데이터베이스에 접근이 불가능하지만, Pod2는 데이터베이스에 접근이 가능하다고 생각해보자.
이렇게 구름모양으로 묶인 것은 노드이다. 즉 EC2이므로 위 EC2에 Security Group을 통해서 Pod1이 있는 노드는 접근을 막고 Pod2가 있는 노드는 접근을 허용하는 것이다. 하지만 쿠버네티스에서 파드와 노드는 이렇게 이상적으로만 생겨나진 않는다.
언젠가 파드가 파괴되고 재성성되었는데,
이렇게 Pod1과 Pod2가 각각 떨어져버린다면 Security Group이 애매해진다. Security Group의 범위는 EC2 인스턴스 하나에 해당되는데 말이다.
AWS EKS에서는 Pod Security Group으로 이를 해결한다. 즉 이게 아마존의 CNI에 해당이 되는데 파드마다 Security Group을 설정할 수 있게 해주는 것이다.
CNI의 종류에는
등이 있다.
이러한 이유로 CNI가 필요해졌고, 간단히 실습 및 테스트를 진행해보자.
가장 먼저 클러스터를 생성한다.
eksctl create cluster \
--name netpol-test \
--nodegroup-name ng-default \
--node-type t3.small \
--nodes 2
그 다음으론 3개의 네임스페이스 내에 같은 형태지만 다른 nginx 파드를 하나씩 실행시킬 것이다.
그리고 network-policy를 통해서 a -> c의 접근을 허용하고, b->c의 접근은 거부하는 형태를 만들어 볼 예정이다.
# namespaces.yaml
apiVersion: v1
kind: Namespace
metadata:
name: namespace-a
---
apiVersion: v1
kind: Namespace
metadata:
name: namespace-b
---
apiVersion: v1
kind: Namespace
metadata:
name: namespace-c
먼저 세 개의 네임스페이스를 만들어준다.
# nginx-deployments.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-a
namespace: namespace-a
spec:
replicas: 1
selector:
matchLabels:
app: nginx-a
template:
metadata:
labels:
app: nginx-a
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-b
namespace: namespace-b
spec:
replicas: 1
selector:
matchLabels:
app: nginx-b
template:
metadata:
labels:
app: nginx-b
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-c
namespace: namespace-c
spec:
replicas: 1
selector:
matchLabels:
app: nginx-c
template:
metadata:
labels:
app: nginx-c
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
그리고 각 네임스페이스에 nginx 파드를 하나씩 넣어준다. namepspace-a,b,c에 각각 nginx-a,b,c를 생성해준다.
kubectl apply -f namespaces.yaml
kubectl apply -f nginx-deployments.yaml
그리고 apply
커맨드를 통해 실행해준다.
이제 접근 대상이 될 c의 ip 주소를 가져오자.
kubectl get pod -n namespace-c -o wide
ip 주소를 알게 되었다면 namespace-a의 파드에 접근해보자. Docker의 exec
처럼 kubectl도 exec
를 통해 파드에 접근할 수 있다.
kubectl exec -it deployment/nginx-a -n namespace-a -- curl 192.168.89.245
kubectl exec -it deployment/nginx-b -n namespace-b -- curl 192.168.89.245
이렇게 접근을 해준다. deployment말고 파드의 이름을 가지고 와서 접근해도 된다.
그리고 접근이 되면
curl <namespace-c의-ip주소>
이렇게 요청을 보냈을 때 nginx의 index.html이 잘 오는지 확인해보면 된다.
아직 network policy를 설정하지도 않았으니 잘 가져온다. 이제 network policy를 설정해보자.
아래와 같이 Network Policy 매니페스트 파일을 작성해준다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: namespace-c
spec:
podSelector:
matchLabels:
app: nginx-c
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: namespace-a
위 파일은 NetworkPolicy라는 Kind를 가지게 된다. 생성 이후에는
kubectl get networkpolicy
kubectl get netpol
이라는 커맨드로 확인할 수 있다. 물론 네임 스페이스 옵션을 추가로 주어야한다.
그리고 파드중에 nginx-c라는 이름을 가진 파드들을 대상으로 네트워크 요청에 대해서 제한을 거는데, Ingress와 Egress 타입을 설정할 수 있다.
Ingress는 들어오는 요청, Egress는 나가는 요청을 선택할 수 있다. 선택가능한 옵션 범위로는 위 파일처럼 namespace를 통해서 설정할 수 있고 cidr를 통해서 설정할 수 도 있다.
자세한 옵션은 공식문서에서 확인할 수 있다. 위 파일은 namespace-a에 대한 ingress만을 허용하는 namespace-c 내부 nginx-c 파드의 Network Policy에 해당한다.
위 파일을 작성 하고
kubectl apply -f network-policy.yaml
로 적용해주고, 다시 nginx 파드들에서 curl 요청을 nginx-c로 보내보자.
nginx-a, nginx-b 에서 curl을 보내면 여전히 둘 다 요청이 보내지는 것을 확인할 수 있다. 위 Network Policy를 통해 namespace-a에서의 ingress만 받도록 했는데, 적용이 안된 것이다. 이유는 바로 CNI 플러그인이 제대로 작동되지 않기 때문이다.
일반적인 플레인 쿠버네티스에서는 CNI 플러그인을 설치해주어야 한다. 위에서 설명했던 여러 종류의 CNI들이 있다. 하지만 AWS EKS에는 기본적으로 AWS VPC CNI가 설치되어 있다. 하지만 설치 되었음에도 이게 작동되지 않는 이유는 옵션이 설정되어 있지 않기 때문이다.
웹 콘솔로 이동해서 클러스 안에 Add-On을 보면
이렇게 Amazon VPC CNI가 활성화되어 있는것을 볼 수 있다. 만약 없다면 Get more add-ons에서 Amazon VPC CNI를 찾아서 추가해주면 된다. 이 add-ons에는 kubecost, kafka, grafana 등 EKS와 함께 사용할 수 있는 다양한 애드온들이 있다.
활성화를 시키고 Amazon VPC CNI에 들어가서 우측 상단 Edit 버튼을 눌러서
이 화면이 보이면 Configuration Values에
{"enableNetworkPolicy": "true"}
이 부분을 작성해서 적용해준다.
그리고 다시 nginx-a, nginx-b에서 nginx-c로 curl을 보내면 nginx-a는 정상적으로 nginx의 index.html을 받아오지만 nginx-b는 요청이 도달하지 않고 타임아웃되는 것을 확인할 수 있다.
이렇게 웹 콘솔에서 적용하지 않고 터미널에서
aws eks update-addon --cluster-name netpol-test --addon-name vpc-cni --configuration-values '{"enableNetworkPolicy": "true"}'
이런 명령어를 실행해줘도 된다.
CNI 플러그인은 다양한 종류가 있고 꼭 Amazon VPC CNI를 사용하지 않고 다른 플러그인을 사용해도 되는데, 대표적으로 Calico가 있다.
Amazon VPC CNI를 사용하면 노드의 CIDR가 그대로 파드로 분배되므로 통신의 복잡성이 줄고 속도가 빨라지지만 Calico를 사용하면 노드와 파드의 네트워크 대역이 달라진다.
대신 이렇게 하면 네트워크가 격리 되어 보안적으로 이점이 있고 IP 대역의 충돌을 막을 수 있다.
하지만 VPC CNI는 IP 대역이 빠르게 찬다는 단점이 있을 수 있다. 이 부분은 클러스터 생성 이전에 따로 파드의 CIDR를 나누는 방법이 있다고 한다.
참고
EKS AWS VPC CNI
https://faun.pub/choosing-your-cni-with-aws-eks-vpc-cni-or-calico-1ee6229297c5