kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Aurora v1.13 から使える SELECT INTO OUTFILE S3 を試した

Aurora v1.14 の強制アップデートがあったりして,改めて Aurora のリリースノートを読み直していたら,Aurora v1.13 から SELECT INTO OUTFILE S3 が使えるようになっていることに気付いた(今さら!).今まで試したことが無かったので,実際に試してみた.

大きめのデータ抽出が必要なときなど,MySQL だと SELECT INTO OUTFILE でファイルに書き出すシチュエーションがあるため,Aurora でも使えるようになるのは非常に良いことだと思う.むしろ v1.13 まで無かったんだ...?という感じ.

docs.aws.amazon.com

事前準備

S3 と IAM ロールを作成する

事前に RDS 用の IAM ロールと IAM ポリシーを用意しておく.必要最小限のアクションは以下のドキュメントにまとまっている.今回は SELECT INTO OUTFILE S3 に必要なアクションだけを許可した.あと S3 も作成しておく.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1506948517000",
            "Effect": "Allow",
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListMultipartUploadParts",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::xxx",
                "arn:aws:s3:::xxx/*"
            ]
        }
    ]
}

docs.aws.amazon.com

Aurora クラスターに IAM ロールをアタッチする

次に Aurora クラスターに IAM ロールをアタッチする必要があり,今回は管理コンソールで実施した.これだけではダメで,さらにパラメータグループ(クラスター)も修正する必要がある.今回は aurora_select_into_s3_role パラメータに作成した IAM ロールの ARN を登録した.詳しくはドキュメントに載っている.

docs.aws.amazon.com

ちなみに aurora_select_into_s3_role パラメータを修正し忘れると,以下のエラーが出る.

ERROR 1871 (HY000): S3 API returned error: Both aurora_select_into_s3_role and aws_default_s3_role are not specified, please see documentation for more details

GRANT

基本的に RDS のマスタユーザーを使うことはないと思うので,SELECT INTO OUTFILE S3 を実行するユーザーに SELECT INTO S3 権限を付与しておく必要がある.ここは公式ドキュメントの通りで良い.

mysql> GRANT SELECT INTO S3 ON *.* TO user@domain-or-ip-address

サンプルデータ

MySQL が公開している world database を使う.

world.sql を流し込んで,3テーブル存在していれば,問題なし.

mysql> SHOW TABLES;
+-----------------+
| Tables_in_world |
+-----------------+
| city            |
| country         |
| countrylanguage |
+-----------------+
3 rows in set (0.01 sec)

SELECT INTO OUTFILE S3

事前準備が多くなってしまったが,やっと本題に入る.既に用意している city テーブルのデータを S3 に出力してみた.

mysql> SELECT * FROM city INTO OUTFILE S3 's3://xxx/city';

S3 にファイルを出力できた.

$ aws s3 ls xxx
2017-10-02 22:34:29     143565 city.part_00000

ファイルも問題なく確認できた.

$ head -n 10 ~/city.part_00000
1  Kabul   AFG Kabol   1780000
2  Qandahar    AFG Qandahar    237500
3  Herat   AFG Herat   186800
4  Mazar-e-Sharif  AFG Balkh   127800
5  Amsterdam   NLD Noord-Holland   731200
6  Rotterdam   NLD Zuid-Holland    593321
7  Haag    NLD Zuid-Holland    440900
8  Utrecht NLD Utrecht 234323
9  Eindhoven   NLD Noord-Brabant   201843
10 Tilburg NLD Noord-Brabant   193238

ポイントなど

詳しくは公式ドキュメントに記載されているが,カスタマイズ可能なパラメータが一部ある.

docs.aws.amazon.com

S3 バケットのリージョン指定

出力する S3 のバケットが Aurora と別のリージョンにある場合,以下のようにリージョンを指定する構文がサポートされている.

s3-region://bucket-name/file-prefix

具体的には以下のようになる.

  • s3-us-west-1://
  • s3-ap-northeast-1://

ただし,デフォルトでは Aurora と同じリージョンになるため,特殊な要件がない限りはそのままで良いと思う.

region 値を指定しないと、Aurora は DB クラスターと同じリージョンの Amazon S3 にファイルを保存します。

FIELDS TERMINATED BYLINES TERMINATED BY

デフォルトではタブ区切りで出力されるが,必要に応じてカンマ区切りなどに変更することもできる.

mysql> SELECT * FROM city INTO OUTFILE S3 's3://xxx/city2'
    FIELDS TERMINATED BY ','
    LINES TERMINATED BY '\n';

S3 にファイルを出力できた.

$ aws s3 ls xxx
2017-10-02 22:34:29     143565 city.part_00000
2017-10-02 22:38:34     143565 city2.part_00000

ファイルもカンマ区切りになっていた.

$ head -n 10 ~/city2.part_00000
1,Kabul,AFG,Kabol,1780000
2,Qandahar,AFG,Qandahar,237500
3,Herat,AFG,Herat,186800
4,Mazar-e-Sharif,AFG,Balkh,127800
5,Amsterdam,NLD,Noord-Holland,731200
6,Rotterdam,NLD,Zuid-Holland,593321
7,Haag,NLD,Zuid-Holland,440900
8,Utrecht,NLD,Utrecht,234323
9,Eindhoven,NLD,Noord-Brabant,201843
10,Tilburg,NLD,Noord-Brabant,193238

マニフェストファイル

MANIFEST ON を指定すると .manifest というファイルも合わせて生成される.LOAD DATA FROM S3 コマンドを使って S3 から Aurora にデータをロードする場合に必要になる.なお,出力されるファイルの閾値は 6GB なので,巨大なテーブルを出力する以外は,基本的には1ファイルになると思う.

mysql> SELECT * FROM city INTO OUTFILE S3 's3://xxx/city3'
    FIELDS TERMINATED BY ','
    LINES TERMINATED BY '\n'
    MANIFEST ON;

S3 にファイルを出力できた.

$ aws s3 ls xxx
2017-10-02 22:34:29     143565 city.part_00000
2017-10-02 22:38:34     143565 city2.part_00000
2017-10-02 22:39:46        114 city3.manifest
2017-10-02 22:39:46     143565 city3.part_00000

マニフェストファイルは以下のような構造になっていた.

$ cat ~/city3.manifest
{
   "entries" : [
      {
         "url" : "s3://xxx/city3.part_00000"
      }
   ]
}

参考情報

SELECT INTO OUTFILE S3 を使うときに気になる secure_file_priv の値を Aurora で確認した.

mysql> SELECT @@aurora_version;
+------------------+
| @@aurora_version |
+------------------+
| 1.14.1           |
+------------------+
1 row in set (0.01 sec)

mysql> SELECT @@secure_file_priv;
+--------------------+
| @@secure_file_priv |
+--------------------+
| /tmp/              |
+--------------------+
1 row in set (0.00 sec)

まとめ

  • Aurora v1.13 から SELECT INTO OUTFILE S3 が使えるようになった
  • Aurora クラスターに IAM ロールをアタッチして,aurora_select_into_s3_role パラメータを修正する必要がある
  • データ抽出が必要なときに「Aurora → S3 に出力できる」と知っておくと良さそう

FuelPHP のセッション情報に含まれる previous_id と rotated_session_id の用途を調べた

FuelPHP のセッション管理で少し気になる部分があったため,フレームワークの実装を読んでみた.公式ドキュメントには詳細に書かれていないけど,知っておくと良さそうな仕様を知ることができた.なお,今回は FuelPHP 1.8 を前提にしている.

設定

まず,セッション管理用の設定ファイル config/session.php の重要なパラメータを紹介する.パラメータは他にもあって,詳細は公式ドキュメントに書いてある.

match_ua

セッションを照合するときに,ユーザーエージェントが異なるとセッションを破棄するという設定で,デフォルトでは true になっている.基本的には true にしておくのが良さそう.

expiration_time

パラメータ名の通り,セッションを破棄するまでの秒数を意味している.デフォルトでは 7200秒 = 2時間 になっている.なお,ドキュメントには記載がないが,セッションストアに Memcached を使う場合 2592000秒 = 30日 が上限となり,自動的に丸められる.これは Memcached の仕様と言える.

// adjust the expiration time to the maximum possible for memcached
$this->config['expiration_time'] = min($this->config['expiration_time'], 2592000);

rotation_time

公式ドキュメントには,セッションハイジャックを防ぐために rotation_time で指定した間隔で自動的にセッションを更新すると書かれている.デフォルトでは 300秒 = 5分 となる.今回気になったのは,このローテーションの仕組みとなる.

ローテートの仕組みを確認した

ローテートの仕組みを確認するために Session_Driver クラスの rotate メソッドを読んでみた.

すると,セッションを破棄するのではなく $this->keys['previous_id'] に一度退避をしてから $this->keys['session_id'] に新規セッションを登録していることがわかった.previous_id の値を活用する実装はフレームワークの中で見つけることはできなかったけど,少なくとも「1世代前のセッションを保持している」という事実を知ることができた.

/**
 * force a session_id rotation
 *
 * @param  bool $force  if true, force a session id rotation
 * @return \Session_Driver
 */
public function rotate($force = true)
{
    // do we have a session?
    if ( ! empty($this->keys))
    {
        // existing session. need to rotate the session id?
        if ($force or ($this->config['rotation_time'] and $this->keys['created'] + $this->config['rotation_time'] <= $this->time->get_timestamp()))
        {
            // generate a new session id, and update the create timestamp
            $this->keys['previous_id'] = $this->keys['session_id'];
            $this->keys['session_id']  = $this->_new_session_id();
            $this->keys['created']         = $this->time->get_timestamp();
            $this->keys['updated']     = $this->keys['created'];
        }
    }
    return $this;
}

セッション取得の仕組みを確認した

次にセッション取得の仕組みを確認するために Session_Memcached クラスの read メソッドを読んでみた.今回は Memcached を前提にしている.

詳細は割愛するとして,気になったのは以下の部分だった.

if (isset($payload['rotated_session_id']))
{
    $payload = $this->_read_memcached($payload['rotated_session_id']);

Memcached を dump すると,セッション情報とは別に,以下のように破棄されたはずのセッション情報だけを保持しているキーが存在する.先ほどの例で言うとキーの値が previous_id と一致し,rotated_session_id の値が session_id と一致している.ようするに,1世代前のセッションから,最新のセッションを引き戻すことができるという意味になる.サンプルコードを使って動作確認をしてみたら,確かにそのような動きになっていた.この仕様は「セッションハイジャックを防ぐ」という目的とコンフリクトしているのではないか?どうなんだろう.

add key_11111111111111111111111111111111 0 1483196400 72
a:1:{s:18:"rotated_session_id";s:32:"22222222222222222222222222222222";}

add key_22222222222222222222222222222222 0 1483197000 72
a:1:{s:18:"rotated_session_id";s:32:"33333333333333333333333333333333";}

他にも,セッション情報の updated の値を見て,既に有効期限を過ぎている場合はエラーを返していたり,match_ipmatch_ua の一致確認をしていたり,読んでいて非常に勉強になるクラスだった.

/**
 * read the session
 *
 * @param  bool $force  set to true if we want to force a new session to be created
 * @return \Session_Driver
 */
public function read($force = false)
{
    // initialize the session
    $this->data = array();
    $this->keys = array();
    $this->flash = array();
    // get the session cookie
    $cookie = $this->_get_cookie();
    // if a cookie was present, find the session record
    if ($cookie and ! $force and isset($cookie[0]))
    {
        // read the session file
        $payload = $this->_read_memcached($cookie[0]);
        if ($payload === false)
        {
            // cookie present, but session record missing. force creation of a new session
            return $this->read(true);
        }
        // unpack the payload
        $payload = $this->_unserialize($payload);
        // session referral?
        if (isset($payload['rotated_session_id']))
        {
            $payload = $this->_read_memcached($payload['rotated_session_id']);
            if ($payload === false)
            {
                // cookie present, but session record missing. force creation of a new session
                return $this->read(true);
            }
            else
            {
                // unpack the payload
                $payload = $this->_unserialize($payload);
            }
        }
        if ( ! isset($payload[0]) or ! is_array($payload[0]))
        {
            logger('DEBUG', 'Error: not a valid memcached payload!');
        }
        elseif ($payload[0]['updated'] + $this->config['expiration_time'] <= $this->time->get_timestamp())
        {
            logger('DEBUG', 'Error: session id has expired!');
        }
        elseif ($this->config['match_ip'] and $payload[0]['ip_hash'] !== md5(\Input::ip().\Input::real_ip()))
        {
            logger('DEBUG', 'Error: IP address in the session doesn\'t match this requests source IP!');
        }
        elseif ($this->config['match_ua'] and $payload[0]['user_agent'] !== \Input::user_agent())
        {
            logger('DEBUG', 'Error: User agent in the session doesn\'t match the browsers user agent string!');
        }
        else
        {
            // session is valid, retrieve the rest of the payload
            if (isset($payload[0]) and is_array($payload[0]))
            {
                $this->keys  = $payload[0];
            }
            if (isset($payload[1]) and is_array($payload[1]))
            {
                $this->data  = $payload[1];
            }
            if (isset($payload[2]) and is_array($payload[2]))
            {
                $this->flash = $payload[2];
            }
        }
    }
    return parent::read();
}

類似した Issue を発見した

2011年の Issue で,FuelPHP のバージョンも大きく違うけど,コメントを読む限りは previous_id の扱いは昔から大きく変わっていないように感じた.論点としては rotation_time を超えた Ajax を呼び出したときを考慮しているという話だけど,そこまで長時間走る Ajax があったとしても,例えば php-fpm の request_terminate_timeout を設定していれば自動的にリクエストがタイムアウトするだろうし,設定で回避する方法もあるのでは?

github.com

memcached-tool

Memcached のデータを見るために memcached-tool を使った.実装を読むと dump のデータ構造も理解することができた.

$ memcached-tool localhost:11211 dump > memcached.dump

まとめ

  • FuelPHP 1.8 の実装を読んでみた
  • セッションをローテートするときに previous_id として「1世代前」のセッションを保持している
  • セッションを取得するときに rotated_session_id の値を使うため「1世代前」のセッションから「最新」のセッションを引き戻すことができる
  • FuelPHP の Issue を見たら,長時間走る Ajax の考慮という説明がされていた

関連記事

d.hatena.ne.jp

セルフアウェアネスを高めよう /「99%の人がしていないたった1%のメンタルのコツ」を読んだ

IBM 時代にお世話になった河野さんのシリーズ最新刊「99%の人がしていないたった1%のメンタルのコツ」が 9/14 に出版されたので,さっそく読んだ.出版おめでとうございます!改めて,自分自身を客観的に見直すことができたような気がする.

99%の人がしていないたった1%のメンタルのコツ

99%の人がしていないたった1%のメンタルのコツ

  • 作者: 河野英太郎,田中ウルヴェ京
  • 出版社/メーカー: ディスカヴァー・トゥエンティワン
  • 発売日: 2017/09/14
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

セルフアウェアネス

「自分のモチベーションを見つめ直す」という話が最初に出てくる.内発的動機と外発的動機に関しては「モチベーション3.0」に詳しく書いてあるけど,自分のことを知るのは本当に大切だと思う.また,経験とタイミングによってもモチベーションが推移するので,定期的に見つめ直す機会を作るのが良いと思う.

kakakakakku.hatenablog.com

ちなみに,個人的には「キャリア・アンカー」が好きで,定期的に読み直している.とは言え,何度やっても「奉仕・社会貢献」「純粋な挑戦」のスコアが高いため,僕のモチベーションはそういう方向性にあるんだなと自己認識をしている.

キャリア・アンカー―自分のほんとうの価値を発見しよう (Career Anchors and Career Survival)

キャリア・アンカー―自分のほんとうの価値を発見しよう (Career Anchors and Career Survival)

さらに,セルフアウェアネスとして「苦手な人」と「嫉妬」という話が出てくるのもすごく良かった.組織に所属していると,良くも悪くも「人間は感情に左右される」ことを感じるため,感情を言語化して対処するというのは,合理的なアクションだとは思うけど,いざ実践しようとすると感情的にタフだろうなという印象もある.

目標設定

「マンダラート」と呼ばれるメソッドは初耳だった.今までは基本的にマインドマップを使っていたけど,次に目標を整理する機会があったら試す.目標設定に限らず,自分の強みを洗い出したり,プロダクトの戦略を考えたり,幅広くユースケースがありそうなメソッドだと思う.

navi.dropbox.jp

リラックス

「笑う」ことでリラックスできるというのは前から知っていて,仕事で意識するようにしている.関連することとしては「ノンバーバル・コミュニケーション」を意識していて,常にリアクションをすることで,頭を活性化させている.瞑想の話も出ていて,これは「マインドフルネス」のことだと思うけど,僕がすごく苦手なことの一つだ.考えすぎてしまう性格なので,まずは数秒からでも挑戦してみたい.

感情

「落ち込むことをプラスと考える」という話も良かった.個人的によく落ち込むタイプなので,信頼のできる友人に相談してみたり,週末の過ごし方を工夫してみたり,本書に書かれているコツを積極的に実践してみようと思う.

XP 祭り 2017 で「Fearless Change を活用した組織変革」をテーマに発表してきた

今日は「XP 祭り 2017」で発表してきた!(最近は発表をしまくっている...w)

発表資料

今日の発表タイトルは 「全ては Fearless Change から学んだ,開発組織の変革を支えた実践的アプローチ」 にした.大好きな "Fearless Change" の活用事例を紹介したくて応募したので,採択されて良かった.

スクラムなどの特定の手法に依存せず,どんな組織にでも適用できる実践的なアプローチを紹介したはずなので,参考になれば嬉しいなと.

Fearless Change

全てのパターンを発表では紹介できなかったので,是非「組織変革のバイブル」として読んでみてもらえると!重要なのは,パターンを理解することではなくて,パターンを咀嚼して,自分の組織にフィットしたアプローチを実践することなので,間違えないように!本当にオススメの一冊!

Fearless Change アジャイルに効く アイデアを組織に広めるための48のパターン

Fearless Change アジャイルに効く アイデアを組織に広めるための48のパターン

  • 作者: Mary Lynn Manns,Linda Rising,川口恭伸,木村卓央,高江洲睦,高橋一貴,中込大祐,安井力,山口鉄平,角征典
  • 出版社/メーカー: 丸善出版
  • 発売日: 2014/01/30
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (16件) を見る

44 engineering management lessons

発表で少し紹介をした "44 engineering management lessons" も非常に参考になるため,読んでみてもらえればなと!

kakakakakku.hatenablog.com

他セッション

ワークショップには参加せず,セッションを聞いていた.まず,昼休みに行われてた「野良 LT」を聞いていて,チームの状態が悪いだけなのに「モブプロが悪い」みたいに言われてしまうのは,モブプロに限らず本当によくあるなーと思った.まずコミュニケーションの問題を立て直すことに着目するべきだと思う.逆に言えば,モブプロをしてチームの状態が悪いことに気付けたことに喜ぶべきかなー.

「陣形」の話で「プロのトラブルメーカー」に対して「ボックスワン」で対応する,という話があって笑った.つらい.今までの経験だと,エンジニアリングマネージャーがそういう対応をしている気がする.

まとめ

マネジメント系のコミュニティで発表したいなと思っていたので,今回発表できて良かった.会場は満席だったし,発表も盛り上がっていたように感じたので,少しでも参考になっていれば嬉しいなと.ちなみに冒頭で紹介をしたポッドキャスト omoiyari.fm にも出演をしているので,是非聞いてみてもらえると!

lean-agile.fm

今回「XP 祭り」は初参加だったけど,予想以上にエンプラな発表が多くて,こういうコミュニティなんだなーと感じた.一言に「アジャイル」と言っても,様々な形があり,多様性という意味では勉強になった.また来年も参加したいと思う!

関連記事

8月に July Tech Festa 2017 で発表した資料は以下にある.「DevOps + 組織変革」をテーマにしたため,より技術的な内容になっている.

kakakakakku.hatenablog.com

「パターン28. 経営層の支持者」をテーマに話したインタビュー記事もある!

developers.cyberagent.co.jp

参加者レポート

t-and-p.hatenablog.com

CA.io で「Amazon ES 移行」をテーマに LT してきた

昨日は Elasticsearch をテーマに開催された CA.io に参加してきた.サイバーエージェントグループのマッチング系サービスが対象なのに,友情出演という形で,特別に LT をさせてもらった.楽しかった!

cyberagent.connpass.com

発表資料

タイトルは「Amazon ES に移行をしたら幸せになれるのか?」にした.最近そこそこ大規模で,レガシーな Elasticsearch を Amazon ES に移行するプロジェクトを担当して,そこで学んだことをザックリと話した.LT だと全然時間が足りなくて,ネタっぽくなってしまった感はある.

speakerdeck.com

雰囲気

f:id:kakku22:20170912214903j:plain

補足

自動スナップショット

Amazon ES のダメな点として「自動スナップショットをリストアする場合はサポートに依頼する必要がある」という話をしたら,LT 中に大谷さんから「数日前に改善されたよ!」と指摘をもらって,実際に調べたら本当にリリースが出ていた.ただし,Amazon ES (Elasticsearch 5.5) 以降で使えるらしく,Amazon ES (Elasticsearch 2.3) では,引き続きダメだった.知れて良かった!!!

運用面

今回は時間が無くて話さなかった Amazon ES の運用面の話は去年に作った資料にまとめてある.参考になればと!

kakakakakku.hatenablog.com

Elasticsearch 6.0 is coming / @johtani

  • Elastic Pioneer Program で Elastic 6.0 を使うと粗品がもらえる!
  • Elastic 6.0 の特徴
    • ダウンタイムなしで 5.x から 6.x にアップデートできる
    • Upgrade Assistant
    • ソートクエリのパフォーマンスが改善する (index sorting)
      • 簡単に言うと,インデックスのときに順番を入れ替えてしまう
    • Elastic 7.0 からインデックスの _type を削除するので,警告が出るようになる

メジャーバージョンが上がるたびに進化していて凄いなと思う.Elasticsearch SQL の話を懇親会で大谷さんに聞いたら,6.0 では入らなそうとのことだった.良し悪しはありそうだけど,SQL 形式で書けることのメリットはありそうなので,期待してる.

タップル誕生が Elasticsearch を導入した3つの理由

  • MongoDB から Elasticsearch に移行した
  • 複数の検索項目があり,柔軟に実装できる点がメリット
  • 全文検索も現在実装中

位置情報を用いたElasticsearchの活用事例

  • すれ違いを検索するときに Elasticsearch を使っている
    • geo_point type でデータを格納している
    • geo_distance で検索をしている
  • 逆 Geocoding (緯度経度から住所を検索する)
    • 国土交通省のデータを Elasticsearch に入れている
  • Geo Shape を使うと Elasticsearch で範囲の中に特定の場所があるかを検索できる

Elasticsearch 活用 & 運用事例

  • mimi はマニアックな属性で検索ができるのが特徴
    • 完全一致 (年齢,住所…)
    • スコアマッチ (目/鼻/メガネ…)
  • インデックス再構築
    • エイリアスも reindex も採用しなかった

トルテが実践したマッチしたユーザーを除く3つの方法

  • マッチしたユーザーを除外する
  • Elastic Cloud と Amazon ES を検証した

speakerdeck.com

まとめ

最近は July Tech Festa 2017 / CROSS 2017 など,発表が多くてバタバタしているけど,移行事例を話せて良かった.また他社の事例を聞いて,まだまだ Elasticsearch を活用できるなと感じることができたため,適材適所に技術選定をしていきたい.