[AWS] Route53 도메인 자동 업데이트

유동 아이피

현재 원격데스크톱으로 사용하고 있는 컴퓨터는 유동 IP를 쓰고 있다.
매번 컴퓨터를 재부팅 하거나 하면 IP가 바뀐다.

이전부터 라즈베리 파이에서도 재부팅시에 IP가 바뀌는 것 때문에 부팅시 자신에게 메일을 보내는 스크립트를 작성했었다.
그때는 직접 메일로 온 IP를 확인하고, 도메인 설정을 수동으로 바꾸어주었었다.

AWS Route53을 이용한 도메인 IP주소 주기적 업데이트

이번에는 저번과 다르게 가비아가 아닌 AWS의 DNS인 Route53 서비스를 이용중이라서, 주기적으로 IP가 변경될 때 AWS SDK 를 이용해서 도메인 레코드를 업데이트하도록 파이썬 코드를 짜보았다.

프로그램 동작로직에 대한 포인트는 아래와 같다.

  1. 자기 자신의 IP를 확인
  2. DNS의 IP를 확인
  3. AWS Route53의 레코드 값 업데이트

1번과 2번의 경우 여러가지 방법이 존재한다.
인터넷에 검색해보면 자신의 IP를 확인하는 방법으로 웹 소켓 연결을 구성하고, 해당 소켓의 정보를 받아오는 방식, ifconfig.me와 같이 IP를 알려주는 서비스에 curl명령어를 날리는 방식 등등 방법이 다양하다.

결론부터 말하자면, CURL을 이용한 IP체크를 사용했다.
AWS에서 제공하는 ipcheck를 위한 URL curl http://checkip.amazonaws.com과 ifconfig.me가 있었는데, 응답시간은 둘째치고 checkip.amazonaws.com의 리턴값에는 \n이 붙어있어서 스트링 처리를 해줘야 했기에 ifconfig.me를 선택했다.

DNS의 IP확인하는 방법 또한 크게 두가지로 볼 수 있다. DNS 쿼리를 날려서 해당 IP를 불러오는 방식과 AWS SDK를 이용해서 실제 레코드에 등록된 IP를 뽑아오는 방식이 있다.
여기서는 DNS 쿼리가 훨씬 간단하고, 빠르기 때문에 DNS 조회 방식을 사용했다. -> 실제 코드에는 두 방식 모드 함수로 만들어 두었다.

그렇게 작성한 코드가 아래의 코드이다.

현재 A레코드만 Upsert할 수 있도록 박아놓았다. 다른 변수같은 것도 Enum클래스 선언을 안했는데, 나중에 여유가 되면코드 리팩토링을 좀 해보려고 한다.

import logging
import requests
import boto3
import dns.resolver
from botocore.config import Config
from botocore.session import Session
from datetime import datetime


def get_my_ip():
    ip_addr = None
    while ip_addr is None:
        try:
            r = requests.get('http://ifconfig.me')
            ip_addr = r.text
        except:
             pass
    return ip_addr

def get_my_ip_lambda():
	import urllib3
    http = urllib3.PoolManager()
    r = http.request('GET', 'http://ifconfig.me')
    return str(r.data)

def find_hostzone_id(domain):
    zone_list = client.list_hosted_zones_by_name()['HostedZones']
    for zone in zone_list:
        if domain in zone['Name']:
            return zone['Id']


def get_current_zones():
    hosted_zones = client.list_hosted_zones_by_name()['HostedZones']
    host_list = [id for _, id in enumerate(hosted_zones)]
    for host_id in host_list:
        yield host_id['Id']


def find_record_each(zone_id, target_name):
    record_sets = client.list_resource_record_sets(HostedZoneId=zone_id,StartRecordName=target_name, StartRecordType='A',MaxItems='1')
    if record_sets['ResponseMetadata']['HTTPStatusCode']==200:
        yield record_sets['ResourceRecordSets']


def update_record(zone_id,target_name,target_ip,comment):
    response = client.change_resource_record_sets(
        HostedZoneId=zone_id,
        ChangeBatch={
            'Changes': [
                {
                    'Action': 'UPSERT',
                    'ResourceRecordSet': {
                        'Name': target_name,
                        'Type': 'A',
                        'TTL': 300,
                        'ResourceRecords': [
                            {
                                'Value': target_ip
                            },
                        ],
                    }
                },
            ]
        }
    )
    return response


def is_same_ip(domain, record_name,my_ip):
    hostzone_id = find_hostzone_id(domain)
    registerd_ip = None
    for records in find_record_each(hostzone_id, record_name):
        for record in records:
            for resources in record['ResourceRecords']:
                registerd_ip = resources['Value']
    if registerd_ip == my_ip:
        return True
    return False


def is_same_dns(domain, my_ip):
    answers = dns.resolver.resolve(domain, 'A')
    for item in answers:
        if str(item) == my_ip:
            return True
    return False


def set_logger(log_name):
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(formatter)
    logger.addHandler(stream_handler)
    file_handler = logging.FileHandler(log_name)
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    return logger 


def set_boto3_session(access_key_id=None, secret_access_key=None):
    my_session = Session()
    # 기존에 설정된 프로파일이 없으면 수동으로 값 설정
    if len(my_session.available_profiles) == 0:
        logger.info('사용가능한 프로파일 없음') #직접 값 설정 필요
        if access_key_id is None:
            access_key_id = str(input("access_key_id를 입력하시오: ")).strip()
        if secret_access_key is None:
            secret_access_key = str(input('secret_access_key를 입력하시오: ')).strip()
        my_session.set_credentials(access_key_id, secret_access_key)
    return boto3.session.Session(botocore_session=my_session) 


def get_config():
    return Config(
        region_name = 'us-east-1',
        retries = {
            'max_attempts': 10,
            'mode': 'standard'
        }
    )

def main():
    logger = set_logger('dnsupdate.log')
    #세션 설정
    my_config = get_config()
    session = set_boto3_session()
    # session = set_boto3_session('액세스키ID','시크릿 액세스키')
    client = session.client('route53', config=my_config)
    myip = get_my_ip()
    record_name = 'subdomain.domain.com.'
    domain = 'domain.com'
    comment = f"ip update {datetime.today().strftime('%m-%d %H:%M')}"
    try:
        if is_same_ip(domain,record_name,myip):
            logger.info("No need to change")
        else:
            zone_id = find_hostzone_id(domain)
            update_record(zone_id, record_name, myip, comment)   
    except:
        if is_same_dns(record_name,myip):
            logger.info("No need to change")
        else:
            zone_id = find_hostzone_id(domain)
            update_record(zone_id, record_name, myip, comment)


if __name__ == '__main__':
    main()

현재 코드를 사용하려면 먼저 필요한 패키지들이 전부 설치되어있어야 한다.
1. boto3, dnspython을 필수로 설치해야 한다.
2. aws configure 를 통한 AWS 액세스키와 시크릿 액세스키에 대한 프로파일 설정이 되어있거나, 코드 실행시 집어넣거나 125번 줄의 주석을 해제하고 액세스키ID와 시크릿 액세스키를 직접 코드에 반영해주어야 한다.
3. main 함수 내에 recordname, domain 명을 본인이 사용하려는 도메인, 서브도메인에 대해서 설정해주어야 한다.

현재 도메인 내의 서브 도메인의 A레코드를 변경하는 코드로 작성하였기 때문에 일반 도메인의 IP를 업데이트 하려면 수정이 필요해 보인다. ->추후 봐서 수정 예정

아직 미흡한 코드이지만 피드백을 주면 열심히 수정해보겠다.