작성일: 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 |
| |
| |
| |
| 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): |
| |
| 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) |
| |
| |
| |
| 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 ]
| |
| |
| import os |
| import time |
| import socket |
| import shutil |
| import subprocess |
| import yaml |
| import json |
| import datetime |
| |
| from kubernetes import client, config |
| from jinja2 import Environment, FileSystemLoader |
| |
| import my_log |
| |
| |
| |
| |
| |
| 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: |
| |
| int_old_serial = int(old_serial) |
| new_serial = str(int_old_serial + 1) |
| else: |
| |
| 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(): |
| |
| 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 |
| |
| |
| |
| |
| |
| |
| if __name__ == '__main__': |
| log = my_log.MyLog("log_kube_dns", logname="kb_log") |
| log.logger.info("##### Start program #####") |
| |
| |
| 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'] |
| |
| |
| config.load_kube_config() |
| v1 = client.CoreV1Api() |
| |
| |
| |
| |
| 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 |
| |
| |
| make_bkup_file(new_serial) |
| |
| |
| json_str = json.dumps(json_data, indent=4) |
| with open(json_file, "w") as json_outfile: |
| json_outfile.write(json_str) |
| |
| |
| make_zone_file() |
| |
| |
| 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) |
| |
| |
| |
| |
실행하기 (테스트하기)
$ /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년간 일하면서 한번도 감정에 스크레치 생기거나 얼굴 붉히며 싸운 적 없음 ^^) |