DDNS(ダイナミックドメインネームシステム)って?
DDNS(ダイナミックドメインネームシステム)とは、動的に割り当てられているIPアドレスとドメイン名を紐づけてくれるものです。
これを利用すれば、固定のグローバルIPアドレスを持っていなくても、
自宅サーバー内にたてたWebアプリケーションに自宅外からアクセスできるようになったり、
出先からリモートデスクトップを利用できるようになったりするので、趣味でいろいろと試してみたいようなときに、コストをかけずにできます。
(※もちろん、サーバーを外部に公開するには相応のリスクがあるので、セキュリティ対策はマストです)
私はMyDNSというDDNSサービスを利用しているのですが、継続して利用するためにはユーザーが定期的にIPアドレスの通知を行なうよう、利用規定で定められています。(これは死蔵アカウントが増殖していくのを防ぐための取り決めだと思われます)
1週間通知がされていないとIPとドメインの紐づけが切れ(DNS情報がなくなり)、1か月IPの通知がされていないとアカウントそのものが削除されます。
通知の方法は、サイトにログインして画面上でボタンを押してもできますが、APIも用意されているので、スクリプトを作成してタスクスケジューラやcronで自動実行するようにしておけば、毎日毎日手間がかかるということはありません。
(ごく個人的に)ちょっと困っていること
ですがたまに、停電や、ブレーカーが落ちた、などでIP通知を送るスクリプトを仕込んだサーバーがシャットダウンしたまま、それに気づかずに放置してしまうということがあります。気づけばアカウント削除の期限である1か月まであと数日だった、ということが何度かありました。
一応、公式からも8日間IP通知が無い場合にはその旨メールでお知らせが届くのですが、結構、外出先でメールを見て、帰ったら対応しておこうと思ってそのまま忘れてしまうということがあったりします。
個人的にはもっとしつこくリマインドしてほしい!(少数意見)
できれば、切れていたら1日1回くらいはお知らせしてほしい。...
というわけで、前置きが長くなりましたが、その死活監視の機能をAWS Lambdaで作ってみました。
やりたいこと
- ドメイン名からIPアドレス取得を試みる。
- 取得できなかったら紐づけが切れている可能性があるとみなしてメール通知する。
- 以上を定期的に自動実行する。
- ランニングコストは無料か、限りなくゼロに近い金額(月間数十円程度まで)でまかなう。
これらを実現するために、以下の手順で構築していきます。
Lambda関数を作成する
Lambdaは月間100万リクエストまで無料なので、やりたいことにはこれで十分。
ということで利用することにします。
Node.js 18.xで作成します。
AWSマネジメントコンソールのLambdaメニューからすべてデフォルト設定のまま進めていくと、テンプレートが.mjsファイル(ECMA Script形式)で作成されていたので、これをもとにコードを書いていきます。
全体構成
./
├── Functions ... 別ファイルへ分割した関数を置くディレクトリ
│ ├── lookup.mjs ... ドメインからIPアドレスの取得を試みる関数
│ └── notify.mjs ... メール送信を行なう関数
├── index.mjs ... エントリポイント
└── settings.mjs ... 設定ファイル
settings.js
export const conf ={
"domain":"{%MyDNSに登録しているドメイン名%}",
"sesRegion":"ap-northeast-1",
"mailTo":"{%メール通知の宛先アドレス%}",
"mailFrom":"{%メール通知の送信元アドレス%}",
"mailBody":"MyDNS.JPへ登録されているドメイン({%MyDNSに登録しているドメイン名%})とIPアドレスの紐づけが確認できませんでした。設定を確認してください。",
"mailSubject":"【MyDNS死活監視】設定を確認してください。"
};
export const status = {
"Unmodified":{"code":"200","description":"変更なし"},
"Notified":{"code":"201","description":"変更あり(メール通知済み)"},
"FailedToNotify":{"code":"503","description":"変更あり(メール通知失敗)"},
"Unhandled":{"code":"500","description":"不明なエラー"}
};
index.mjs
import { conf, status } from './settings.mjs';
import lookup from './Functions/lookup.mjs';
import notify from './Functions/notify.mjs';
export const handler = async (event) => {
try {
//ドメイン名から現在のIPアドレスを取得
const result_lookup = await lookup(conf.domain);
console.log("STEP_LOOKUP\n" + JSON.stringify(result_lookup));
//取得に失敗したらDNS情報がなくなっている可能性があるのでメール通知する
let result_notify = null;
if (result_lookup.code !== 200) {
result_notify = await notify();
console.log("STEP_NOTIFY\n" + JSON.stringify(result_notify));
}
//処理結果のレスポンスを送信
let result = null;
if (result_notify === null) {
result = status.Unmodified;
} else {
if (result_notify.code === 200) {
result = status.Notified;
} else {
result = status.FailedToNotify;
}
}
return makeResponse(result);
} catch (err) {
console.error("UNHANDLED_ERROR_OCCURED\n" + JSON.stringify(err));
return makeResponse(status.Unhandled);
}
};
function makeResponse(result) {
console.log("RESULT\n" + JSON.stringify(result));
return {
resultCode: result.code,
result: result.description
};
}
Functions/lookup.mjs
import mod_dns from 'dns';
/**
* [async function]
* ドメイン名をもとにグローバルIPアドレスを取得する
* <param name="domain">ドメイン名</param>
* <return>
* <property name="code">結果コード(200:成功 500:失敗)</property>
* <property name="data">
* <property name="domain">ドメイン名</property>
* <property name="address">成功した場合、取得したグローバルIPアドレス</property>
* <property name="error">エラーが発生した場合、Errorオブジェクトのインスタンス</property>
* </property>
* </return>
*/
export default async function lookup(domain) {
try {
let result = { "code": null, "data": { "domain": domain, "address": null, "error": null } };
return new Promise((resolve, reject) => {
mod_dns.lookup(domain, { "family": 4 }, (err, address, family) => {
result.code = err === null ? 200 : 500;
result.data.address = address;
result.data.error = err;
if (err === null) {
resolve(result);
} else {
//※何らかの理由で取得できなかった、という結果を元に
// 呼出元の処理は続けたいので、ここではresolveとする
resolve(result);
}
});
});
}
catch (err) {
throw err;
}
}
Functions/notify.mjs
import { conf } from '../settings.mjs';
import { SESClient as mod_ses_client,SendEmailCommand as mod_ses_command } from "@aws-sdk/client-ses";
/**
* [async function]
* メール送信を行なう
* <param name="domain">ドメイン名</param>
* <return>
* <property name="code">結果コード(200:成功 500:失敗)</property>
* <property name="data">
* <property name="error">エラーが発生した場合、Errorオブジェクトのインスタンス</property>
* </property>
* </return>
*/
export default async function notify() {
let result = { "code": null, "data": { "error": null } };
try {
return new Promise(async (resolve, reject) => {
// メール送信設定
const send_params = {
Destination: {
ToAddresses: [conf.mailTo]
},
Message: {
Body: { Text: { Data: conf.mailBody } },
Subject: { Data: conf.mailSubject }
},
Source: conf.mailFrom
};
const ses_command = new mod_ses_command(send_params);
const ses_client = new mod_ses_client({"region":conf.sesRegion});
// メール送信
try {
await ses_client.send(ses_command);
result.code =200;
} catch (err) {
result.code =500;
result.data.error = err;
}
if(result.code === 200){
resolve(result);
}else{
reject(result);
}
});
}
catch (err) {
throw err;
}
}
SES(Simple Email Service)を設定する
紐づけが切れていた際にメールで通知する機能を持たせるため、SES(Simple Email Service)の設定をします。
今回は自分のメールアドレスから自分のメールアドレスへ送れさえすればよいので、サンドボックス環境内から出さないまま使用することとします。
(サンドボックスから出して使用する必要がある場合は下記の手順で行ないます)
AWSマネジメントコンソールでSESのメニューに入り、「最初のEメールの送信」 > 「IDの作成」ボタンを押します。
「Eメールアドレス」に自分のメールアドレスを入力して「IDの作成」ボタンを押します。メールアドレスはGmailなどでも大丈夫です。
ロールにSESの使用権限を付与する
Lambda関数を作成する際に、デフォルトの設定では実行ロールの項目は「基本的な Lambda アクセス権限で新しいロールを作成」が選択されているので、上の手順でLambda関数を作成した際に、この関数のためのロールが作成されています。
このロールに、SESでのメール送信を許可する設定を追加します。
AWSマネジメントコンソールでLambdaのメニューに入り、「設定」 > 「アクセス権限」を開きます。
ロール名がリンクになっているので、クリックします。
ポリシーの設定画面が開かれるので、
「許可ポリシー」 > 「ポリシー名」のアコーディオンブロックを開き、「編集」ボタンを押します。
ロールにアタッチされているポリシーに、SESによるメール送信の権限を追加して、保存します。
トリガーを設定する
Event Bridgeで定期的に実行するためのトリガーを作成します。
ここではテストのために短めの間隔で設定しました。
AWSマネジメントコンソールでLambdaのメニューに入り、「関数の概要」 > 「トリガーを追加」ボタンを押します。
「ソースを選択」で「EventBridge」を選択するとLambda関数をどのようなスケジュールで実行するか設定する画面が表示されます。
「Create a new rule」、「Schedule expression」を選択し、
最下部のテキストボックスにcron書式でスケジュール設定を入力します。
ここでは15分間隔で関数が実行されるように設定しました。
「Rule name」へは任意の名称を入力します。選択、入力できたら「追加」ボタンを押します。
ここまで作って、試してみます。
MyDNSのサイトへログインして、紐づけの設定を手動で解除します。「0.0.0.0」を設定すると期限を待たず意図的に解除することができます。