BETA

lambdaとRoute53を使ってDDNS機能を作った

投稿日:2018-12-25
最終更新:2018-12-26
※この記事は外部サイト(https://www.rocher.kyoto.jp/arbr/?p=1460)からのクロス投稿です

概要

固定IPを振っていない店用にlambdaとRoute53を使ってDDNS機能を作った

背景

この間、店の呼び出しブザーを作ったけど死活監視をしていない。zabbixエージェントを入れようとしたけど店には固定IPが来ていないのでDDNSなり(VPNを貼るなり)しないといけない。

システム

環境

  • python3.7.0
  • raspbian9.4
  • Raspberry Pi B+
  • API Gateway
  • Lambda
  • Route53

システム概要

店のネットワーク内にあるラズパイからAPI Gateway経由でLambdaを呼出。

呼び出されたLambdaで呼び出し元(ラズパイ)のグローバルIPを取得。

今回のグローバルIPが前回のグローバルIPと違ったらLambda内でbotoを使ってRoute53のレコードを変更

lambdaのソース

ポン置きのラズパイから起動しているのでセキュリティ的に不安。なので、対象サーバとかzoneIdは引数でなくlambda側で持っている。
対象のAレコードなかったりしたら動かないけどログ見たらなんとかなるはず。

import boto3  
import logging  

logger = logging.getLogger()  
logger.setLevel(logging.INFO)  

def lambda_handler(event, context):  
        ZONE_ID = 'Route53のHosted Zone ID'  
        logger.debug('call lambda')  
        source_ip = event['source_ip']  
        original_ip = event['original_ip']  

        logger.debug( 'source_ip -> ' + source_ip)  
        logger.debug( 'original_ip -> ' + original_ip)  


        if original_ip == '' or original_ip != source_ip:  
            logger.info('original_ip != source_ip')  
            logger.info( 'source_ip -> ' + source_ip)  
            logger.info( 'original_ip -> ' + original_ip)  
            client = boto3.client('route53')  

            try:  
                response = client.list_resource_record_sets(HostedZoneId=ZONE_ID)  
                target = [item for item in response['ResourceRecordSets'] if item['Name'] == 'hogehoge.epea.co.jp.' and item['Type'] == 'A'][0]  
                logger.info(target)  

                setting_ip = target['ResourceRecords'][0]['Value']  

                if setting_ip != source_ip:  
                    logger.info('modify start')  
                    target['ResourceRecords'][0]['Value'] = source_ip  

                    client.change_resource_record_sets(  
                        HostedZoneId = ZONE_ID,  
                        ChangeBatch = {  
                            'Comment': '多分IPかわった',  
                            'Changes': [{  
                                'Action': 'UPSERT',  
                                'ResourceRecordSet':target  
                                }]  
                            }  
                    )  
                    logger.info('modify finish')  
            except Exception as e:  
                logger.error('KOKODESUKOKODESU')  
                import traceback  
                traceback.print_exc()  
                raise Exception("Check CloudWatch")  
        return {  
            'statusCode': 200,  
            'body': source_ip  
        }  

全体の呼び出し元

Loopしながら呼び出し続けるのみ。

#!/usr/bin/env python3  
# coding: utf-8  
import json  
import logging  
import time  
import os  
import signal  
import sys  

import requests  

def invoker(originalip):  
    logger.debug('invocker start')  
    logger.debug( 'original_ip -> ' + original_ip)  

    headers = {'Content-Type' : 'application/json','x-api-key': ddns_token}  
    payload = {'original_ip': original_ip}  
    res = requests.post('https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/default/ddns'  
        , data=json.dumps(payload)  
        , headers=headers)  
    if res.status_code != 200:  
        print(res.text)  
        print(res.status_code)  
        raise Exception("TODO")  

    logger.debug('res body ' + res.json()['body'])  
    logger.debug('invocker finish')  
    return res.json()['body']  

def handler(signal, frame):  
    logger.info('invocker stop')  
    sys.exit(0)  

signal.signal(signal.SIGINT, handler)  
signal.signal(signal.SIGTERM, handler)  

try:  
    formatter = '%(levelname)s : %(asctime)s : %(message)s'  
    logging.basicConfig(level = logging.INFO, filename = 'ddns.log', format=formatter)  
except:  
    print >> sys.stderr, 'error: could not open log file'  
    sys.exit(1)  
logger = logging.getLogger(__name__)  
logger.setLevel(logging.INFO)  

ddns_token = os.environ['DDNS_TOKEN']  
original_ip = ''  
logger.info('invocker start')  
logger.debug('ddns_token ->[' + ddns_token + ']')  
while True:  
    logger.debug('in main loop')  
    original_ip = invoker(original_ip)  
    time.sleep(900)  

権限

ラムダ作った時に作られる権限の他にRoute53のレコード参照/操作権限を付与

{  
    "Version": "2012-10-17",  
    "Statement": [  
        {  
            "Sid": "VisualEditor0",  
            "Effect": "Allow",  
            "Action": [  
                "route53:ChangeResourceRecordSets",  
                "route53:ListResourceRecordSets"  
            ],  
            "Resource": [  
                "arn:aws:route53:::change/hostedzone/Route53のHosted Zone ID",  
                "arn:aws:route53:::hostedzone/Route53のHosted Zone ID"  
            ]  
        }  
    ]  
}

API Gatewayの設定

リクエストのマッピング

#set ($body = $util.parseJson($input.json('$')))  
{  
   "original_ip" : "$body.original_ip",  
   "source_ip" : "$context.identity.sourceIp"      
}  

エラー時のマッピング

正規表現(でなくそのままだけど) "Check CloudWatch"でメソッドレスポンスのステータスを500に指定

systemd

特にコメントなし

[Unit]  
Description=DDNS Daemon  

[Service]  
EnvironmentFile=/home/pi/.config/environment.d/ddns.conf  
WorkingDirectory=/home/pi/develop/ddns/  
ExecStart=/home/pi/develop/ddns/invoke_ddns.py  
ExecStop=/bin/kill ${MAINPID}  
Restart=always  
Type=simple  
User=pi  
Group=pi  

[Install]  
WantedBy=multi-user.target  
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@qPkJnEPdYTMgQPdyの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!