はじめに
エンジニアの皆さんは自宅に検証用のサーバを持っていたりすることも多いと思います。
かくいう私も自宅で検証用サーバが1台だけあり、そこにVMを立てて個人用途でOSSのアプリケーションを動かしており、外部からもDNSで名前解決してアクセスできるようにしています。
発端
そんな状況で先日引っ越しをしまして、グローバルIPが変更になりDNSのレコード更新が必要になりました。
グローバルIPも瞬断程度であれば変わることはあまりないですし、固定IPの契約も料金払うほどのモチベーションはないです(プロバイダの固定IPサービスは約5000円/月!?)。
だけどグローバルIPが変わる都度自宅からグローバルIPを確認して、毎回DNSレコード変更するのも面倒です。
そういったことを解決するのにルーターのメーカーが提供しているDynamic DNS(以降DDNS)なんてものもあるようですが、調べるのが面倒です。
それならDDNSを自作すればいいんじゃね?となりGithub Copilot君に手伝ってもらいながらLambdaで作成してみました。
要件
- 自宅の検証用サーバから定期的に自作DDNSにポーリング(何らかの手段でAWSのAPIをたたくイメージ)
- ポーリングされたときのグローバルIPが現在のレコードのIPと異なっていたら、LambdaからRoute53のレコードを更新する
-
あまりエンドポイントの管理をしたくない(セキュリティ的な意味で)
- API Gatewayを立てたりアクセス元IP制限とか面倒なのしたくない
(没案) Amazon EventBridgeで特定のユーザからのイベントを監視
エンドポイントの管理をしたくないというところから、AWS CLIで適当にaws sts get-caller-identity
とかをDDNS専用ユーザーが定期的に実行し、EventBridgeからIPアドレス情報取得とLambdaをキックする方法を思いつきました。
ちょこっとだけ検証して、あとはLambda実行すればいいところまでやりましたが、冷静に考えてなんかめんどくさい構成になってね?そもそもAWS CLI実行するんならそのままLambdaを実行すればよくね?となり没案としました。
※以下はその時に考えたイベントパターンの供養です
{
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["sts.amazonaws.com"],
"eventName": ["GetCallerIdentity"],
"userIdentity": {
"arn": ["<DDNS用IAMユーザのARN>"]
}
}
}
(採用案)Lambdaの関数URLをIAM認証でアクセスする案
Lambdaをcurlで簡単に実行できる方法ありかなーと思い、調べたら関数URLというのがありました。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/urls-configuration.html
IAM認証もできそうなので、これでエンドポイントを変に管理しなくてもよさそう。
というわけでこちらの案を採用します。
実装周り
Lambdaのコード
以下が実装するLambdaのPythonスクリプトです。
もし使いたい人がいたらhosted_zone_id
、records_to_update
の部分修正して使ってください。
関数のCPU/メモリなどのリソースはデフォルト(最小)で、3秒過ぎるくらいなので、タイムアウト値は10秒とかにすればいいと思います。
import json
import boto3
def lambda_handler(event, context):
# Route53の設定
hosted_zone_id = 'YOUR_HOSTED_ZONE_ID'
record_type = 'A'
# 更新したいレコードのリスト
records_to_update = [
'YOUR_RECORD_NAME_1',
'YOUR_RECORD_NAME_2'
]
# Route53クライアントを作成
route53 = boto3.client('route53')
# アクセス元のIPアドレスを取得
source_ip = event['requestContext']['http']['sourceIp']
# 更新結果を格納するリスト
update_results = []
for record_name in records_to_update:
try:
# Route53のレコードを取得
response = route53.list_resource_record_sets(
HostedZoneId=hosted_zone_id,
StartRecordName=record_name,
StartRecordType=record_type,
MaxItems="1"
)
# 現在のレコードのIPアドレスを取得
current_ip = response['ResourceRecordSets'][0]['ResourceRecords'][0]['Value']
# IPアドレスが異なる場合、Route53のレコードを更新
if current_ip != source_ip:
route53.change_resource_record_sets(
HostedZoneId=hosted_zone_id,
ChangeBatch={
'Changes': [
{
'Action': 'UPSERT',
'ResourceRecordSet': {
'Name': record_name,
'Type': record_type,
'TTL': 300,
'ResourceRecords': [{'Value': source_ip}]
}
}
]
}
)
update_results.append(f"{record_name}({record_type}) is updated from {current_ip} to {source_ip}")
else:
update_results.append(f"No change for {record_name} (Current IP: {current_ip})")
except Exception as e:
return {
'statusCode': 500,
'body': json.dumps(f"Error updating records: {str(e)}", ensure_ascii=False, indent=4)
}
return {
'statusCode': 200,
'body': json.dumps(update_results, ensure_ascii=False, indent=4)
}
Lambda用IAMロール
やりたい操作はRoute53でレコードのリストとレコードの更新だけができればいいだけです。
LambdaのログをCloudWatch Logsに出力するためにlogsのActionが少しありますが、AWS管理ポリシーのAWSLambdaBasicExecutionRole
を追加で付与するだけでもいいと思います。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowRoute53Operations",
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/<YOUR_HOSTED_ZONE_ID>"
},
{
"Sid": "AWSLambdaBasicExecutionRole1",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:ap-northeast-1:<YOUR_AWS_ACCOUNT_ID>:*"
},
{
"Sid": "AWSLambdaBasicExecutionRole2",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:<YOUR_AWS_ACCOUNT_ID>:log-group:/aws/lambda/<LAMBDA_FUNCTION_NAME>:*"
]
}
]
}
IAM認証用ユーザ作成
自宅サーバから関数URLにアクセスするため、IAMアクセスキーが必要です。
そのためIAM認証用のユーザを作成し、アクセスキーを発行します。
対象ユーザには関数URLから実行できる権限をのみを付与します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "lambda:InvokeFunctionUrl",
"Resource": "arn:aws:lambda:ap-northeast-1:<YOUR_AWS_ACCOUNT_ID>:function:<YOUR_FUNCTION_NAME>"
}
]
}
使い方
自宅のサーバからcronやらなんやらで定期的に以下コマンドを実行します。
--aws-sigv4
オプションが必要なため、curlは7.75以上のバージョンが必要っぽいです。
curl "<YOUR_LAMBDA_FUNCTION_URL>" \
--aws-sigv4 "aws:amz:ap-northeast-1:lambda" \
--user "<YOUR_ACCESS_KEY>:<YOUR_SECRET_ACCESS_KEY>"
aws configure
が設定されていれば以下でも大丈夫です。
curl "<YOUR_LAMBDA_FUNCTION_URL>" \
--aws-sigv4 "aws:amz:ap-northeast-1:lambda" \
--user "$(aws configure get aws_access_key_id):$(aws configure get aws_secret_access_key)"
※2024/12/16追記:IPoEの設定などをした際、アクセス元IPがIPv6アドレスになって失敗したことがあったので、curlにオプションで-4
とかつけたほうが間違いなさそうです
実行時のレスポンス
IPアドレス載せられないので味気ないですが、、、
- 変更時
[
"<YOUR_RECORD_NAME_1>(A) is updated from <SOURCE_IP> to <CURRENT_IP>",
"<YOUR_RECORD_NAME_2>(A) is updated from <SOURCE_IP> to <CURRENT_IP>"
]
- 変更なし
[
"No change for <YOUR_RECORD_NAME_1> (Current IP: <CURRENT_IP>)",
"No change for <YOUR_RECORD_NAME_2> (Current IP: <CURRENT_IP>)"
]
おわりに
これで今後はグローバルIPを気にしなくてよくなりました。
この記事作成の直前に実装したので運用上の問題などはまだ分かってないですが、問題があった時にグローバルIP周りの観点を忘れそうなのがちょっと心配です。失敗したときの通知設定とかしたいですね。
また最近Terraform触ってたのでIaC化までして載せたかったですが全然時間がありませんでした。気が向いたら載せます(これは面倒になってやらないパターン)。