작성일: 4월 15일
Kubernetes를 사용하면서 만나는 문제 상황
Kubernetes service resource를 삭제하고 다시 생성하면, Public IP(또는 EXTERNAL-IP) 값이 변경된다.
AWS, Oracle Cloud, Azure, GCP(Google Cloud Platform) 같은 Public Cloud Infra에서 제공하는
Reserved Public IP 기능과 Service YAML에 'externalIPs' 값을 설정하면 Public IP Address를 고정시킬 수 있지만,
Helm Chart를 작성하거나 Kubernetes Service Manifest 파일을 작성할 때마다
- Reserved Public IP 생성 요청
- 위에서 생성된 Public IP 값을 확인 후 Service Manifest 작성(또는 Helm chart) 작성
한다는 것은 참 불편한 작업이다.
그래서 아래의 기능을 수행하는 Python script를 만들었다.
- my-conf.yaml 파일에서 Public IP address 정보를 갱신할 'Kubernetes service' 정보와 'Internet domain name' 목록을 설정
- DNS 서버에서 Kubernetes API server에게 Service resource의 public IP address를 조회 요청
- 이렇게 얻은 public IP address(즉, EXTERNAL-IP)를 자동으로 DNS 서버의 Zone file에 반영
- BIND9 서비스 데몬을 재기동
아래 예제 설정과 Python 소스 코드를 이용하면 잘 동작함 ^^
설정 파일 작성 [ 파일명: my-conf.yaml ]
$ cat my-conf.yaml
zone_file: /var/cache/bind/my-domain.kr.zone
json_file: /var/cache/bind/my-domain.kr.json
domain_name: my-domain.kr
services:
- kube_svc_name: almighty
host_name: almighty
- kube_svc_name: myhappyserver
host_name: myhappynacserver
- kube_svc_name: my-ingress-nginx-controller
host_name: myproxyserver
BIND9 Zone File 생성을 위한 Jinja2 Template 파일 작성 [ 파일명: zone.j2 ]
$ cat zone.j2
; ------------------------------------------------------------------------------------------
; NOTE:
; 이 파일은 대한민국 서울에 사는 Andrew Jeong이 만든 "DNS Auto Configuration Application"이
; 자동으로 생성한 파일입니다.
; 사용자가 이 파일을 직접 편집해도 일정 시간 이후에 Application에 의해서 재작성될 것입니다.
; 따라서 이 Zone file을 직접 편집하는 것을 금합니다.
; 만약, Zone file에 반영할 내용이 있다면 같은 폴더에 있는 JSON 파일을 수정해야 합니다.
; ------------------------------------------------------------------------------------------
; Internet Domain Zone : {{ origin }}
; Exported date : {{ now }}
$ORIGIN {{ origin }}
$TTL {{ ttl }}
;
; SOA Record
;
@ IN SOA {{ soa['mname'] }} {{ soa['rname'] }} (
{{ soa['serial'] }} ; serial
{{ soa['refresh'] }} ; refresh
{{ soa['retry'] }} ; retry
{{ soa['expire'] }} ; expire
{{ soa['minimum'] }} ; minimum ttl
)
{% if ns is defined -%}
;
; NS Records
;
{% for entry in ns -%}
@ IN NS {{ entry['host'] }}
{% endfor %}
{% endif %}
{%- if mx is defined -%}
;
; MX Records
;
{% for entry in mx -%}
@ IN MX {{ entry['preference'] }} {{ entry['host'] }}
{% endfor %}
{% endif %}
{%- if spf is defined -%}
; SPF Records
{% for entry in spf -%}
{{ entry['domain'] }} IN TXT "{{ entry['record'] }}"
{% endfor %}
{% endif %}
{%- if a is defined -%}
;
; A Records
;
{% for entry in a -%}
{{ "{:<17}".format(entry['name']) }} IN A {{ entry['ip'] }}
{% endfor %}
{% endif %}
{%- if aaaa is defined -%}
; AAAA Records
{%- for entry in aaaa -%}
{{ entry['name'] }} IN AAAA {{ entry['ip'] }}
{% endfor %}
{% endif %}
{%- if cname is defined -%}
;
; CNAME Records
;
{% for entry in cname -%}
{{ "{:<17}".format(entry['name']) }} IN CNAME {{ entry['alias'] }}
{% endfor %}
{% endif %}
{%- if ptr is defined -%}
;
; PTR Records
;
{% for entry in ptr -%}
{{ entry['name'] }} IN PTR "{{ entry['host'] }}"
{% endfor %}
{% endif %}
{%- if txt is defined -%}
;
; TXT Records
;
{% for entry in txt -%}
{{ entry['name'] }} IN TXT "{{ entry['txt'] }}"
{% endfor %}
{% endif %}
{%- if srv is defined -%}
;
; SRV Records
;
{% for entry in srv -%}
{{ entry['name'] }} IN SRV {{ entry['priority'] }} {{ entry['weight'] }} {{ entry['port'] }} {{ entry['target'] }}
{% endfor %}
{%- endif %}
; End of Zone
BIND9 Zone File 생성을 위한 JSON 파일 작성 [ 파일명: /var/cache/bind/my-domain.kr.json ]
$ cat /var/cache/bind/my-domain.kr.json
{
"origin": "my-domain.kr.",
"ttl": 600,
"soa": {
"mname": "ns.my-domain.kr.",
"rname": "sejong.my-domain.kr.",
"serial": "2024041500",
"refresh": 3600,
"retry": 600,
"expire": 604800,
"minimum": 86400
},
"ns": [
{
"host": "ns.my-domain.kr."
},
{
"host": "ns1.my-domain.kr."
}
],
"a": [
{
"name": "@",
"ip": "16.6.80.3"
},
{
"name": "ns",
"ip": "16.6.80.3"
},
{
"name": "ns1",
"ip": "13.2.88.15"
},
{
"name": "myhappyserver",
"ip": "13.93.28.9"
},
{
"name": "my-ingress-nginx-controller",
"ip": "10.1.7.6"
},
... 중간 생략 ...
{
"name": "almighty",
"ip": "129.154.195.186"
}
],
"cname": [
{
"name": "my-nicname-server",
"alias": "almighty"
},
{
"name": "toy",
"alias": "my-ingress-nginx-controller"
},
... 중간 생략 ...
{
"name": "my-supersvc",
"alias": "mail.sogang.ac.kr"
}
]
}
로그 파일 생성을 위한 Python Script 작성 [ 파일명: my_log.py ]
$ cat my_log.py
#!/usr/bin/python3
import logging
import logging.handlers
import os
import datetime
import threading
import time
class MyLog():
def __init__(self, dir, logname) -> None:
self.logname = logname
self.dir = os.path.join(dir,logname)
self.InitLogger()
def InitLogger(self):
## Log format 설정하기
formatter = logging.Formatter("[%(asctime)s] %(levelname)s %(filename)s:%(lineno)d - %(message)s")
if os.path.exists(self.dir) == False :
os.makedirs(self.dir)
log_File = os.path.join(self.dir, self.logname + ".log")
timedfilehandler = logging.handlers.TimedRotatingFileHandler(filename=log_File, when='midnight', interval=1, encoding='utf-8')
timedfilehandler.setFormatter(formatter)
timedfilehandler.suffix = "%Y%m%d.log"
self.logger = logging.getLogger(self.logname)
self.logger.addHandler(timedfilehandler)
self.logger.setLevel(logging.INFO)
self.delete_old_log(self.dir, 3)
now = datetime.datetime.now()
self.toDay = "%04d-%02d-%02d" % (now.year, now.month, now.day)
self.th_auto_delete = threading.Thread(target=self.auto_delete, daemon=True)
self.th_auto_delete.start()
'''
함수 인자 설명:
- fpath:삭제할 파일이 있는 디렉토리,
- age:경과일수
'''
def delete_old_log(self, fpath, age):
for fp in os.listdir(fpath):
fp = os.path.join(fpath, fp)
if os.path.isfile(fp):
timestamp_now = datetime.datetime.now().timestamp()
if os.stat(fp).st_mtime < timestamp_now - (age * 24 * 60 * 60):
try:
os.remove(fp)
except OSError:
print(fp, 'this log file is not deleted')
def auto_delete(self):
while True:
now = datetime.datetime.now()
day = "%04d-%02d-%02d" % (now.year, now.month, now.day)
if self.toDay != day:
self.toDay = day
self.delete_old_log(self.dir, 3)
time.sleep(600)
## This is test code.
if __name__ == '__main__':
log = MyLog("my_log_test_dir", logname="sejong")
for idx in range(3):
log.logger.info(f"로그 메시지가 잘 기록되는지 테스트 중 {idx}")
Kubernetes External-IP를 조회 및 DNS 자동 설정을 위한 Python Script 작성 [ 파일명: kube-dns-auto-config.py ]
#!/usr/bin/python3
import os
import time
import socket
import shutil
import subprocess
import yaml
import json
import datetime
#from datetime import datetime
from kubernetes import client, config
from jinja2 import Environment, FileSystemLoader
import my_log
###############################################################
## NOTE: You have to fix the following variable 'my_run_dir'
###############################################################
my_run_dir = "/opt/kube-dns-auto-config/"
def search_host_from_json_data(host_name):
for item in json_data['a']:
if item['name'] == host_name:
return item
return None
def chk_ip_addr_changed(conf_svc_item):
kube_svc_name = conf_svc_item["kube_svc_name"]
host_name = conf_svc_item["host_name"]
log.logger.info(f"\n")
log.logger.info(f"[ Checking configuration '{kube_svc_name}' / '{host_name}' ]")
for item in kube_svc_info.items:
if item.metadata.name == kube_svc_name:
ip_addr_k8s_svc = item.status.load_balancer.ingress[0].ip
log.logger.info(f" {kube_svc_name:<11} from K8S SVC EXT-IP : {ip_addr_k8s_svc:>15}")
dns_record = search_host_from_json_data(host_name)
if dns_record == None:
log.logger.info(" host_name {host_name} is not found.")
return 0
log.logger.info(f" {host_name:<11} from JSON file : {dns_record['ip']:>15}")
if ip_addr_k8s_svc == dns_record['ip']:
log.logger.info(" IP address of domain name is not changed. Nothing to do.")
return 0
else:
log.logger.info(f" IP address of domain name is changed; {dns_record['ip']} -> {ip_addr_k8s_svc}")
dns_record['ip'] = ip_addr_k8s_svc
return 1
return 0
def get_new_serial(old_serial):
curr_date = datetime.datetime.today().strftime("%Y%m%d")
bool_today = old_serial.startswith(curr_date)
if bool_today is True:
## Serial 값이 오늘 한번이라도 Update된 경우
int_old_serial = int(old_serial)
new_serial = str(int_old_serial + 1)
else:
## Serial 값이 처음 Update되는 경우
new_serial = f"{curr_date}00"
log.logger.info(f"old_serial: [{old_serial}]")
log.logger.info(f"new_serial: [{new_serial}]")
return new_serial
def make_bkup_file(serial):
zone_file = my_conf_info['zone_file']
bkup_zone_file = f"{zone_file}_{serial}"
shutil.copy(zone_file, bkup_zone_file)
bkup_json_file = f"{json_file}_{serial}"
shutil.copy(json_file, bkup_json_file)
return None
def make_zone_file():
## Jinja2 environment object and refer to templates directory
env = Environment(loader=FileSystemLoader(my_run_dir))
template = env.get_template('zone.j2')
template.globals['now'] = datetime.datetime.now().isoformat()
rendered_data = template.render(json_data)
log.logger.info(f"rendered_data: \n{rendered_data}")
fp = open(my_conf_info['zone_file'], "w")
fp.write(rendered_data)
fp.close()
return None
## ------------------------------------------------------------------------
## Begin of Main
## ------------------------------------------------------------------------
if __name__ == '__main__':
log = my_log.MyLog("log_kube_dns", logname="kb_log")
log.logger.info("##### Start program #####")
## Load configration from YAML file.
conf_path = my_run_dir + "my-conf.yaml"
with open(conf_path) as fp:
my_conf_info = yaml.load(fp, Loader = yaml.FullLoader)
json_file = my_conf_info['json_file']
## Configs can be set in Configuration class directly or using helper utility
config.load_kube_config()
v1 = client.CoreV1Api()
##
## !!! Main service logic !!!
##
while True:
svc_ip_changed = 0
with open(json_file) as json_fp:
json_data = json.load(json_fp)
kube_svc_info = v1.list_service_for_all_namespaces(watch=False)
for conf_svc_item in my_conf_info['services']:
svc_ip_changed += chk_ip_addr_changed(conf_svc_item)
if svc_ip_changed > 0:
log.logger.info(f"IP address is changed")
log.logger.info(f"json_data: \n\n{json_data}\n")
new_serial = get_new_serial(json_data['soa']['serial'])
json_data['soa']['serial'] = new_serial
## BKUP file 만들기
make_bkup_file(new_serial)
## JSON 파일에 변경된 내용을 Update한다.
json_str = json.dumps(json_data, indent=4)
with open(json_file, "w") as json_outfile:
json_outfile.write(json_str)
## JSON Data를 Zone DB 포맷으로 변환하여 Zone file에 저장
make_zone_file()
## Restart BIND9 service
ret_value = subprocess.call("systemctl restart bind9", shell=True)
log.logger.info(f"\n\n")
log.logger.info(f"[ BIND9 is restarted ] 'systemctl' return: {ret_value}\n\n")
time.sleep(60)
## ------------------------------------------------------------------------
## End of Main
## ------------------------------------------------------------------------
실행하기 (테스트하기)
$ /opt/kube-dns-auto-config/kube-dns-auto-config.py
위와 같이 Python script를 실행하고, 로그 파일을 열람한다.
$ tail -f /opt/kube-dns-auto-config/log_kube_dns/kb_log/kb_log.log
주의 사항
위 Python script를 실행하는 DNS 서버 장비에 $KUBE_CONFIG 파일이 존재해야 한다.
예를 들어, /home/sejong/.kube/config 같은 파일이 있어야 이 Python 프로그램이 Kubernetes API server에 접근이 가능하다.
##
## 채용 관련 글
##
제가 일하고 있는 기업 부설연구소에서 저와 같이 연구/개발할 동료를 찾고 있습니다.
(이곳은 개인 블로그라서 기업 이름은 기재하지 않겠습니다. E-mail로 문의주시면 자세한 정보를 공유하겠습니다.)
근무지 위치:
서울시 서초구 서초동, 3호선 남부터미널역 근처 (전철역 출구에서 회사 입구까지 도보로 328m)
필요한 지식 (아래 내용 중에서 70% 정도를 미리 알고 있다면 빠르게 협업할 수 있음):
- 운영체제 (학부 3~4학년 때, 컴퓨터공학 운영체제 과목에서 배운 지식 수준):
예를 들어, Processor, Process 생성(Fork)/종료, Memory, 동시성, 병렬처리, OS kernel driver
- Linux OS에서 IPC 구현이 가능
예를 들어, MSGQ, SHM, Named PIPE 등 활용하여 Process간 Comm.하는 기능 구현이 가능하면 됨.
- Algorithm(C언어, C++ 언어로 구현 가능해야 함)
예를 들어, Hashtable, B-Tree, Qsort 정도를 C 또는 C++로 구현할 수 있을 정도
- Network 패킷 처리 지식(Layer 2 ~ 4, Layer 7)
예를 들어, DHCP Server/Client의 주요 Feature를 구현할 정도의 능력이 있으면 됨.
- Netfilter, eBPF 등 (IP packet hooking, ethernet packet 처리, UDP/TCP packet 처리)
- IETF RFC 문서를 잘 읽고 이해하는 능력 ^^
# 위에 열거한 내용 외에도 제가 여기 블로그에 적은 내용들이 대부분 업무하면서 관련이 있는 주제를 기록한 것이라서
# 이 블로그에 있는 내용들을 잘 알고 있다면, 저희 연구소에 와서 연구/개발 업무를 수행함에 있어서 어려움이 없을 겁니다.
회사에서 사용하는 프로그래밍 언어:
- 프로그래밍 언어: C, C++, Go
(참고: 아직 연구소 동료들이 Rust를 사용하진 않습니다만, 새 언어로써 Rust를 사용하는 것을 고려하는 중)
근무 시간:
- 출근: 8~10시 사이에서 자유롭게 선택
- 퇴근: 8시간 근무 후 퇴근 (오후 5시 ~ 7시 사이)
- 야근 여부: 거의 없음 (내 경우, 올해 상반기 6개월간 7시 이후에 퇴근한 경우가 2회 있었음)
- 회식 여부: 자유 (1년에 2회 정도 회식하는데, 본인이 집에 가고 싶으면 회식에 안 감. 왜 참석 안 하는지 묻지도 않음)
외근 여부:
- 신규 프로젝트 멤버 -> 외근 전혀 하지 않음 (나는 신규 프로젝트만 참여해서 지난 1년 동안 한번도 외근 없었음)
- 상용 프로젝트 멤버 -> 1년에 5회 미만 정도로 외근
팀 워크샵 여부:
- 팀 워크샵 자체를 진행하지 않음. (워크샵 참석하는 거 싫어하는 개발자 환영 ^^)
연락처:
- "sejong.jeonjo@gmail.com" # 궁금한 점은 이 연락처로 문의주세요.
- 블로그 비밀 댓글 (제가 하루에 한번씩 댓글 확인하고 있음)
원하는 인재상:
- 우리 부설연구소는 "긴 호흡으로 프로젝트를 진행"하기 때문에 최소 2년간 한 가지 주제를 꾸준하게 연구/개발할 수 있는 개발자를 원함.
- 우리 부설연구소는 자주적으로 연구 주제를 찾아서 업무를 하기 때문에 능동적으로 생각하고 행동하는 동료를 원함.
- 차분하게 연구 주제에 몰입하고, 해법을 찾는 것을 즐기는 사람.
내가 느끼는 우리 연구소의 장점:
- 갑/을 관계가 없음. (제가 근무하고 있는 연구소는 SI업종이 아니라서 갑/을 회사 개념이 없음)
- 연구소 자체적으로 연구 주제를 발굴하고 시스템을 개발하기 때문에 개발 일정에 대한 스트레스가 적음
- 빌딩 전체를 우리 회사가 사용하므로 분위기가 산만하지 않음.
- 근처에 예술의전당, 우면산 둘레길이 있어서 점심 시간에 산책하기 좋음 ^^
- 연구소 동료들 매너가 Good (2년간 일하면서 한번도 감정에 스크레치 생기거나 얼굴 붉히며 싸운 적 없음 ^^)