【学習メモ】EC2でbindtoroute53.plでインポートしてroadworkerで編集する

# はじめに
Route53にゾーンファイルをインポートするのに1000レコード以上あったので、bindtoroute53.plを使ってみました。
EC2インスタンスを使ってインポートしてみました。
ついでに、roadworkerを使ってroute53のレコードを編集もしました。
route53からBIND形式に戻す方法も書きます。

## EC2の準備
インスタンスに接続したところから始めます。

```bash:パスワードの設定
$ ssh -i key xxx.xxx.xxx.xxx                                                                                        
Last login: Thu Jun 28 10:43:56 2018 from yyy.yyy.yyy.yyy

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2018.03-release-notes/
$ sudo su
# passwd
```
念の為rootのパスワードを設定しておきます。
以降、rootで作業します。

### CPANの準備
bindtoroute53.plにはCPANモジュールが必要とのことなのでインストールします。

```bash:CPANのインストール
yum install -y cpan
perl -MCPAN -e shell

→Enter連打

cpan shell -- CPAN exploration and modules installation (v1.9800)
Enter 'h' for help.

cpan[1]> quit
Terminal does not support GetHistory.
Lockfile removed.

*** Remember to restart your shell before running cpan again ***
# exit
$ exit
```

exitしなさいと言われたのでログインし直します。

CPANモジュールをインストールしていきます。

```bash:CPANモジュールのインストール1
$ su
Password:

yum install -y gcc
(中略)
perl -MCPAN -e 'install Net::DNS::ZoneFile'
perl -MCPAN -e 'install NetAddr::IP'
perl -MCPAN -e 'install Net::DNS'
perl -MCPAN -e 'install Net::IP'
perl -MCPAN -e 'install Digest::HMAC'
perl -MCPAN -e 'install Digest::SHA1'
perl -MCPAN -e 'install Digest::MD5'
perl -MCPAN -e 'install MIME::Base64'
```
gccが必要になるので先にインストールしておきます。

```bash:gccがないときのエラー
# perl -MCPAN -e 'install Digest::SHA1'
Reading '/root/.cpan/Metadata'
  Database was generated on Thu, 28 Jun 2018 04:54:22 GMT
Running install for module 'Digest::SHA1'
Running make for G/GA/GAAS/Digest-SHA1-2.13.tar.gz
Checksum for /root/.cpan/sources/authors/id/G/GA/GAAS/Digest-SHA1-2.13.tar.gz ok

  CPAN.pm: Building G/GA/GAAS/Digest-SHA1-2.13.tar.gz

Checking if your kit is complete...
Looks good
Generating a Unix-style Makefile
Writing Makefile for Digest::SHA1
Writing MYMETA.yml and MYMETA.json
cp SHA1.pm blib/lib/Digest/SHA1.pm
Running Mkbootstrap for SHA1 ()
chmod 644 "SHA1.bs"
"/usr/bin/perl" -MExtUtils::Command::MM -e 'cp_nonempty' -- SHA1.bs blib/arch/auto/Digest/SHA1/SHA1.bs 644
"/usr/bin/perl" "/usr/share/perl5/vendor_perl/ExtUtils/xsubpp"  -typemap '/usr/share/perl5/ExtUtils/typemap' -typemap '/root/.cpan/build/Digest-SHA1-2.13-ZYeggO/typemap'  SHA1.xs > SHA1.xsc
mv SHA1.xsc SHA1.c
gcc -c   -D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pipe -fstack-protector -I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic   -DVERSION=\"2.13\" -DXS_VERSION=\"2.13\" -fPIC "-I/usr/lib64/perl5/CORE"   SHA1.c
/bin/sh: gcc: command not found
make: *** [SHA1.o] Error 127
  GAAS/Digest-SHA1-2.13.tar.gz
  /usr/bin/make -- NOT OK
'YAML' not installed, will not store persistent state
Running make test
  Can't test without successful make
Running make install
  Make had returned bad status, install seems impossible
```
```gcc: command not found```なんてわかりやすいエラーでしょう。

### bindtoroute53.plの準備
bindtoroute53.plスクリプトをダウンロードしておきます。

```bash:bindtoroute53.plのダウンロード
cd /usr/local/src
wget http://awsmedia.s3.amazonaws.com/catalog/attachments/bindtoroute53.pl
chmod +x bindtoroute53.pl
```
[BIND to Amazon Route 53 Conversion Tool(aws.amazon.com)](https://aws.amazon.com/jp/code/bind-to-amazon-route-53-conversion-tool/)

## bindファイルをxmlへ変換
変換してみます。
インスタンスに、zoneファイルをアップロードしてあるものとします。

```bash:xml形式変換その1
ls /usr/local/src
bindtoroute53.pl domain.zone

./bindtoroute53.pl --ignore-origin-ns --ignore-soa --origin < /usr/local/src/domain.zone \
> /usr/local/src/domain.zone.xml
(中略)

ResourceRecords exceeds 1000. Try separating into multiple batches. at ./bindtoroute53.pl line 220.
```
セパレートしろと言われているんでしょうか。
990行ごとに分割してみます。

```bash:ゾーンファイルの分割
split -l 990 domain.zone -d sep
ls | grep sep
sep00 sep01 sep02 sep03
```
sep00～03まで出来上がりました。

ただし、zoneファイルが省略して記載されている場合、省略部分で切れてしまう可能性があるので、ファイルの行数を確認して分けた方が無難です。

```bash:ゾーンファイルの分割(行単位)
sed -n    1,990p  domain.zone > sep00
sed -n  991,1970p domain.zone > sep01
sed -n 1971,2951p domain.zone > sep02
sed -n 2952,3500p domain.zone > sep03
```


```bind:xml形式への変換
./bindtoroute53.pl --ignore-origin-ns --ignore-soa --origin domain < sep00 > sep00.zone.xml
Ignore ...(中略)
./bindtoroute53.pl --ignore-origin-ns --ignore-soa --origin domain < sep01 > sep01.zone.xml
./bindtoroute53.pl --ignore-origin-ns --ignore-soa --origin domain < sep02 > sep02.zone.xml
./bindtoroute53.pl --ignore-origin-ns --ignore-soa --origin domain < sep03 > sep03.zone.xml
```


さてインポートするにはどうしたら…
とりあえずブラウザのコンソールから作成したゾーンのインポートに入れてみます。
XMLを理解してくれるのかと半信半疑で行いますが

```bash:xmlコピペインポートの失敗2
Error parsing zone file: File too large
```
結局大きすぎだそうです。
念の為、XMLがインポートできるか、数レコードのXMLで試してましたがだめでした。

```bash:xmlコピペインポートの失敗2
Error parsing zone file: Error in line 1: Invalid type 'version=' (encountered after 0 correct records) 
In line:
<?xml version="1.0" encoding="UTF-8"?>
```
XMLを解釈してくれません。

## dnscurl.plで反映
分割しても、どうやってインポートすれば良いか書かれていない…
なんのために作ったXMLだよ！と思いましたが、
検索するとdnscurl.plというスクリプトで反映させられるような記事がいっぱい出てきます。

ですが、肝心のAWS公式dnscurl.plの紹介ページはリンク切れ。
インポートした記事のいくつかにwgetのリンク先が書いてあり、
2018/6/29時点でファイルのリンクは切れていなかったので作業を進められましたが、
公式からあっち行けこっち行けと[盥回し](http://ja.uncyclopedia.info/wiki/%E3%81%9F%E3%82%89%E3%81%84%E3%81%BE%E3%82%8F%E3%81%97)にされたら樹海にいた気分です。

というわけで、dnscurl.plスクリプトをDLして使ってみます。

```bash:dnscurl.plの準備
cd /usr/local/src
wget http://awsmedia.s3.amazonaws.com/catalog/attachments/dnscurl.pl
chmod +x dnscurl.pl
```
### アクセスキーの設定
[AWS Route53のDNSレコードをコマンドラインから操作する(takeshiyako.blogspot.com)](https://takeshiyako.blogspot.com/2016/09/aws-route53dns.html)によると、
Route53だけ編集できるポリシーを作って、Route53用のユーザーを作ってそのポリシーを適用し、
そのユーザーでアクセスキーを発行する流れでしたが、
今回は自分のアカウントでアクセスキーを発行します。

アクセスキーを設定して有効化確認します。

```bash:EC2インスタンス上で確認
# aws configure
AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXX
AWS Secret Access Key [None]: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Default region name [None]: 
Default output format [None]: 
```
これを公開してしまうと乗っ取られて大量課金されるフラグが立つんですね。
アクセスキーが設定されているのと、このアクセスキーでAWSコマンドが動作するか確認します。

```bash:アクセスキーの確認
# cat /root/.aws/credentials
[default]
aws_access_key_id = XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# aws route53 list-hosted-zones
{
    "HostedZones": [
        {
            "ResourceRecordSetCount": X, 
            "CallerReference": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 
            "Config": {
                "PrivateZone": false
            }, 
            "Id": "/hostedzone/XXXXXXXXXXXXXX", 
            "Name": "your.domain."
        }
}
```

動作しています。

```dnscurl.pl --debug```によれば、接続テストができるようなのでしてみます。
アクセスキーの参照は```/root/.aws-secrets```というファイルが標準で使われるようなので、作成します。

```bash:.aws-secretsの作成
# tee /root/.aws-secrets <<EOF
%awsSecretAccessKeys = (
    # my personal account
    'fred-personal' => {
        id => 'XXXXXXXXXXXXXXXXXXXX',
        key => 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    },
);
EOF
# chmod 600 /root/.aws-secrets

```
その後試したところ、```/root/.aws/credentials```が使われるようです。

```bash:/root/.aws/credentialsの作成
tee /root/.aws/credentials <<EOF > /dev/null
[default]
aws_access_key_id = XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
EOF
chmod 600 /root/.aws/credentials
```


```bash:dnscurl.plの接続テスト
./dnscurl.pl --keyname fred-personal -- https://route53.amazonaws.com/2010-10-01/hostedzone
(中略)
```
XML形式でいろいろ返ってきてれば成功です。

### dnscurl.plでアップロード
事前にホストゾーンを作成しておく必要があり、ホストゾーンのID(Hosted Zone ID)をメモしておきます。
アップロードします。

```bash:分割ファイルのインポート
./dnscurl.pl --keyname fred-personal -- -H "Content-Type: text/xml; charset=UTF-8" -X POST --upload-file sep00.zone.xml https://route53.amazonaws.com/2010-10-01/hostedzone/XXXXXXXXXXXXX/rrset
./dnscurl.pl --keyname fred-personal -- -H "Content-Type: text/xml; charset=UTF-8" -X POST --upload-file sep01.zone.xml https://route53.amazonaws.com/2010-10-01/hostedzone/XXXXXXXXXXXXX/rrset
./dnscurl.pl --keyname fred-personal -- -H "Content-Type: text/xml; charset=UTF-8" -X POST --upload-file sep02.zone.xml https://route53.amazonaws.com/2010-10-01/hostedzone/XXXXXXXXXXXXX/rrset
./dnscurl.pl --keyname fred-personal -- -H "Content-Type: text/xml; charset=UTF-8" -X POST --upload-file sep03.zone.xml https://route53.amazonaws.com/2010-10-01/hostedzone/XXXXXXXXXXXXX/rrset
```

以下の様なエラーが出る場合、分割したsep00、sep01…を結合しているために発生します。
XML形式ではない(ヘッダが数箇所出てくる)ので修正する必要があります。

```
<ErrorResponse xmlns="https://route53.amazonaws.com/doc/2010-10-01/"><Error><Type>Sender</Type><Code>MalformedInput</Code><Message>Could not parse XML</Message></Evim
```

```bash:xml削除する部分
  </Changes>
 </ChangeBatch>
</ChangeResourceRecordSetsRequest>
<?xml version="1.0" encoding="UTF-8"?>
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2010-10-01/">
 <ChangeBatch>
  <Comment>This change imports a zone file</Comment>
  <Changes>
```

以下のようなエラーが出る場合、XML上でも1,000レコードを超えているようです。

```
<ErrorResponse xmlns="https://route53.amazonaws.com/doc/2010-10-01/"><Error><Type>Sender</Type><Code>InvalidChangeBatch</Code><Message>Number of records limit of 1000 exceeded.</Message></Error><RequestId>
```
結局XMLでも1000レコード以上扱えないようなので、上記エラーが出る場合は再度分割をやり直します。


エラーが出た…

```bash:インポート時のエラーその1
<?xml version="1.0"?>
<ErrorResponse xmlns="https://route53.amazonaws.com/doc/2010-10-01/"><Error><Type>Sender</Type><Code>InvalidChangeBatch</Code><Message>Tried to create resource record set [name='your.domain', type='TXT'] but it already exists</Message></Error><RequestId>
```
これは同じレコードがyour.domainであるよというわかりやすいエラーです。

```bash:インポート時のエラーその2
<?xml version="1.0"?>
<ErrorResponse xmlns="https://route53.amazonaws.com/doc/2010-10-01/"><Error><Type>Sender</Type><Code>InvalidChangeBatch</Code><Message>Duplicate Resource Record: XXX.XXX.XXX.XXX</Message></Error><RequestId>
```
SNIで同じIP使ってると、どのFQDNかわからないエラーだから困る…
同じIPのAレコードが1つのFQDNに対して2個入っているのが原因でした。

エラーが出たファイルは取り込みされていないので、エラーを修正して再度取り込んだところ、問題なく取り込めました。

実行後、PENDINGと出ていれば正常に処理されているようです。

```
<?xml version="1.0"?>
<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2010-10-01/"><ChangeInfo><Id>/change/XXXXXXXXXXXXXX</Id><Status>PENDING</Status><SubmittedAt>2018-09-05T04:39:22.554Z</SubmittedAt></ChangeInfo></ChangeResourceRecordSetsResponse>
```

参考資料
[bindtoroute53.plとdnscurl.plを使ってRoute53にBINDのゾーン情報を登録する。(qiita.com/kooohei)](https://qiita.com/kooohei/items/1702e67f0cead1072ad8)
まさに同じことをやっている記事です。


## roadworker
さてインポートがうまくいったようなので、今度はroadworkerというツールでRoute53をCLI操作してみたいと思います。

こちらの記事を参考にしています。
[AWS Amazon EC2にrbenvでRuby環境を構築する(qiita.com/hytkgami)](https://qiita.com/hytkgami/items/5cc393c4327bdc4ea0ef)
[RoadWorkerで管理するAWS Route53(qiita.com/SatoHiroyuki)](https://qiita.com/SatoHiroyuki/items/6a629dc2ee33bbd6ea32)

#### roadworkerのインストール
roadworkerをインストールしていきます。

```bash:roadworkerのインストール
gem install roadworker
Building native extensions.  This could take a while...
Successfully installed pcaprub-0.12.4
Fetching: packetfu-1.1.13.gem (100%)
ERROR:  Error installing roadworker:
        packetfu requires Ruby version >= 2.1.0.
```
rubyのバージョンが古いと言われます。

```bash:rubyバージョンの確認
ruby -v
ruby 2.0.0p648 (2015-12-16) [x86_64-linux]
```

確かに古いので、rbenvを入れて入れられる最新の2.5.1を上記記事を参考にインストールし、roadworkerを入れます。
gitも入っていないのでgitも入れます。
注意)2.4.0以降は警告が出るので、後述の2.3.7をインストールする手順を参考にしてください。

```bash:ruby2.5.1インストール
# yum -y install gcc-c++ glibc-headers openssl-devel readline libyaml-devel readline-devel zlib zlib-devel libffi-devel libxml2 libxslt libxml2-devel libxslt-devel sqlite-devel
# yum -y install libpcap-devel

# tee /etc/profile.d/rbenv.sh <<EOF > /dev/null 2>&1
export RBENV_ROOT="/opt/rbenv"
export PATH="\${RBENV_ROOT}/bin:\${PATH}"
eval "\$(rbenv init -)"
EOF

# yum install -y git
# git clone https://github.com/sstephenson/ruby-build.git /opt/rbenv/plugins/ruby-build
# chmod -R 775 /opt/rbenv
# source /etc/profile.d/rbenv.sh
# rbenv install 2.5.1
# rbenv rehash
# rbenv global 2.5.1

# ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]

# gem install roadworker
(中略)
# roadwork -v
roadwork 0.5.8
```
無事roadworkerが入りました。

現在のroute53の情報を、Routefileにダウンロードします。

```bash:Routefileのダウンロード
# cd
# roadwork -e -o Routefile

# head Routefile
# -*- mode: ruby -*-
# vi: set ft=ruby :
hosted_zone "your.domain." do
  rrset "syour.domain.", "A" do
    ttl 300
    resource_records(
      "XXX.XXX.XXX.XXX"
    )
  end
```
上手くダウンロードできています。

XXX.XXX.XXX.XXXからYYY.YYY.YYY.YYYに変えて、差分を見て、適用してみます。

```bash:Routefileの編集
# cp -p Routefile Routefile.original
# vim Routefile

# roudwork -t -f Routefile
(中略)
/opt/rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/net-dns2-0.8.7/lib/net/dns/rr/classes.rb:35: warning: constant ::Fixnum is deprecated
./opt/rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/net-dns2-0.8.7/lib/net/dns/rr/types.rb:127: warning: constant ::Fixnum is deprecated
(中略)
```
と思ったら差分比較ですごくたくさんのエラー。
Fixnumとやらがdeprecatedだそうです。エラーではないけれどもひたすら出まくるので気になります。
検索すると2.4.0から出るらしいです。

rubyを古めの2.3.7にしてテストし直します。

```bash:2.3.7でテスト
# rbenv install 2.3.7
# rbenv rehash
# rebenv global 2.3.7
# gem install roadworker
(中略)
# roadwork -t -f Routefile
# roadwork -t -f Routefile
.............FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
your.domain. A:
  expected=XXX.XXX.XXX.XXX(0)
  actual=XXX.XXX.XXX.XXX(60)
XXXX examples, XXXX failures
```
5分くらいかかります。
Fがエラーなのはなんとなく想像が付きます。

エラーの出ているレコードを見ると、TTLを0にしているレコードがエラーを出しているようです。

https://github.com/codenize-tools/roadworker/blob/master/lib/roadworker/dsl-tester.rb
このソースによると、

```bash:dsl-tester.rb
179:if is_same
180:  unless actual_ttls.all? {|i| i <= expected_ttl }
181:    is_same = false
182:  end
183:end
```
このあたりでfalseと判定されてそうです。

```bash:dsl-tester.rb
85:expected_ttl = fetch_dns_name(record.dns_name) ? 60 : record.ttl

97:actual_ttls = response.answer.map {|i| i.ttl }
```
結果の画面はexpectedは0が入っているっぽいので、97行目のactualに60が入ってしまうのがおかしいようにも思いますがよくわかりません。
人生初のプルリクを送るチャンスかと思いましたが、rubyはわからないので遠慮しておきます。

diffで変えた所以外に違いがなければOKとします。

```bash:編集したファイルの差分を比較
diff Routefile.original Routefile
# diff Routefile Routefile3
...c...
<      "XXX.XXX.XXX.XXX"
---
>      "YYY.YYY.YYY.YYY"
```
dry-runでチェックします。

```bash:roadworker-dry-run
# roadwork -a -f Routefile --dry-run                                                                     
Apply `Routefile` to Route53 (dry-run)
Update ResourceRecordSet: your.domain. A (dry-run)
  resource_records:
    -[{:value=>"XXX.XXX.XXX.XXX"}]
    +[{:value=>"YYY.YYY.YYY.YYY"}] (dry-run)
No change
```
dry-run問題ないようです。
本適用してみます。

```bash:roadwork本適用
# roadwork -a -f Routefile                                                                               
Apply `Routefile` to Route53
Update ResourceRecordSet: your.domain. A 
  resource_records:
    -[{:value=>"XXX.XXX.XXX.XXX"}]
    +[{:value=>"YYY.YYY.YYY.YYY"}]

# dig +noall +ans your.domain @ns-xxxxx.awsdns-56.co.uk                                  
your.domain.        0       IN      A       YYY.YYY.YYY.YYY
```
変更後のDNSで引けているようです。

どうやらテストではなくdry-runでテストしたほうが時間もかからず良さげで早くて非常に便利です。

splitも試してみました。

```bash:splitしてみる
# mkdir split ; cd split
# roadwork -e --split -o Routefile

# ls 
your1.domain.route your2.domain.route Routefile

# cat Routefile
# -*- mode: ruby -*-
# vi: set ft=ruby :
require 'your1.domain.route'
require 'your2.domain.route'
```

個別になったyour1.domain.routeを編集して、Routefileを対象に編集すれば良いようです。

多くのDNSを一度に更新するとエラーが出ました。
根気よく```roadwork -a -f Routefile  ```を続ければそのうち終わります。

```
[ERROR] Aws::Route53::Errors::Throttling: Rate exceeded
```


### route53からBINDへ

route53からBINDのゾーンファイルへ変換する方法は、```jq```コマンドで整形すれば簡単にいきそうです。

```
HostedZonesInfo=$(aws route53 list-hosted-zones)
HostedZonesNames=$(echo ${HostedZonesInfo} | jq -r '.HostedZones[].Name')
BINDDIR=bind

mkdir ./${BINDDIR}
echo "${HostedZonesNames}" | while read zonename
do
  hostedzoneid=$(echo ${HostedZonesInfo} | jq -r ".HostedZones[] | select (.Name == \"$zonename\") | .Id" | cut -d'/' -f3 )
  aws route53 list-resource-record-sets --hosted-zone-id ${hostedzoneid} --output json | \
  jq -jr '.ResourceRecordSets[] | "\(.Name) \tIN\t\(.Type) \t\(.ResourceRecords[].Value)\n"' \ 
  > ./${BINDDIR}/named.$${zonename}
done
```

参考資料
[Exporting DNS zonefile from Amazon Route 53](https://stackoverflow.com/questions/20337749/exporting-dns-zonefile-from-amazon-route-53)