さくらのクラウドDNSを使ってダイナミックDNSサービス(DDNS)を自作する。


IPv4の固定IPサービスをずっと使ってきましたが、このIPに縛られてフレッツ光から乗り換えできず不便を感じていました。

「もう固定IPじゃなくてもよくない?」と思い自作DDNSに挑戦。仕組みを作ってみると自宅VPNへの接続も簡単になって固定IPにこだわる理由がほぼ消えました。

しかも、さくらのクラウドDNSは月額44円。家庭用でわざわざ高い固定IPを契約するより、はるかにコスパが良い選択肢だと思います。使い勝手も悪くないので同じように悩んでいる人にはおすすめです。


さくらのクラウドの設定

持っていなければ、さくらのクラウドアカウントを作成します。

さくらのクラウドAPIの設定

API設定を行い「API TOKEN」と「API SECRET」を発行して控えておきます。

さくらのクラウドDNSの設定

さくらのクラウドDNSのリソースIDを控えておきます。

既存のドメインに「ddns」などのホスト名を付けて利用します。
例:ddns.example.jp

リソースレコードに「home」を追加します。このホスト名が自宅回線のIPアドレスを返す仕組みです。
例:home.ddns.example.jp

DNSの追加設定

ddns.example.jp が、さくらのクラウドDNSを参照するように、NSレコードを設定します。


さくらのクラウドAPIに接続してDNSを更新する

さくらのクラウドAPIでDNS設定を変更する方法は公式の情報が少なくてかなり苦戦しました。でも試行錯誤しながらなんとか動かすところまで持っていけました。今回のコードはPHPですが、処理の流れはどの言語でも参考になると思います。

<?php

////===========================================================================================
//   *さくらのクラウドDNSでDDNSクラス
////===========================================================================================

class SakuraCloudDDNS{

	private $token = '';
	private $secret = '';

	public  $global_ip = '';
	public  $host_name = '';
	public  $ttl = 300;
	public  $dns_resource_id = '';
	public  $zone = 'is1a';
	public  $result_msg = '';
	public  $result_ary = [];

	///=======================================================================
	//   +__construct
	///=======================================================================

	public function __construct($p_token , $p_secret){

		$this->token = $p_token;
		$this->secret = $p_secret;
	}

	///=======================================================================
	//   +DDNS更新処理
	///=======================================================================

	public function DnsUpdate(){

		//---必要パラメータが空なら何もしない
		if($this->dns_resource_id === '' || $this->host_name === '' || $this->global_ip === ''){

			$this->result_msg = "設定不足です(dns_resource_id / host_name / global_ip)";
			return;
		}

		$json = $this->_apiGet("commonserviceitem/{$this->dns_resource_id}");

		$data = json_decode($json , true);

		if(is_array($data) && !empty($data["CommonServiceItem"]["Settings"]["DNS"]["ResourceRecordSets"])){

			$records = &$data["CommonServiceItem"]["Settings"]["DNS"]["ResourceRecordSets"];

			$updated = false;
			$found = false;

			foreach ($records as &$record){

				//---同一のホスト名のAレコードを選択
				if($record["Type"] === "A" && $record["Name"] === $this->host_name){

					$found = true;

					if($record["RData"] !== $this->global_ip){

						$record["RData"] = $this->global_ip;
						$record["TTL"]   = $this->ttl;

						$updated = true;
					}
				}
			}

			unset($record);

			if($found === false){

				$this->result_msg = "対象ホスト名のAレコードがありません({$this->host_name})";
				return;
			}

			//---IPアドレスが変わっているので更新する
			if($updated === true){

				$body = [
					"CommonServiceItem" => [
						"Settings" => [
							"DNS" => [
								"ResourceRecordSets" => $records,
							],
						],
					],
				];

				$result = $this->_apiPut("commonserviceitem/{$this->dns_resource_id}" , $body);

				//--- JSONを配列にデコード
				$res_ary = $this->result_ary = json_decode($result , true);

				if(is_array($res_ary) && !empty($res_ary["Success"]) && $res_ary["Success"] === true){

					$this->result_msg = "IP更新成功 ($this->global_ip)";

					file_put_contents("./latest_global_ip.txt" , $this->global_ip);

				} else{

					$this->result_msg = "IP更新失敗";
				}

			} else{

				$this->result_msg = "IP変更なし → 更新スキップ";
			}

		} else{

			$this->result_msg = "API取得失敗";
		}
	}

	///=======================================================================
	//   +_apiGet
	///=======================================================================

	private function _apiGet($p_path){

		$url = "https://secure.sakura.ad.jp/cloud/zone/{$this->zone}/api/cloud/1.1/{$p_path}";

		$ch = curl_init($url);

		curl_setopt($ch , CURLOPT_HTTPAUTH , CURLAUTH_BASIC);
		curl_setopt($ch , CURLOPT_USERPWD , $this->token . ":" . $this->secret);
		curl_setopt($ch , CURLOPT_RETURNTRANSFER , true);

		$result = curl_exec($ch);

		curl_close($ch);

		return $result;
	}

	///=======================================================================
	//   +_apiPut
	///=======================================================================

	private function _apiPut($p_path , $p_body){

		$url = "https://secure.sakura.ad.jp/cloud/zone/{$this->zone}/api/cloud/1.1/{$p_path}";

		$ch = curl_init($url);

		curl_setopt($ch , CURLOPT_HTTPAUTH , CURLAUTH_BASIC);
		curl_setopt($ch , CURLOPT_USERPWD , $this->token . ":" . $this->secret);
		curl_setopt($ch , CURLOPT_RETURNTRANSFER , true);
		curl_setopt($ch , CURLOPT_CUSTOMREQUEST , "PUT");
		curl_setopt($ch , CURLOPT_HTTPHEADER , [ "Content-Type: application/json" ]);
		curl_setopt($ch , CURLOPT_POSTFIELDS , json_encode($p_body));

		$result = curl_exec($ch);

		curl_close($ch);

		return $result;
	}
}

宅内サーバ上(?)に置いた cron.php を5分ごとに実行しています。レコードのTTLも300秒にしているので、最悪でも5分待てば最新のIP情報に反映される仕組みになります。

<?php

//---タイムゾーン
date_default_timezone_set('Asia/Tokyo');

//---相対PATHをセットする
chdir(dirname(__FILE__));

require("./_include/sakura_cloud_ddns.class.php");
require("./_include/config.inc.php");

//---------------------------------------------
//   *さくらクラウドDNSでDDNS更新 実行
//---------------------------------------------

$obj = new SakuraCloudDDNS(SAKURA_API_TOKEN , SAKURA_API_SECRET);

// 更新したいレコード名(ddns.xxxxxx.xxx ゾーンの中の "home")
$obj->host_name = "home";

// さくらクラウドDNSのリソースID
$obj->dns_resource_id = DNS_RESOURCE_ID;

//---------------------------------------------
//   *グローバルIPの取得
//---------------------------------------------

//--- ipify から取得
$obj->global_ip = trim(file_get_contents("https://api.ipify.org"));

// テスト用:固定IPで強制上書きしたいときだけ使う
// $obj->global_ip = "1.1.1.1";

//---------------------------------------------
//   *DDNS更新実行
//---------------------------------------------

$obj->DnsUpdate();

echo $obj->result_msg;

file_put_contents("./latest_log.txt" , date("Y-m-d H:i:s") . "  {$obj->result_msg}");

_include/config.inc.php の中で SAKURA_API_TOKENSAKURA_API_SECRET、そして更新対象となる DNS_RESOURCE_ID を定義しています。この3つをセットしておけば、後はスクリプト側で自動的にDNSを書き換える流れになります。


まとめ

プログラムが正常に動くと、latest_log.txt に実行ログが、latest_global_ip.txt に最後に更新されたIPアドレスが保存されます。

VPNの接続先に home.ddns.example.jp を指定すれば、固定IPと同じように扱えるのでけっこう便利です。

もちろん自宅サーバが常時動いていることが前提なので「これ誰が使うんだ?」という気もしますが、仕組みとしてはおもしろく作れたのでシェアしておきます!


おまけ

宅内ネットワークから自分のグローバルIPは簡単には取れないと思っていましたが、実際はレンタルサーバにこのコードを置くだけで普通に取得できることに気付きました。

<?php

echo $_SERVER['REMOTE_ADDR'];

あとはこのURLを file_get_contents() するだけです。簡単でした。