AWS SDK for php でRoute53を操作する

やりたいこと

AWS Route53 を aws sdk を用いて php で操作したい。

前提条件

・管理ポリシー
 作成したユーザーの管理ポリシーは以下のようなポリシーをアタッチしておく
– AmazonRoute53DomainsFullAccess
– AmazonRoute53FullAccess

1. phpで AWS CLIのアクセスする準備

セットアップ手順はいくつかありますが、ここではComposerを利用した手順でサクッと準備します。

1-1. Composer のインストール

# curl -sS https://getcomposer.org/installer | php

1-2. 最新版のSDK導入

# php composer.phar require aws/aws-sdk-php

1-3. Composer’s autoloaderの読み込み

実際にphpでSDKを参照するにはautoload.phpを読み込む。

<?php
require ‘vendor/autoload.php’;

2. スクリプトの準備

以下の構成でスクリプトを準備します。

+config.php 設定ファイル(環境によってkey,secret等を設定する。)
+DNSUtil.php Route53制御用のクラス
+test.php テスト呼び出しファイル

2-1. config.php

<?php

class AWSConfig {

// AWS - credentials
  public static $Credential_Key    = "xxxxxxxxxxxxxxxxxxxx";
  public static $Credential_Secret = "****************************************";
  public static $Region = "ap-northeast-1";

// AWS - Route53
  public static $Route53_HostedZoneId = "...";

}

2-2. DNSUtil.php

<?php
/**
 * Route53 を制御してDNSの管理を行います。
 *
 */
require_once (dirname(__FILE__) . '/config.php');
require '/<path to aws-sdk>/vendor/autoload.php';
use AwsRoute53Route53Client;

class DNSUtil {
   /**
    * コンストラクタ
    *
    */
    function __construct() {
        $this->client = Route53Client::factory(array(
            'version'     => 'latest',
            'credentials' => array(
                'key'     => AWSConfig::$Credential_Key,
                'secret'  => AWSConfig::$Credential_Secret,
            ),
            'region' => AWSConfig::$Region,
        ));
    }

   /**
    * レコード一覧取得
    * ここではレコードタイプを指定して一覧取得しています。
    *
    */
    public function getList($type="A") {
        $ret = array();
        try {
            // http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Route53.Route53Client.html#_listResourceRecordSets
            $res = $this->client->listResourceRecordSets(array(
                'HostedZoneId' => AWSConfig::$Route53_HostedZoneId,
            ));
            foreach ($res['ResourceRecordSets'] as $record) {
                $res_type = $record['Type'];
                if ($res_type != $type) {
                    continue;
                }
                $name = $record['Name'];
                $val  = $record['ResourceRecords'][0]['Value'];
                $ret[$name] = $val;
            }
        } catch (Exception $e) {
            // http://docs.aws.amazon.com/aws-sdk-php/v2/api/namespace-Aws.Route53.Exception.html
            echo $e->getMessage();
        }
        return $ret;
    }

   // Aレコードの更新 なければ作成、あれば更新
    public function update_ARecord($domain, $address) {
        // http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Route53.Route53Client.html#_changeResourceRecordSets
        $arrays = array(
            'Action' => 'UPSERT',  // string: CREATE | DELETE | UPSERT
            'ResourceRecordSet' => array(
                'Name' => $domain,
                'Type' => 'A',
                'TTL'  => 86400,
                'ResourceRecords' => array(
                    array(
                        'Value' => $address,
                    ),
                ),
            ),
        );
        try {
            $this->client->changeResourceRecordSets(array(
                'HostedZoneId' => AWSConfig::$Route53_HostedZoneId,
                'ChangeBatch'  => array(
                    'Comment' => 'from my PHP script',
                    'Changes' => array($arrays),
                ),
            ));
        } catch (Exception $e) {
            echo $e->getMessage();
        }
    }

    // Aレコード削除
    public function delete_ARecord($domain, $address) {
        $arrays = array(
            'Action' => 'DELETE',  // string: CREATE | DELETE | UPSERT
            'ResourceRecordSet' => array(
                'Name' => $domain,
                'Type' => 'A',
                'TTL'  => 86400,
                'ResourceRecords' => array(
                    array(
                        'Value' => $address,
                    ),
                ),
            ),
        );
        try {
            $this->client->changeResourceRecordSets(array(
                'HostedZoneId' => AWSConfig::$Route53_HostedZoneId,
                'ChangeBatch'  => array(
                    'Comment' => 'from my PHP script',
                    'Changes' => array($arrays),
                ),
            ));
        } catch (Exception $e) {
            echo $e->getMessage();
        }
    }

}

Route53Clientのインスタンスを生成します。
レコードの更新APIでは、レコードの作成、削除、更新が「CREATE」「DELETE」「UPSERT」
で指定できます。

2-3. test.php

<?php
require_once (dirname(__FILE__) . '/DNSUtil.php');

$obj = new DNSUtil();
// Aレコードの更新・設定
$obj->update_ARecord('hoge1.test.net', '11.22.33.44');
//$obj->delete_ARecord('hoge1.test.net', '11.22.33.44');

// Aレコードの一覧取得
$list = $obj->getList("A");
foreach ($list as $domain => $address) {
    print("domain : $domain -> address:$address n");
}

動作確認

以下のように呼び出してエラーなく、情報が更新、取得できることを確認できる。

# php test.php

参考文献

AWS SDK for PHP ; Installing using Composer
http://docs.aws.amazon.com/aws-sdk-php/v2/guide/installation.html#installing-using-composer

listResourceRecordSets:一覧取得
http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Route53.Route53Client.html#_listResourceRecordSets

changeResourceRecordSets:レコード値の更新
http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.Route53.Route53Client.html#_changeResourceRecordSets

続きを読む

AutoScaling環境においてGitを使わないでEC2を冗長化する方法

制約

Git禁止(協力会社がFTPしか使えないため)

結論

  1. Master Serverを建てる
  2. Master Serverのファイルの更新をlsyncdで監視し、常にS3と同期する
  3. 任意のタイミングでSlave Server(ELB配下の全EC2インスタンス)をS3と同期する

イメージ図

だいたいこんな感じ

AutoScaling環境においてGitを使わないでEC2を冗長化する方法

Master Server編

1. S3バケットを作成する

公式サイトに詳しい方法が書いてあります。
S3 バケットを作成する方法 – Amazon Simple Storage Service

2. AWS CLIを設定する

公式サイトに詳しい方法が書いてあります。
AWS CLI の設定 – AWS Command Line Interface

3. lsyncdをインストールする

EPELリポジトリを利用してインストールします。

$ sudo yum install --enablerepo=epel lsyncd

4. lsyncdの設定を変更する

普通の書き方だとファイル名の末尾に/(スラッシュ)が混入してエラーが出たりうまくsyncしてくれなかったので、
下記のような感じでパースしてあげる必要があります。
komeda-shinjiさんの記事がとても参考になりました。ありがとうございます。

$ sudo vim /etc/lsyncd.conf
/etc/lsyncd.conf

source_dir = "/var/www/source"
s3bucket = "files.hoge.com"
prefix = "source"

snscmd = "echo !!! error "

settings {
    logfile    = "/var/log/lsyncd.log",
    statusFile = "/var/log/lsyncd.status",
    nodaemon = false,
    statusInterval = 1,
    delay = 5,
}

cp = function(event)
  local src_path = event.sourcePathname
  local dst_path = event.targetPathname

  if (string.sub(event.source, -1, -1) == "/") then
      src_path = string.sub(event.source, 1, -2) .. event.pathname
  end
  if (string.sub(event.target, -1, -1) == "/") then
      dst_path = string.sub(event.target, 1, -2) .. event.pathname
  end
  local s3cmd = "aws s3 cp '" .. src_path .. "' '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"


  local runcmd = "rc=0 && [ -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .. " || :"
  spawnShell(event, runcmd)
end

rm = function(event)
  local src_path = event.sourcePathname
  local dst_path = event.targetPathname

  if (string.sub(event.source, -1, -1) == "/") then
      src_path = string.sub(event.source, 1, -2) .. event.pathname
  end
  if (string.sub(event.target, -1, -1) == "/") then
      dst_path = string.sub(event.target, 1, -2) .. event.pathname
  end

  local s3cmd = "aws s3 rm '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"
  local runcmd = "rc=0 && [ ! -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .. " || :"
  spawnShell(event, runcmd)
end

mv = function(event)
  local src_path = event.o.targetPathname
  local dst_path = event.d.targetPathname

  if (string.sub(event.o.target, -1, -1) == "/") then
      src_path = string.sub(event.o.target, 1, -2) .. event.o.pathname
  end
  if (string.sub(event.d.target, -1, -1) == "/") then
      dst_path = string.sub(event.d.target, 1, -2) .. event.d.pathname
  end
  local s3cmd = "aws s3 mv '" .. src_path .. "' '" .. dst_path .. "'"
  local msg_body = "command failed: " .. s3cmd
  local msg = " --message '" ..  string.gsub(msg_body, "'", "\"") .. "'"

  local runcmd = "rc=0 && [ -f '" .. src_path .. "' ] && for try in 1 2 3; do " .. s3cmd .. "; rc=$?; [ $rc -eq 0 ] && break; done || " .. snscmd .. msg .." || :"
  spawnShell(event, runcmd)
end

s3sync = {
    maxProcesses = 1,
    onCreate = cp,
    onModify = cp,
    onDelete = rm,
--  onMove = mv,
}

sync {
    s3sync,
    source = source_dir,
    target = "s3://" .. s3bucket .. "/" .. prefix,
}

5. lsyncdを起動する

$ sudo /etc/rc.d/init.d/lsyncd start
$ sudo chkconfig lsyncd on
$ /etc/rc.d/init.d/lsyncd status
lsyncd (pid  12345) is running...

6. Sourceを変更する

$ vim /var/www/source/hoge.txt
/var/www/source/hoge.txt
hogehoge

7. Sourceの変更がS3に反映しているか確認する

$ tailf /var/log/lsyncd.log

Slave Server編

1. AWS CLIを設定する

公式サイトに詳しい方法が書いてあります。
AWS CLI の設定 – AWS Command Line Interface

2. S3のSourceをローカルに同期するシェルスクリプトを作成する

$ vim /usr/local/bin/s3sync.sh
/usr/local/bin/s3sync.sh
#!/bin/sh
SOURCE_DIR="/var/www"
S3_BUCKET="files.hoge.com"
PREFIX="source"

TARGET_MASTER="s3://${S3_BUCKET}/${PREFIX}/"
TARGET_LOCAL="${SOURCE_DIR}/${PREFIX}/"

aws s3 sync ${TARGET_MASTER} ${TARGET_LOCAL} --delete

3. 同期スクリプトをEC2インスタンス起動時に実行するように設定する

$ vim /etc/rc.local
/etc/rc.local
s3sync.sh
exit 0

※ちなみに、rc.localに書かれたスクリプトはroot権限で実行されます。

4. 任意のタイミングで同期スクリプトを実行する

 cronするなり、execするなり

ボツ案

Master Server+rsyncパターン

s3Syncパターン

s3fsパターン

NFSパターン

AMIを最新にしてから起動するパターン

続きを読む

日本語ファイル名との闘い: Rubymine, AWS CLI, Docker for Windows (1) – AWS CLI

3部作

  1. AWS CLI (この記事)
  2. Rubymine (次に書く)
  3. Docker for Windows (その次に書く)

AWS CLI で s3 にファイルあげるときに日本語のファイル名でエラー

以下のような感じのエラーが出たりしたとき

UnicodeEncodeError: 'utf-8' codec can't encode 

とか

Please check your locale settings.  The filename was decoded as: ANSI_X3.4-1968 On posix platforms, check the LC_CTYPE environment variable.

ざっくり手順

  1. たぶんそれはpython2 だから python3 にあげる
  2. やり直す
  3. さらに失敗し、それはファイル名が utf-8 ではなく sjis なのかもしれないとする

CentOS6の場合

  1. python3 へ

    $ yum install python34 # もしくは yum search python | grep 3 ぐらいで探す
    $ yum install python34-setuptools # これは easy_install 使うため
    

    この時点で easy_install が2つか3つになってる

    たとえばこんな感じ
    -rwxr-xr-x 1 root root 323 11月 12 09:38 2010 /usr/bin/easy_install
    -rwxr-xr-x 1 root root 331 11月 12 09:38 2010 /usr/bin/easy_install-2.6
    -rwxr-xr-x 1 root root 334 10月 11 00:17 2016 /usr/bin/easy_install-3.4
    
  2. やりなおす

    $ aws s3 うんたら
    
  3. それでだめなら言語設定ちゃんとする
    sjis なら以下のような感じで

    /etc/sysconfig/i18n
    LANG="ja_JP.sjis"
    LC_CTYPE="ja_JP.sjis"
    SUPPORTED="ja_JP.sjis:ja_JP:ja"
    SYSFONT="latarcyrheb-sun16"
    

    ここで使えるロケールは以下でチェックできる

    $ locale -a
    

    sjis はなかったので、以下のような感じで追加

    $ localedef -f SHIFT_JIS -i ja_JP ja_JP.SJIS
    

参考

続きを読む

s3 syncでリージョンの異なるバケットにデータをコピーする

リージョンが異なるバケットにデータをコピーしようとs3 syncしたら以下のエラーが出力され、コピーされなかった。

A client error (PermanentRedirect) occurred when calling the ListObjects operation: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint: <コピー先バケット>
You can fix this issue by explicitly providing the correct region location using the --region argument, the AWS_DEFAULT_REGION environment variable, or the region variable in the AWS CLI configuration file.  You can get the bucket's location by running "aws s3api get-bucket-location --bucket BUCKET".
Completed 1 part(s) with ... file(s) remaining
Build step 'Execute shell' marked build as failure
Finished: FAILURE

ちなみに使用したコマンドは次の通り

aws s3 sync s3://${SRC_BUCKET}/assets s3://${DST_BUCKET}/assets --delete 

どうやらコピー元とコピー先の両方のリージョンを指定する必要があるらしい。
というわけで次のように--source_regionにコピー元を指定して--regionにコピー先のリージョンを指定して実行したところ、うまくコピーできました。

aws s3 sync s3://${SRC_BUCKET}/assets s3://${DST_BUCKET}/assets --delete --source-region=${SRC_REGION} --region=${DST_REGION}

まぁ例によってドキュメントにはちゃんとかいてあるんですけどね。

続きを読む