概要
固定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
それはそうとボルログに店の情報のせてもらったけど今はほとんど更新されてないのね。。