Kafka Connect docker image 만들기
이번 글에서는 Kafka Connect를 쿠버네티스(이하 K8s)에서 구축하기 위해 도커 이미지를 만드는 방법에 대해서 설명하려고 한다. 이 말을 들었을 때, "왜 이미지를 따로 만들지?"라고 생각할 수 있다. 왜냐하면 컨플루언트에서 제공하는 도커 이미지가 있기 때문이다. 하지만 필자는 직접 이미지를 만들기로 했다. 이유는 Kafka Connect 내에 따로 개발한 Connector들을 빌드할 때, 해당 Connector들을 포함한 Kafka Connect 도커 이미지를 만들고 싶었다. 그리고 현재 운영 중인 환경과 동일한 버전으로 구성된 도커 이미지를 만들어서 환경 차이에 따라 혹시 발생할지 모르는 문제를 피하고 싶었기 때문이다.
그럼 도커 이미지를 만드는 내용에 들어가기 앞서 왜 쿠버네티스 환경에서 Kafka Connect를 구축하려고 했을까?
1. Kafka Connect를 쿠버네티스에서 운영한 이유
첫 번째로는 서버 리소스 낭비를 막고 최적의 리소스를 사용하는 효율성을 갖게 된다. Kafka Connect는 외부 레파지토리와 Kafka 간의 데이터 이동을 위해 만들어진 시스템이다. Kafka Broker와 같이 많은 자원을 필요로 하지 않는다. 아래 그림 1은 컨플루언트에서 제공하는 문서에서 발췌해서 가져왔다. Kafka 컴포넌트별로 필요한 서버 자원을 보여준다.
Kafka Connect는 CPU에 구속받지 않고 많은 heap 메모리도 사용하지 않는다. 물론 서버 사양이 좋으면 데이터 처리에 도움이 되겠지만 성능이 좋은 몇 대의 서버보다 적당한 서버 여러 대가 데이터 처리에 더 도움이 된다고 생각했다. 쿠버네티스 환경에서 운영하면 처리할 데이터 양에 따라 유연하게 파드 수를 늘리거나 줄일 수 있으므로 효율성이 높다.
두 번째는 connect 클러스터에 포함된 connector들의 다양한 기능들을 하나의 connect cluster에서 실행하기보다 connector들의 성격에 맞게 분리하기 위함이다. 클러스터의 성격과 환경(운영, 테스트)에 맞게 분리를 하게 되면 운영 중인 서비스의 영향도를 최소화할 수 있다. 그리고 테스트를 위한 클러스터를 쉽게 구축할 수 있기 때문에 connector의 개발 생산성이 향상된다.
2. Docker Image 생성 시 고려한 요소
그럼 K8s에서 수행할 connect docker image는 어디서 얻을 수 있을까? docker image는 confluent에서 제공하는 image가 존재한다. 그런데 docker image를 새롭게 만들기로 결정했다. 대표적인 이유는 다음과 같다.
- 커스텀하게 개발한 connector plugin들의 빌드 결과로 새로운 plugin이 포함된 docker image를 자동으로 생성하고 싶었다. 그리고 빌드버전과 동일하게 docker image의 버전도 같이 관리하길 원했다.
- 커스텀하게 개발한 connector plugin에서 필요로 하는 설정 파일의 조정이 있었고, 계정 변경 등의 환경을 직접 컨트롤해야 했다.
하지만 docker image를 만들 때, confluent github의 confluent-docker-utils의 일부를 참고해서 사용하기도 했다. 새롭게 만든 docker image는 외부에 공개하지 않고 private 한 docker repository로 관리했다.
2.1. pod 간 통신을 위한 설정
connect는 클러스터로 구성된다. 클러스터라 함은 여러 대의 서버가 묶여서 하나의 서비스로 이루어진다는 것이다. 여러 대가 하나의 서비스로 보이기 위해서 connect도 내부에서 서로의 서버들과 연결되고 메시지를 주고받는다. 그때, 다른 서버들이 자신의 서버의 위치를 알 수 있게 IP를 설정한다. 해당 설정의 이름이 rest.advertised.host.name이며 connect 각 서버에 등록된다. 그런데 K8s의 pod는 생성될 때마다 IP가 달라지기 때문에 문제가 발생한다. pod의 IP를 pod 안에 실행한 설정 파일에 명시해줘야 하는 것이다. 아래 코드와 같이 환경 변수 값으로 pod ip를 할당받고, docker를 띄울 때 환경 변수를 확인하여 connect 설정 파일에 추가했다.
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
2.2. hdfs 권한을 위한 리눅스 유저 생성
이번에는 connect cluster를 띄울 때의 리눅스 계정 문제였다. 사실 계정이 이슈가 될 이유는 없는데, connect cluster가 정상적으로 띄워지면 task의 생성과 삭제가 모두 API로 이루어진다. 필자에게 계정이 이슈가 된 부분은 connect 포함된 자체 개발한 plugin에서 특정 계정으로 띄워야 하는 이슈가 있었기 때문이다. hdfs에 파일을 업로드하는 기능이 있었는데, hdfs의 권한 처리를 위해서 특정 계정으로 사용해야 했었기 때문이다. 그래서 docker image를 만들 때, 필수로 설치해야 하는 리눅스 패키지들과 java 같은 공통 요소들을 먼저 루트 계정으로 설치하고 계정 전환을 한 후에 connect가 포함된 kafka 코드의 다운로드와 설정 작업을 하고 실행을 하도록 했다.
2.3. log 파일 이름 중복 방지 및 pv 설정
connect log들을 생성할 때, 이름의 중복 방지를 위해서 pod ip를 log4j 설정에 넣어줬다. pod ip는 앞서 설명했던 환경 변수로 동일하게 사용했다. 그리고 log들은 pod가 정지 및 재시작으로 K8s의 node가 변경되더라도 동일한 위치에서 확인할 수 있도록 NAS 디렉터리에 PV를 연결했다. K8s service 이름으로 디렉터리를 구분하고 pod ip를 log4j 파일 이름으로 설정해서 중복을 방지할 수 있었다.
3. confluent-docker-utils 사용
confluent-docker-utils에서 사용한 부분은 환경 변수들을 connect cluster의 설정에 추가하는 부분이다. python으로 만든 코드인데 기본 템플릿 파일이 있고 환경 변수의 값을 토대로 설정 파일들의 내용을 수정하는데 도움을 유틸이었다. 필자가 많이 사용한 코드는 dub.py에 있는데 다음과 같은 대표적인 기능을 가지고 있다. connect를 위한 docker가 아니더라도 설정을 바꿔야 할 필요가 있을 때 참고하면 좋을 것 같다.
1. template : Uses Jinja2 and environment variables to generate configuration files.
2. ensure: ensures that a environment variable is set. Used to ensure required properties.
3. ensure-atleast-one: ensures that atleast one of environment variable is set. Used to ensure required properties.
4. wait: waits for a service to become available on a host:port.
5. path: Checks a path for permissions (read, write, execute, exists)
4. 결론
K8s를 통해 connect cluster를 docker로 구성하고 사용하니 운영 효율성과 개발 생산성이 많이 향상되었다. 일단 격리된 환경으로 빠르게 connect cluster 구축이 가능해서 테스트를 위한 환경 구축이 너무 편해졌다. 환경에 신경을 쓰지 않으니 커스텀 plugin 개발에 더 집중할 수 있었다. 그리고 connect cluster 사용 목적에 따라 cluster를 분리하고, 리소스를 분배할 수 있게 되면서 운영 효율성도 증대되었다. 많고 무거운 task를 수행하는 connect cluster에 리소스를 더 할당하고 간단한 cluster는 리소스를 줄이는 방식으로 효율화가 되었다. 물론 초기에 docker image를 구축하는 시간은 생각보다 오래 걸렸다. 물리 서버에서 구축된 connect는 설정을 직접 바꾸는 방식으로 간단히 수정하면 되지만 docker는 새롭게 image를 생성해야 했다. 이 부분도 커스텀하게 환경 변수를 넘기는 방식으로 수정해서 수작업도 많이 줄었다.