Amazon Auroraの先進性を誰も解説してくれないから解説する

TL;DR;

  • Amazon AuroraはIn-Memory DBでもなくDisk-Oriented DBでもなく、In-KVS DBとでも呼ぶべき新地平に立っている。
  • その斬新さたるやマスターのメインメモリはキャッシュでありながらWrite-BackでもなくWrite-Throughでもないという驚天動地。
  • ついでに従来のチェックポイント処理も不要になったのでスループットも向上した。
  • 詳細が気になる人はこの記事をチェキ!

Amazon Aurora

Amazon AuroraはAWSの中で利用可能なマネージド(=運用をAWSが面倒見てくれる)なデータベースサービス。
ユーザーからはただのMySQL、もしくはPostgreSQLとして扱う事ができるのでそれらに依存する既存のアプリケーション資産をそのまま利用する事ができて、落ちたら再起動したりセキュリティパッチをダウンタイムなしで(!?)適用したりなどなどセールストークを挙げだすとキリがないけど、僕はAWSからお金を貰っているわけではないのでそこは控えめにしてAuroraでのトランザクションの永続性について論文から分かる範囲と想像で補った内容を説明していく。

Auroraのアーキテクチャ

AWSの公式資料を取ってこればいくらでもそれっぽい図はあるが、説明に合わせて必要な部分だけ切りだした。

aurora1.png

AZとはAvailability Zoneの事で、AWSのデータセンターで障害が発生した場合に別の故障単位になるよう設計されているユニットの事である。物理的には部屋が分かれているのか建物が分かれているのかわからないが、電源やスイッチは確実に系統が分かれておりミドルウェアのバージョンアップなども分かれているという。それをまたがる形でMasterが一つとSlaveが複数(最大15台)立ち上がる。MasterはDBに対する読み書き両方ができるが、Slaveは読み出ししかできない。典型的なWebサービスは読み出しが負荷の多くを占めるので読み出し可能な複製が複数容易できるのは理にかなっている。

このそれぞれのコンポーネントを繋ぐ矢印はRedo-Logを表している。Redo-Logとは「特定のページを書き換える操作とその内容」が記述されたDBログの最小単位である。一般にDBを複製すると言うと読み書きされるあらゆるデータが複製されるものであるがAuroraではこのRedo-Logしか複製しない点が面白い。論文中にTHE Log IS THE DATABASEとでっかく書いてあるのは恐らくこの辺に由来する。

Masterは普通のMySQL(もしくはPostgreSQL)サーバのように見えてユーザから読み書きがリクエストできる。
InnoDBの代わりのバックエンドのデータストアとして分散KVSが稼働しており、その分散KVSはAZを跨った6多重に冗長化されている。論文中ではKVSだなんて一言も書いていないがストレージバックエンドの説明として理解しやすいのであえてKVSに例える。
6多重のうち4つにまで保存できた段階で永続化完了と見なしユーザに返答する事でレイテンシの短縮を図っている。システムはいろんなノイズで遅れるが、全体の足を引っ張って律速するは決まってstrugglerであり、90パーセンタイルぐらいであれば圧倒的に機敏に返事を返してくるのは巨大システムの常である。
全部の複製が全く同じ情報を持っていないといけないので、仮にログをもらえず取りこぼした複製がいたとしてもMasterに聞き直さず複製同士でGossip通信を行って全部のログを全員が受け取るように取り計らう。
この辺の話はAWSの人の公式スライドにも腐るほど出てくるので僕は詳しく説明しない。

トランザクションの挙動の違い

どれかのDBにとって極端に都合が良いワークロードで比較しても単なるセールストークにしかならない。
複数の方式のDBが明白に異なる挙動をする典型例のワークロードとして「巨大テーブルの全部の行の特定のフラグを立てる」という一つのトランザクションを例に挙げて伝統的なDisk-Oriented DB・In-Memory DB・Auroraの動作を順を追って説明する。

update.png.png
SQL文としてはこんな感じである。

UPDATE table1 SET flag = true;

なおこのtable1はものすごく行数が多い(=縦に長い)とする。

Disk-Oriented DBの挙動

まず巨大テーブル全体を一気にメモリに置くアーキテクチャにはなっておらず、メモリ上に用意したデータベースページ領域にDisk上のDBの一部を複製してくる所から始まる。ここまではMySQLでもPostgreSQLでも同じはず。この文脈でのページとはDBの中身の一部が8KBぐらいの大きさ毎に詰め込まれた連続したメモリ領域であり、OSが提供するメモリぺージとは少し違う。

diskoriented1.png

Disk上のデータを直接一瞬で書き換えることは当然できないので、狭いメモリ空間でLRU等を用いて取り回しながら書き込みの終わった未コミットなダーティページをディスクに書き戻しながら進行する他ない。
だがそんなことをすると、その瞬間にDBのプロセスが強制終了してリスタートした時に未コミットなダーティページがディスク上から読み出し可能な状態で観測される恐れがある。そこで各DBは僕の知る限り以下の挙動をとる。

ARIES

進行しながらRedo-Undo logをディスクに永続化し、もし途中でシステムがリスタートした時はリカバリとしてUndo処理を行う。
aries.png
この図で言うとページ1は未コミットなトランザクションによって既に書き換えられているが、Undo-Redo Logの形で既にWALを永続化しているのでリカバリ可能でありダーティなページはそのままディスクに永続化して構わない。なので空いたスペースに次に更新したいページ5をフェッチしてくる事ができる。

PostgreSQL

 上書きは常に新たなバージョンでの追記操作であり、clogというデータで保存されているトランザクションステータスが commited でない限り読み出しできない。したがって痕跡は物理的にページに残るがデータベースのユーザからは不可視であり問題にならず、いずれバキュームされて物理的にも消失する。postgres.png
この図でいうと、ページ1は物理的にはダーティだが追記がされているだけでありclogのお陰で論理的に他のトランザクションから見えないのであればそのままディスクに永続化されても問題が発生しない。なのでバッファプールからページ1を追い出して、空いた領域にページ5を持ってくる事ができる。

MySQL

ibdataの中に更新前の値が保存されており、ディスクに書き戻される際にはそちらも永続化されるので、リスタート時のリカバリ処理でibdataとテーブルデータを突き合わせて可視なデータがユーザから見えるように整合性を保つ(詳しくないが多分)。

いずれにせよ、トランザクションが走りながらログを記述していく事は変わらない。

In-Memory DBの挙動

全部のデータがメモリに収まる前提を置いて良いのでこちらはだいぶシンプルに収まる。
進行途中でログを書き出す必要は無いし、バッファの中でLRU等を用いてどのページをディスクに書き戻すかなども心配しなくてよい。
トランザクションログを書き出すタイミングは典型的な実装としてはコミット時に一気に書き出す事が多いようだ。

inmemory.png

リスタート時はログデータをスキャンしてデータベースを再構築するので、ユーザから commit が命じられていないトランザクションはログにすら残っておらず、ダーティページはそもそも概念が存在しない。

Auroraの挙動

メモリにもローカルのディスクにもテーブル全体が入りきらない前提で設計されている。
トランザクションの都合上必要なページがMasterのメモリで運良くキャッシュできていない場合、KVSに問い合わせを行いページを持ってくる。
なお、KVSは物理的には6多重で保存しているが論理的には一つのデータが6重に保存されているだけなので論理的には1つのストレージ領域と考えて良い(RAID1を論理的には単一のHDD扱いするのと同様)のでそう書く。

走りながら当然ログも永続化していく。6多重で保存されるのはログも同じだ。驚くべき事にRedo-logしか保存していかない。

当然Masterのメモリには全データ乗らないので、どうにかして処理用にメモリを取り回す必要がある。
そこでMasterは一番使わないと判断したページをKVSに書き戻…さずに捨てる。  もう一度言う、捨てるのだ、キャッシュなのに。

aurora2.png

そんなことをしたらKVSに載ったページは古いままじゃないかと心配になるが、Auroraの分散KVSは単なるストレージではなくてAurora用の専用のロジックが駆動するインテリジェントな分散KVSである。
こいつらはMasterから受け取ったRedo-Logを必要に応じて手元のページに適用(Apply)していく事ができる1

aurora3.png

なんでせっかく作った更新済みPage1を捨ててまで新たにKVS側でログを適用し直すかというと、基本的にAWSにおいてMasterのCPUやネットワーク資源は限られたリソースである一方、KVS側のCPUは相対的に持て余したリソースであり安いこと。さらには後に述べるチェックポイントの簡潔さのために完全にこちら側に倒した設計を行っていると考えている。

Masterがページを問い合わせる場合、バージョン番号もセットで問い合わせるのでそこまでに投げつけたRedo-logをKVS側で適用した最新ホカホカのページが返ってくるのでMasterは手元のメモリに乗っているダーティなページを気兼ねなく任意のタイミングで捨てて構わない。問い合わせの際はトランザクションの識別子を入れて引いてくるので、読んではいけないDirtyなページを獲得することはない。Slaveがページを問い合わせる場合は必ず永続化されたバージョンのものだけを読むようにしている。
ついでに言うとSlaveのページはMasterが6多重な分散KVSの他にSlaveにもRedo-logを投げつける。それを受け取るたびに(恐らくKVSと同じようなロジックで)ログ適用を行い、最新のコミット済みデータが読めるようになっている。ここで気づいた人もいると思うが、MasterはSlaveにログを共有するがその完了を待つとは一言も書いていない。4/6のKVS永続化が完了した時点でユーザにコミットを報告してしまう。なのでMaster側で更新を確認したデータがSlave側で読めるようになるには若干のタイムラグが発生する可能性がある。いわゆるSequential Consistencyである。ミリ秒オーダーなのでHTTPなWebサービスの文脈で大問題になるケースは稀だが覚えておいた方がいいかも知れない。

チェックポイントの挙動の違い

Auroraはシステム全体で見ると、Masterがせっかく更新したページをそのまま複製せずにKVSがログリプレイして再構築する分CPUクロックは無駄になっている。しかし、Masterはページを書き戻す必要が無くなり、更に言うとMasterがチェックポイント処理をする必要もなくなった。なぜならチェックポイント処理は分散KVS側で継続的にページ単位で実施されているからだ。なんだこれは。In-MemoryDBでもDisk-Oriented DBとも違うチェックポイントアーキテクチャだ。それぞれのチェックポイント戦略をここに列挙する。

  • ARIES: Checkpoint-Begin をWALに書いてからその瞬間のDirty Page TableとTransaction Tableを保存して、リスタート時のRedo-Log適用開始ポイントを算出可能にする。
  • MySQL: ダーティなページをディスクに書き出す。ページの境界とブロックストレージのページ境界が一致しない事のほうが普通なのでチェックポイント中に電源が落ちたらページの一部が中途半端に永続化されてしまう。そこで二度書く事によってアトミック性を達成する(Double Write と呼ぶ)
  • PostgreSQL: ダーティなページをディスクに書き出す。ページの境界とブロックストレージのページ境界が一致しない事のほうが普通なのでチェックポイント中に電源が落ちたらページの一部が中途半端に永続化されてしまう。そこでそのチェックポイント後に最初にそのページに触るWALの中にページ(デフォルトで8KB)を丸っと埋め込んで完全性を保障する。
  • In-Memory DB: どこかのタイミングでメモリの内容をモリッとディスクに書き出してリスタート時に整合性を直すSiloRとか、ログを並列スキャンして完全なイメージを生成するFOEDUSとか戦略はまだ多岐に渡っている。
  • Aurora: バックエンドのAuroraストレージが自動でログを適用していく。ページごとにログバッファが付いてて、バッファの長さがしきい値を超えるたびにページへのログ適用が実施される。ログは未コミットのトランザクションの進行中のログも含むがMasterがリスタートしている時点でそのトランザクションはそれ以上進むはずがないのでログを切り詰める(Truncate)。その際には最新の永続化済みのコミット完了のLSNまで復旧する。なおこの復旧処理はMasterが元気に進行している最中であってもバックグラウンドで良しなに実行される。ここのバックグラウンド処理とチェックポイントに差がないのがAuroraの学術的新規性の一つだと思う。

ベンチマーク結果

論文から抜粋すると
bench.png
大きめのインスタンスの場合に性能向上の伸びしろが大きいようだ。

その他

なんか他に工夫ないの

ログ処理周りは大胆に手が加えられており、中でも感心したのはFlush Pipeliningが実装されている。
通常、ログが永続化されるのを待つにはロガーにログ内容を渡して、完了が報告されるまでセマフォなどで寝るのが典型的な実装パターンである。しかしAuroraではロガーにログ内容を渡した後に、クライアントに完了を報告せよというキューに依頼を投げ込むだけで、そのスレッドは即座に次のリクエストを捌く処理に移行する。ログを4/6多重で保存した後で、キューの中身を確認する専用のスレッドが居て、今回永続化されたログのLSNとキューに登録された依頼を見比べて、永続化されたコミットの完了をクライアントに報告する。
PostgreSQLでもpgbenchでベンチマークを取ってイジメてみるとすぐにセマフォ処理近辺がボトルネックになるのでこの辺弄っても良さそうな気がするが大改造になるのでコミュニティには歓迎されない気がする。

Aurora Multi-Masterってどうなの

この論文で解説されてる仕組みだとLSNの発行からして複数台のマシンからやってダメなのでログのフォーマットのレベルで改造が加えられてそうな気がする。詳しくは動画で
https://www.youtube.com/watch?time_continue=2620&v=rPmKo2g9znA
どうやらパーティション単位で「テーブルのこの範囲はサーバAがリーダーね!」的に分割統治してMasterを複数用意するようだ。そして自分がMasterじゃないテーブルには一応書き込めるが最終的には調停者が決定するとの事である。更新が競合している場合はWrite性能は上がらないが競合していない場合は性能はよく伸びるらしい。

どんな更新がこれから来るかな

分散KVS側のCPUが安くて空いていて、そいつが保存しているページ内容に対してredo-logを適用できる程度に中身を解釈して動いているので、そいつらに集計系クエリを実行させるのはコストメリットが良さそう。貧者のOLAPとしてJOINが苦手なDB実装がクローズドな世界に君臨する可能性はあると思っている。もしくはredo-logをRedshiftにそのまま投げつけていってSlaveの一つとして稼働するようになるとか。

まとめ

  • Auroraは投げつけられたRedoログをストレージ側でバックグラウンドで適用できるからMasterの負担が減った。なので性能が伸びるようになった。
  • インテリジェントな分散ストレージすげーな!

  1. この処理を6多重全部でやると重いので実は一部のマシンでしかこのApply操作はしないらしい。 

続きを読む

Amazon Aurora(MySQL互換) Auto Scalingで追加されたレプリカ(Reader)インスタンスのバッファキャッシュはどうなる?

…と来れば、やっぱりAuto Scalingで追加されたレプリカ(Reader)インスタンスのバッファキャッシュ(バッファプール・バッファプールキャッシュ)が起動時にウォームアップされるのか、についても知りたいところ(?)。

というわけで、いい加減しつこいですが、検証…というには雑なので、確認をしてみました。

1. Amazon Aurora(MySQL互換) Auto Scalingの設定

いつもの通り、すでにクラスメソッドさんのDevelopers.IOに記事があります。

ありがたやありがたや。

というわけで、私の記事では部分的にスクリーンショットを載せておきます。

※以前、以下の記事を書くときに使ったスナップショットからインスタンスを復元して使いました。

クラスター画面から、Auto Scaling ポリシーを追加しようとすると、
aws_as1.png

新コンソールへのお誘いがあり、
aws_as2.png

先へ進むと見慣れない画面が。
aws_as3.png

Auto Scalingを設定するには、新コンソールでもクラスター画面から。
簡単にスケールするよう、「平均アクティブ接続数」を選択して「3」アクティブ接続を指定します。
aws_as4.png

とりあえずAuto Scalingで作成されたレプリカで確認できればいいので、上限は少な目で。
aws_as5.png

書き忘れましたが、Auto Scaling ポリシーを新規追加する前に、最低1つのレプリカ(Reader)インスタンスを作成しておきます(そうしないと怒られます)。

これで、Auto Scalingの準備ができました。

2. Auto Scalingでレプリカが自動追加されるよう負荷を掛ける

引き続き、クライアントから複数セッションで接続します(とりあえず4つぐらい)。

まずは接続。

クライアント接続
$ mysql -u mkadmin -h test-cluster.cluster-ro-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.10 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

(ここで3つ追加接続)

mysql> SHOW PROCESSLIST;
+----+----------+--------------------+------+---------+------+----------------------+------------------+
| Id | User     | Host               | db   | Command | Time | State                | Info             |
+----+----------+--------------------+------+---------+------+----------------------+------------------+
|  2 | rdsadmin | localhost          | NULL | Sleep   |    2 | delayed send ok done | NULL             |
|  3 | rdsadmin | localhost          | NULL | Sleep   |    2 | cleaned up           | NULL             |
|  4 | rdsadmin | localhost          | NULL | Sleep   |   13 | cleaned up           | NULL             |
|  5 | rdsadmin | localhost          | NULL | Sleep   |  568 | delayed send ok done | NULL             |
|  6 | mkadmin  | 172.31.21.22:43318 | NULL | Query   |    0 | init                 | SHOW PROCESSLIST |
|  7 | mkadmin  | 172.31.21.22:43320 | NULL | Sleep   |   99 | cleaned up           | NULL             |
|  8 | mkadmin  | 172.31.21.22:43322 | NULL | Sleep   |   79 | cleaned up           | NULL             |
|  9 | mkadmin  | 172.31.21.22:43324 | NULL | Sleep   |    9 | cleaned up           | NULL             |
+----+----------+--------------------+------+---------+------+----------------------+------------------+
8 rows in set (0.00 sec)

各セッションで、SQLを実行していきます。

SQL(SELECT)実行
mysql> USE akptest2;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> SELECT s.member_id memb, SUM(s.total_value) tval FROM dept d, member m, sales s WHERE d.dept_id = m.dept_id AND m.member_id = s.member_id AND d.dept_name = '部門015' GROUP BY memb HAVING tval > (SELECT SUM(s2.total_value) * 0.0007 FROM dept d2, member m2, sales s2 WHERE d2.dept_id = m2.dept_id AND m2.member_id = s2.member_id AND d2.dept_name = '部門015') ORDER BY tval DESC;
+-------+---------+
| memb  | tval    |
+-------+---------+
| 28942 | 1530300 |
| 47554 | 1485800 |
(中略)
| 29294 | 1176700 |
| 70092 | 1176300 |
+-------+---------+
41 rows in set (24.33 sec)

(別セッションで)

mysql> SELECT s.member_id memb, SUM(s.total_value) tval FROM dept d, member m, sales s WHERE d.dept_id = m.dept_id AND m.member_id = s.member_id AND d.dept_name = '部門015' GROUP BY memb HAVING tval > (SELECT SUM(s2.total_value) * 0.0007 FROM dept d2, member m2, sales s2 WHERE d2.dept_id = m2.dept_id AND m2.member_id = s2.member_id AND d2.dept_name = '部門002') ORDER BY tval DESC;
(中略)
60 rows in set (0.19 sec)

すると、めでたく(?)レプリカインスタンスが自動的に追加されました!(手動で作成するのと同様、ちょっと時間が掛かりましたが。)
aws_as6.png

※1個目のレプリカインスタンスはAZ-cに作成しましたが、こちらはAZ-aに追加されました。

3. いよいよ確認

そこで、このインスタンスに直接指定で接続してみます。

自動追加されたレプリカインスタンスに接続
$ mysql -u mkadmin -h application-autoscaling-d43255f2-e133-4c84-85a1-45478224fdd2.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 5.6.10 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> USE akptest2;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show variables like 'aurora_server_id';
+------------------+--------------------------------------------------------------+
| Variable_name    | Value                                                        |
+------------------+--------------------------------------------------------------+
| aurora_server_id | application-autoscaling-d43255f2-e133-4c84-85a1-45478224fdd2 |
+------------------+--------------------------------------------------------------+
1 row in set (0.01 sec)

さあ、いよいよ、バッファキャッシュがどうなっているか、確認です!
最初に発行したものと同じSQLを発行してみます。
バッファキャッシュに載っていれば、1秒未満で実行できるはずですが…。

追加インスタンスで確認
mysql> SELECT s.member_id memb, SUM(s.total_value) tval FROM dept d, member m, sales s WHERE d.dept_id = m.dept_id AND m.member_id = s.member_id AND d.dept_name = '部門015' GROUP BY memb HAVING tval > (SELECT SUM(s2.total_value) * 0.0007 FROM dept d2, member m2, sales s2 WHERE d2.dept_id = m2.dept_id AND m2.member_id = s2.member_id AND d2.dept_name = '部門015') ORDER BY tval DESC;
+-------+---------+
| memb  | tval    |
+-------+---------+
| 28942 | 1530300 |
| 47554 | 1485800 |
(中略)
| 29294 | 1176700 |
| 70092 | 1176300 |
+-------+---------+
41 rows in set (24.71 sec)

残念!!
…まあ、予想通りですね。

接続を切ってしばらく待つと、自動追加されたインスタンスは削除されます。
aws_as7.png

※私が試したときには、設定した時間よりはるかに長い時間が経過してから削除が始まりました。安全を見ているのでしょうか?

さて、プロキシが間に入り、コンテナ(多分)でDBノードが構成されるAmazon Aurora Serverlessでは、どうなるんでしょうか?


続きを読む

consul-template & supervisorでプロセスの可視化

こちらはフロムスクラッチ Advent Calendar 2017の9日目の記事です。

はじめに

ポプテピピック

もうすぐ、ポプテピピック始まりますね。
どうも、jkkitakitaです。

概要

掲題通り、consul + supervisordで
プロセス監視、管理に関して、可視化した話します。

きっかけ

どうしても、新規サービス構築や保守運用しはじめて
色々なバッチ処理等のdaemon・プロセスが数十個とかに増えてくると
↓のような悩みがでてくるのではないでしょうか。

  1. 一時的に、daemonをstopしたい
  2. daemonがゾンビになってて、再起動したい
  3. daemonが起動しなかった場合の、daemonのログを見る
  4. daemonが動いているのかどうか、ぱっとよくわからない。
  5. ぱっとわからないから、なんか不安。 :scream:

個人的には
5.は、結構感じます。笑
安心したいです。笑

ツールとその特徴・選定理由

簡単に本記事で取り扱うツールのバージョン・特徴と
今回ツールを選んだ選定理由を記載します。

ツール 特徴 選定理由
supervisor
v3.3.1
1. プロセス管理ツール
2. 2004年から使われており、他でよく使われているdaemon化ツール(upstart, systemd)と比較して、十分枯れている。
3. 柔軟な「プロセス管理」ができる。
4. APIを利用して、プロセスのstart/stop/restart…などが他から実行できる。
1.今までupstartを使っていたが、柔軟な「プロセス管理」ができなかったため。

※ upstartは「プロセス管理」よりかは、「起動設定」の印象。

consul
v1.0.1
1. サービスディスカバリ、ヘルスチェック、KVS etc…
2. その他特徴は、他の記事参照。
https://www.slideshare.net/ssuser07ce9c/consul-58146464
1. AutoScalingするサーバー・サービスの死活監視

2. 単純に使ってみたかった。(笑)

3. 本投稿のconsul-templateを利用に必要だったから(サービスディスカバリ)

consul-template
v0.19.4
1. サーバー上で、consul-templateのdaemonを起動して使用
2. consulから値を取得して、設定ファイルの書き換え等を行うためのサービス
ex.) AutoScalingGroupでスケールアウトされたwebサーバーのnginx.confの自動書き換え
1. ansibleのようなpush型の構成管理ツールだと、AutoScalingGroupを使った場合のサーバー内の設定ファイルの書き換えが難しい。

2. user-data/cloud-initを使えば実現できるが、コード/管理が煩雑になる。保守性が低い。

cesi
versionなし
1. supervisordのダッシュボードツール
2. supervisordで管理されているdaemonを画面から一限管理できる
3. 画面から、start/stop/restartができる
4. 簡易的なユーザー管理による権限制御ができる
1. とにかく画面がほしかった。

2. 自前でも作れるが、公式ドキュメントに載っていたから

3. 他にもいくつかOSSダッシュボードあったが、一番UIがすっきりしていたから。(笑)

実際にやってみた

上記ツールを使って
daemonを可視化するために必要な設定をしてみました。
本記事は、全て、ansibleを使って設定していて
基本的なroleは
ansible-galaxyで、juwaiさんのroleを
お借りしています。
https://galaxy.ansible.com/list#/roles?page=1&page_size=10&tags=amazon&users=juwai&autocomplete=consul

supervisor

クライアント側(実際に管理したいdaemonが起動するサーバー)

supervisord.conf
; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Notes:
;  - Shell expansion ("~" or "$HOME") is not supported.  Environment
;    variables can be expanded using this syntax: "%(ENV_HOME)s".
;  - Comments must have a leading space: "a=b ;comment" not "a=b;comment".

[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)
;chmod=0700                 ; socket file mode (default 0700)
;chown=nobody:nogroup       ; socket file uid:gid owner
;username=user              ; (default is no username (open server))
;password=123               ; (default is no password (open server))

[inet_http_server]         ; inet (TCP) server disabled by default
port=0.0.0.0:9001        ; (ip_address:port specifier, *:port for all iface)
username=hogehoge              ; (default is no username (open server))
password=fugafuga               ; (default is no password (open server))
;セキュリティ観点から、ここのportは絞る必要有。

[supervisord]
logfile=/tmp/supervisord.log        ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB               ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10                  ; (num of main logfile rotation backups;default 10)
loglevel=info                       ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid        ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024                         ; (min. avail startup file descriptors;default 1024)
minprocs=200                        ; (min. avail process descriptors;default 200)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[include]
files=/etc/supervisor.d/*.conf

/etc/supervisor.d/配下に
起動するdaemonを設定します。

daemon.conf
[group:daemon]
programs=<daemon-name>
priority=999

[program:<daemon-name>]
command=sudo -u ec2-user -i /bin/bash -c 'cd /opt/<service> && <実行コマンド>'
user=ec2-user
group=ec2-user
directory=/opt/<service>
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/var/log/<service>/daemon.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/var/log/<service>/daemon.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10


[eventlistener:slack_notifier]
command=/usr/bin/process_state_event_listener.py
events=PROCESS_STATE
redirect_stderr=false
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/var/log/<service>/event_listener.stdout.log
stdout_logfile_maxbytes=2MB
stdout_logfile_backups=10
stderr_logfile=/var/log/<service>/event_listener.stderr.log
stderr_logfile_maxbytes=2MB
stderr_logfile_backups=10
environment=SLACK_WEB_HOOK_URL="xxxxxxx"

eventlistener:slack_notifierは、下記投稿を参考に作成。
https://qiita.com/imunew/items/465521e30fae238cf7d0

[root@test02 ~]# supervisorctl status
daemon:<daemon-name>              RUNNING   pid 31513, uptime 13:19:20
slack_notifier                    RUNNING   pid 31511, uptime 13:19:20

server側(daemonの管理画面を表示するwebサーバー)

supervisord.conf
クライアント側と同様

consul

server側

[root@server01 consul_1.0.1]# pwd
/home/consul/consul_1.0.1

[root@server01 consul_1.0.1]# ll
total 16
drwxr-xr-x 2 consul consul 4096 Dec  3 04:49 bin
drwxr-xr-x 2 consul consul 4096 Dec  3 06:06 consul.d
drwxr-xr-x 4 consul consul 4096 Dec  3 04:50 data
drwxr-xr-x 2 consul consul 4096 Dec  3 04:50 logs

[root@server01 consul.d]# pwd
/home/consul/consul_1.0.1/consul.d

[root@server01 consul.d]# ll
total 16
-rw-r--r-- 1 consul consul 382 Dec  3 06:06 common.json
-rw-r--r-- 1 consul consul 117 Dec  3 04:49 connection.json
-rw-r--r-- 1 consul consul  84 Dec  3 04:49 server.json
-rw-r--r-- 1 consul consul 259 Dec  3 04:49 supervisord.json
/home/consul/consul_1.0.1/consul.d/common.json
{
  "datacenter": "dc1",
  "data_dir": "/home/consul/consul_1.0.1/data",
  "encrypt": "xxxxxxxxxxxxxxx", // consul keygenで発行した値を使用。
  "log_level": "info",
  "enable_syslog": true,
  "enable_debug": true,
  "node_name": "server01",
  "leave_on_terminate": false,
  "skip_leave_on_interrupt": true,
  "enable_script_checks": true, // ここtrueでないと、check script実行できない
  "rejoin_after_leave": true
}
/home/consul/consul_1.0.1/consul.d/connection.json
{
  "client_addr": "0.0.0.0",
  "bind_addr": "xxx.xxx.xxx.xxx", // 自身のprivate ip
  "ports": {
    "http": 8500,
    "server": 8300
  }
}
/home/consul/consul_1.0.1/consul.d/server.json
{
  "server": true, // server側なので、true
  "server_name": "server01",
  "bootstrap_expect": 1 // とりあえず、serverは1台クラスタにした
}
/home/consul/consul_1.0.1/consul.d/supervisord.json
{
  "services": [
    {
      "id": "supervisord-server01",
      "name": "supervisord",
      "tags" : [ "common" ],
      "checks": [{
        "script": "/etc/init.d/supervisord status | grep running",
        "interval": "10s"
      }]
    }
  ]
}

consul自体もsupervisordで起動します。

/etc/supervisor.d/consul.conf
[program:consul]
command=/home/consul/consul_1.0.1/bin/consul agent -config-dir=/home/consul/consul_1.0.1/consul.d -ui // -uiをつけて、uiも含めて起動。
user=consul
group=consul
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul_1.0.1/logs/consul.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul_1.0.1/logs/consul.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

agent側(管理したいdaemonが起動するサーバー側)

/home/consul/consul_1.0.1/consul.d/common.json
{
  "datacenter": "dc1",
  "data_dir": "/home/consul/consul_1.0.1/data",
  "encrypt": "xxxxxxxxxxxxxxx", // server側と同じencrypt
  "log_level": "info",
  "enable_syslog": true,
  "enable_debug": true,
  "node_name": "agent01",
  "leave_on_terminate": false,
  "skip_leave_on_interrupt": true,
  "enable_script_checks": true,
  "rejoin_after_leave": true,
  "retry_join": ["provider=aws tag_key=Service tag_value=consulserver region=us-west-2 access_key_id=xxxxxxxxxxxxxx secret_access_key=xxxxxxxxxxxxxxx"
  // retry joinでserver側と接続。serverのcluster化も考慮して、provider=awsで、tag_keyを指定。
]
  }
/home/consul/consul_1.0.1/consul.d/connection.json
{
  "client_addr": "0.0.0.0",
  "bind_addr": "xxx.xxx.xxx.xxx", // 自身のprivate ip
  "ports": {
    "http": 8500,
    "server": 8300
  }
}
/home/consul/consul_1.0.1/consul.d/daemon.json
{
  "services": [
        {
      "id": "<daemon-name>-agent01",
      "name": "<daemon-name>",
      "tags" : [ "daemon" ],
      "checks": [{
        "script": "supervisorctl status daemon:<daemon-name> | grep RUNNING",
        "interval": "10s"
      }]
    }
  ]
}
/home/consul/consul_1.0.1/consul.d/supervisord.json
{
  "services": [
    {
      "id": "supervisord-agent01",
      "name": "supervisord",
      "tags" : [ "common" ],
      "checks": [{
        "script": "/etc/init.d/supervisord status | grep running",
        "interval": "10s"
      }]
    }
  ]
}

agent側もsupervisordで管理

/etc/supervisor.d/consul.conf
[program:consul]
command=/home/consul/consul_1.0.1/bin/consul agent -config-dir=/home/consul/consul_1.0.1/consul.d // -uiは不要
user=consul
group=consul
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul_1.0.1/logs/consul.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul_1.0.1/logs/consul.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

cesi

image2.png

こちらのrepoから拝借させていただきました :bow:
基本的な設定は、README.mdに記載されている通り、セットアップします。

/etc/cesi.conf
[node:server01]
username = hogehoge
password = fugafuga
host = xxx.xxx.xxx.xxx // 対象nodeのprivate ip
port = 9001

[node:test01]
username = hogehoge
password = fugafuga
host = xxx.xxx.xxx.xxx // 対象nodeのprivate ip
port = 9001

[cesi]
database = /path/to/cesi-userinfo.db
activity_log = /path/to/cesi-activity.log
host = 0.0.0.0

(ansibleのroleにもしておく。)
cesiのコマンドも簡単にsupervisordで管理する様に設定します。

/etc/supervisor.d/cesi.conf
[program:cesi]
command=python /var/www/cesi/web.py
user=root
group=root
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stopasgroup=true
stopsignal=QUIT
stdout_logfile=/root/cesi.stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/root/cesi.stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

スクリーンショット 2017-12-10 1.51.12.png

うん、いい感じに画面でてますね。
ただ、この画面の欠点としてnodeが増えるたびに、
都度、 /etc/cesi.confを書き換えては
webサーバーを再起動しなければならない欠点がありました。
なので
今生きているサーバーは何があるのかを把握する必要がありました。
 → まさにサービスディスカバリ。
そこで、設定ファイルの書き方もある一定柔軟にテンプレート化できる
consul-tamplteの登場です。

consul-template

ここも同様にして、ansibleで導入します。
https://github.com/juwai/ansible-role-consul-template
あとは、いい感じに公式ドキュメントをみながら、templateを書けばok。

[root@agent01 config]# ll
total 8
-rwxr-xr-x 1 root   root    220 Dec  4 05:16 consul-template.cfg
/home/consul/consul-template/config/consul-template.cfg
consul = "127.0.0.1:8500"
wait = "10s"

template {
  source = "/home/consul/consul-template/templates/cesi.conf.tmpl"
  destination = "/etc/cesi.conf"
  command = "supervisorctl restart cesi"
  command_timeout = "60s"
}
/home/consul/consul-template/templates/cesi.conf.tmpl
{{range service "supervisord"}}
[node:{{.Node}}]
username = hogehoge
password = fugafuga
host = {{.Address}}
port = 9001

{{end}}

[cesi]
database = /path/to/cesi-userinfo.db
activity_log = /path/to/cesi-activity.log
host = 0.0.0.0

上記のように、consul-tamplateの中で
{{.Node}}という値を入れていれば
consulでsupervisordのnode追加・更新をトリガーとして
consul-templateが起動し

  1. /etc/cesi.confの設定ファイルの更新
  2. cesiのwebserverの再起動

が実現でき、ダッシュボードにて、supervisordが、管理できるようになります。

また
consul-templateは、daemonとして起動しておくものなので
consul-templateもまた、supervisordで管理します。

/etc/supervisor.d/consul-template.conf
[program:consul-template]
command=/home/consul/consul-template/bin/consul-template -config /home/consul/consul-template/config/consul-template.cfg
user=root
group=root
autostart=true
autorestart=true
redirect_stdout=true
redirect_stderr=true
stdout_logfile=/home/consul/consul-template/logs/stdout.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=10
stderr_logfile=/home/consul/consul-template/logs/stderr.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=10

早速、実際サーバーを立ててみると…

スクリーンショット 2017-12-10 1.48.57.png

うん、いい感じにサーバーの台数が8->9台に増えてますね。
感覚的にも、増えるとほぼ同時に画面側も更新されてるので
結構いい感じです。(減らした時も同じ感じでした。)

めでたしめでたし。

やってみて、感じたこと

Good

  1. 各サーバーのプロセスの可視化できると確かに「なんか」安心する。
  2. サーバー入らずに、プロセスのstart/stop/restartできるのは、運用的にもセキュリティ的にも楽。
  3. supervisordは、探しても記事とかあまりない?気がするが、本当にプロセスを「管理」するのであれば、感覚的には、まぁまぁ使えるんじゃないかと感じた。
  4. consul-templateの柔軟性が高く、consulの設計次第でなんでもできる感じがよい。
  5. 遊び半分で作ってみたが、思ったより評判はよさげだった笑

Not Good

  1. supervisord自体のプロセス監視がうまいことできていない。
  2. まだまだsupervisordの設定周りを理解しきれていない。。。
     ※ ネットワーク/権限/セキュリティ周りのところが今後の課題。。usernameとかなんか一致してなくても、取れちゃってる・・・?笑
  3. consulもまだまだ使えていない。。。
  4. cesiもいい感じだが、挙動不審なところが若干ある。笑
    ※ 他のダッシュボードもレガシー感がすごくて、あまり、、、supervisordのもういい感じの画面がほしいな。
    http://supervisord.org/plugins.html#dashboards-and-tools-for-multiple-supervisor-instances

さいごに

プロセスって結構気づいたら落ちている気がしますが
(「いや、お前のツールに対する理解が浅いだけだろ!」っていうツッコミはやめてください笑)

単純にダッシュボードという形で
「可視化」して、人の目との接触回数が増えるだけでも
保守/運用性は高まる気がするので
やっぱりダッシュボード的なのはいいなと思いました^^

p.s.
色々と設定ファイルを記載していますが
「ん?ここおかしくないか?」というところがあれば
ぜひ、コメントお願いいたします :bow:

続きを読む

AWS Cloud9 のPHP/MySQL を 7.1/5.7 にしてみる

PHP Advent Calendar 2017 の9日目です。

Docker を絡めた内容にすると予告してましたが、がらっと変更してしまいました・・・
新しく選んだテーマは、「AWS Cloud9」です。

AWS Cloud9 とは

「AWS Cloud9」とは、今年の11月末から12月頭にかけて開催された「AWS re:Invent 2017」で発表された新しいサービスです。

Cloud9 自体は以前からサービスされていたもので、2016年7月に Amazon に買収されて、とうとう AWS に統合されたという流れです。

Cloud9 は、ブラウザ上で動作する IDE で、複数の開発言語に対応し、共同作業が可能という特徴があるサービスです。それが、AWSに統合されたということで、IAMベースのユーザ管理や、ネットワークの制御等もできるので、より細かい管理ができるという形になります。

セットアップしてみる

とりあえずは、AWSのアカウントが必要なので、もし持っていない場合は作成する必要があります。アカウントの作成が終われば、「AWS Cloud9」の環境構築となります。

「AWS Cloud9」は現在以下のリージョンのみで提供されています。

  • EU(アイルランド)
  • アジアパシフィック(シンガポール)
  • 米国東部(バージニア北部)
  • 米国東部(オハイオ)
  • 米国西部(オレゴン)

残念ながら、東京リージョンには来ていないので、今回は「米国東部(バージニア北部)」(us-east-1)で試してみます。

welcome 画面

Welcome to AWS Cloud9.png

まずは、「AWS Cloud9」のサービストップの「Create environment」をクリックします。

Step1 Name environment

step1.png

Step1として、環境名(Name)と説明文(Description)を入力して、Step2へ行きます。

Step2 Configure settings

step2.png

Step2では、 作業するための環境設定を行います。

Environment Type としては、以下の2つを選ぶことになります。

  • 新しい EC2 インスタンスをこの環境用に起動する
  • 既存のサーバーに SSH 接続して作業をする

今回は、新しいインスタンスを立てますが、既存サーバーへの接続での共同編集というのも面白そうですね。

Environment Type で新しいインスタンスを使うことを選択した場合は、EC2 のインスタンスタイプを選択します。

また、コストを抑えるための設定があります。デフォルトでは、IDEを閉じてから30分後にインスタンスが停止され、再度IDEを開くとインスタンスが再起動するというものです。

それ以外の設定として、使用する IAM role と ネットワークの設定が行なえます。特に設定しなければ、Cloud9しか制御できない IAM role で、新しい VPC ネットワーク が設定されます。

ちなみに、既存のサーバーに SSH 接続する方を選択すると以下の選択肢になります。

step2-ssh.png

Step3 Review

step3.png

確認画面です。内容に問題がなければ、「Create environment」ボタンを押して、環境作成を開始します。

この画面では、以下のような注意が表示されます。

step3-info.png

「Create environment」ボタンを押すと、環境作成中画面ということで次のような画面になります。環境は、だいたい 2 〜 3 分くらいで作成されました。

build-cloud9.png

「AWS Cloud9」 IDE 画面

cloud9.png

IDE の画面としては、オーソドックスな画面で、左側にソースツリー、右側の上部のメインとなる部分にソース等の表示がありますが、右側の下部がターミナルになっているというのが面白いですね。

ここで、起動したインスタンスの情報を見てみるとこんな感じでした。

uname.png

さらに、起動したインスタンスの PHP と MySQL のバージョンをみてみると・・・

default_php_mysql_version.png

PHP はともかく、MySQLが 5.5 というのがちょっとつらい。

というわけで、アップグレードするためのシェルを準備しました。以下のものをターミナルから実行するとPHPとMySQLがバージョンアップできます。

sh -c "$(curl -fsSL https://gist.githubusercontent.com/kunit/c2cc88d18d4ce9ad972bab2bdc3b6f3f/raw/27f538fe5d21d024f72a6dfbee7563dc7247ad46/aws-cloud9-php71-mysql57.sh)"

実行する sh の内容を貼っておくと以下のような感じです。

(2017/12/10 10:12 追記) 最初書いていたスクリプトは、わざわざ PHP 5.6を削除してましたが、 alternatives の機能を使えば、PHPの切り替えができたので、7.1をインストールして、 alternatives で切り替えるものにしました

https://gist.github.com/kunit/c2cc88d18d4ce9ad972bab2bdc3b6f3f

#!/bin/sh

sudo service mysqld stop
sudo yum -y erase mysql-config mysql55-server mysql55-libs mysql55
sudo yum -y install mysql57-server mysql57
sudo service mysqld start

sudo yum -y install php71 php71-cli php71-common php71-devel php71-mysqlnd php71-pdo php71-xml php71-gd php71-intl php71-mbstring php71-mcrypt php71-opcache php71-pecl-apcu php71-pecl-imagick php71-pecl-memcached php71-pecl-redis php71-pecl-xdebug
sudo alternatives --set php /usr/bin/php-7.1 
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/bin/composer

この sh を実行すると、以下のように、PHP 7.1.11 および MySQL 5.7.20 の環境になります。

upgrade_php_mysql_version.png

では、実際のコードを編集および動作させてみよう

環境を作っただけで満足してしまいそうですが、実際のコードを動かしてみたいと思います。

サンプルとして使用させていただいたのは、CakePHP Advent Calendar 2017 2日目の @tenkoma さんの記事、CakePHP 3 のチュートリアルにユニットテストを追加する (1) のコードです。

AWS Cloud9 のターミナルから、以下のコマンドを実行し、ソースコードの取得及び compose install を行います。(us-east-1 で起動しているので、composer install もさくっと終わります)

git clone https://github.com/tenkoma/cakephp_cms.git
cd cakephp_cms
composer install

そして、サンプルを動かすために、MySQLにテスト用のデータベースを作らないと行けないので、以下のコマンドを実行します。

mysql -u root -e 'CREATE DATABASE test_cake_cms CHARACTER SET utf8mb4;GRANT ALL  ON test_cake_cms.* TO cakephp@localhost IDENTIFIED BY "AngelF00dC4k3~";FLUSH PRIVILEGES;'

あとは、Cloud9 の IDE 上から、 cakephp_cms/config/app.php のテストデータベースの設定部分を編集して、ターミナル上から phpunit を実行すると、次のようになります。

edit_app_and_phpunit.png

ターミナル部分を拡大するとこんな感じです。

phpunit.png

キーバインド

IDEの設定項目がありますが、最初に変更したのがこちら。

keybinding.png

プルダウンの種類的には、以下の4つでした。

  • default
  • Vim
  • Emacs
  • Sublime

ダッシュボード

IDEを開いたタブを閉じても、ダッシュボードに行けば再度 IDE を開き直せます。

dashboard.png

何もせずに、環境構築時に設定した時間が経過したらインスタンスは自動的に停止され、次にIDEを開いたときに再起動されます。

AWS Cloud9 の料金

AWS Cloud9 自体は無料で、使用する EC2 インスタンスに対する課金のみとなります。そういった意味で、IDEを閉じたら、30分後にEC2を自動停止してくれるというのは結構ありがたいですね。

最後に

AWS Cloud9 ですが、ちょっとした共同作業の環境として使えるだけではなく、使いようによってはおもしろい使い方ができるかもと思っています。

普段 PhpStorm という超強力な IDE を使っているので、それとくらべて IDE としての使い勝手はどうなんだということも今後いろいろと試してみたいなと思ったりもしています。

AWS アカウントさえあれば、本当に簡単に起動できるので、みなさんも試してみるのはいかがでしょうか?

明日の担当は、@hanhan1978 さんです。

続きを読む

サービス稼働中のままSQL Serverの領域を拡張/縮小させる

この記事はSilbird Advent Calendar 2017 8日目の記事となります。

弊社では、稼働中のサービスの永続化データ格納先としてAmazon RDS for SQL Serverを利用しています。
その中で経験したDB領域の拡張と縮小について、大きな2つのトラブル事例とその対応内容をご紹介しようと思います。
DB領域の拡張、縮小はサービスを一時的止める(メンテナンスに入れる)た状態でないとできないと思われがちですが、サービス稼働中のまま実行することができます。

[事例1] DB自動拡張中に応答停止

1つめはDBの自動拡張についてです。
DBの初期容量は、想定ユーザー数やアクセス数をもとにある程度余裕を持って見積もっていると思います。しかし、Webサービスの世界ではその見積もりどおりにユーザーが増えていくとは限りません。サービス運営者としては嬉しい悲鳴ですが、ユーザー数・滞在時間の増加によりデータが見積もり以上に容量が増加してしまうケースがあります。

SQL Serverでは初期割り当て時の容量を超えてしまった場合に、領域を自動拡張する機能がデフォルトで有効になっています。しかし、この自動拡張が動いたときに、サービスが停止するトラブルが発生してしまいました。

DB自動拡張のデフォルト設定

下図は、SQL ServerでDBを新規作成しようとしたケースで、初期サイズとして100GBを割り当てています。そして、「自動拡張/最大サイズ」が「10%単位で無制限」となっているのがわかります。
なので、データが初期サイズの100GBを超えた場合に、自動で10GBの領域が自動で作成され、DBの領域は110GBとなります。
image.png

この「10%単位で無制限」というデフォルト設定が曲者で、自動拡張中にサービスからのクエリ要求が応答しない状態になってしまいました。
MSのサポートサイトにも記載がありますが、自動拡張中はトランザクションが停止するようです。実際、SQL Serverが10GBの領域を拡張している間クエリがタイムアウトしていました。

[INF] SQL Server における自動拡張および自動圧縮の構成に関する注意事項

DB自動拡張設定の変更

データは日々拡張していき初期サイズに収まらなくなると、SQL Serverの自動拡張に頼らざるを得ません。サービスをメンテナンスに入れて一気に拡張する方法もありますが、メンテナンスに入れることなく自動拡張の設定を変更することで対応しました。

自動拡張の設定で「自動拡張/最大サイズ」を「100MBで無制限」 とすることで、自動拡張にかかる時間を1秒未満に抑え、サービスへの影響を最小限とすることができました。
image.png

自動拡張の発生履歴

SQL Server Management Studioから直近のDB自動拡張履歴は確認することができます。
DB名を右クリック – レポート – 標準レポート – ディスク使用量 から、現在のディスクの利用状況とともに自動拡張イベント(開始時刻、実行時間、変更後のフィアルサイズ)を確認することができます。

ディスク使用量の概要レポート

自動拡張についてまとめ

  • SQL Serverのデフォルトの自動拡張は「10%単位で無制限」、自動拡張中はトランザクションが停止する
  • 初期サイズが大きいと自動拡張サイズが大きくなり、サービスのダウンタイムが発生する可能性が高くなる
  • 自動拡張の設定を割合(%)から絶対値(MB)とすることで、拡張にかかる時間・サービスへの影響を最小限とすることができる

[事例2] 自動拡張でディスクを圧迫し、拡張不可に

サービスの展開、自動拡張の結果

サービスを複数のプラットフォームに展開し、それに伴ってDBの数を増やし自動拡張を続けた結果、RDSインスタンス作成時に確保したディスク容量を全て使い果たしてしまいました。その時のAWSコンソールから見たときのRDSインスタンスの状況です。(Storage 1MB…)

image.png

データ・トランザクションログ領域ともにこれ以上自動拡張ができない状態になってしまい、特定のDBのデータ更新クエリが全てエラーになる状態になってしまいました。

Amazon RDS for SQL Server の制約

AWS上のクラウドサービスのため、ディスク容量を拡張することで対応できると思われるかもしれませんが、2017年12月時点 RDS for SQL Serverではインスタンス作成時に割り当てたディスク容量を拡張することができません。
そのため、別のインスタンスを作成して移行するか、削除可能なデータがあれば削除して対応する必要があります。

delete文、truncate文がエラーでデータを削除できず

対応にあたってまずやろうとしたことは、いつか使うだろうと思ってため続けていた履歴データの削除です。不要な(サービス稼働に必須ではない)履歴データを削除しようとクエリを実行しましたが、データの削除にもトランザクションログを作成する必要があり、delete, truncate文ともに実行エラーとなってしまいました。
不要なデータの削除はいったん諦めることにしました。

image.png

DB領域の縮小

自動拡張を続けてしまったDBがある一方、初期に割り当てた容量を使い果たしていないDBもありました。そのため、空き領域のあるDBを縮小することで拡張用の領域の確保を試みました。

DBを縮小するコマンドは以下の通りです。

USE [db_name]
DBCC SHRINKFILE (N'db_file_name' , 99000)

コマンドは、DBを右クリック – タスク – 圧縮 – ファイル より
「未使用領域の解放前にページを再構成する」にチェックを入れ、圧縮後のファイルサイズを指定して
「スクリプト」から出力することもできます

shrink.png

このときの注意点としては

ファイルの圧縮は小さい単位(例: 1GB)で複数回実行することです。
一度に大量の圧縮を行うとDBが応答しなくなり、サービスが停止します。例えば150GBのDBを120GBへ圧縮したい場合は 149→148→147・・・ と細かく実行する必要があります。
自動拡張時の苦い教訓があることと、圧縮するDBのサービスは正常に稼働中であったため、細かく圧縮を行いました。圧縮を行うことで、ディスクに空きが生まれ自動拡張が実行されました。
DBの縮小中はトランザクションは停止しないようです。

DB領域の拡張

同時にDB領域の拡張も行いました。自動拡張を続けているDBはこれを機に割り当て領域を拡張して、そもそも自動拡張が発生しないように対応を行いました。

DBを拡張するコマンドは以下の通りです

USE [db_name]
ALTER DATABASE [db_name] MODIFY FILE ( NAME = N'db_file_name', SIZE = 100000000KB )

コマンドは、DBを右クリック – プロパティ – ファイル より、データファイルの初期サイズを変更して、「スクリプト」から出力することもできます。
サービス稼働中に実行するときの注意点としては、圧縮は小さい単位(例: 1GB)で複数回実行することです、もっと大きい単位でも問題なく拡張できるかもしれませんが、1GB単位で実行しました。

不要なデータの削除

一時的にトラブルは解消しましたが、自動拡張を続ける限り今後もディスクの空き容量枯渇のリスクを抱えています。そもそもDBに保持するデータ量を減らすため、一定期間を経過した履歴データは削除を行うようにしました。これによって約30%データ量を削減し、定期的に削除を行うことで今後1年は自動拡張が発生しないような状態となりました。

容量監視の重要さ

この2つの事例はいずれも DBの容量監視をしていれば事前に気づけていました。
恥ずかしながら、このトラブルが発生するまでRDSの空き容量の監視を行っていませんでした。CloudWatch上で簡単に監視できるため、RDSのディスクが一定容量を下回るとSlackへ通知するよう設定を行いました。

まとめ

  • DB自動拡張中はトランザクションが停止する、そのため自動拡張は割合(%)ではなく絶対値(MB)かつ、一度に拡張されるサイズを小さくしたほうがいい
  • DBの手動縮小・拡張は1GB単位など小刻みに行えば、サービス稼働中でも可能
  • 容量監視は大事

続きを読む

AWS GlueでDBのデータをマスキングして出力してみる

はじめに

AWS Glueは2017年8月に発表された、フルマネージドでサーバレスなETLサービスです。
RDSからS3にデータを抽出したり、S3にあるログファイルをカタログに登録してAmazon Athenaで解析したりできます。
現在は、バージニア北部・オハイオ・オレゴンの3つのリージョンのみしかサポートされていませんが、もうまもなく東京リージョンもサポートされるのではないでしょうか。

本記事では、AWS Glueを使用してRDSインスタンスのデータを特定のカラムのみマスキングしてから、CSV形式でS3に抽出してみます!

前提

以下のRDSインスタンスが準備されているものとします。

  • MySQL RDSインスタンス

また、今回は以下のようなテーブルを使用します。
ID, ユーザ名、パスワードの3フィールドのみの簡単なテーブルです。
(まずパスワードが平文で保存されていることはありえませんが。。)

#+---+---------+------------+
#| ID| username|    password|
#+---+---------+------------+
#|  1|    alice|   alicepass|
#|  2|      bob|     bobpass|
#|  3|  charlie| charliepass|
#|  4|     dave|    davepass|
#|  5|    ellen|   ellenpass|
#+---+---------+------------+

また、事前に任意のS3バケットに以下のファイルをアップロードしておいてください。

job.py
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job

## @params: [JOB_NAME]
args = getResolvedOptions(sys.argv, ['JOB_NAME'])

sc = SparkContext()
glueContext = GlueContext(sc)
job = Job(glueContext)
job.init(args['JOB_NAME'], args)

# ---------- ここに書いていく ----------

job.commit()

ETL処理の作成

AWS GlueでETL処理を行うには、主に以下の手順で行います。
(DataCatalogを使用しない場合は、2と3は省略可能です。)

  1. Connectionの作成
  2. Crawlerの作成
  3. Crawlerを実行し、DataCatalogの登録
  4. Jobの作成
  5. JobのPythonスクリプトの記述
  6. Jobの実行

今回のケースでは、DataCatalogの登録にこれといったメリットが感じられないので使用しないことにします。

1. Connectionを作成する

まずはRDSへの接続情報を保持するConnectionを作成してみましょう。

  1. AWS Glueのコンソール画面の左メニューからConnectionsを選択します。
  2. Add connectionボタンをクリックします。
  3. 表示されたフォームに以下の通りに入力します。

    Connection name 任意
    Connection type Amazon RDS
    Database engine MySQL
  4. Nextをクリックします。

  5. フォームのInstance, Database name, Username, Passwordの欄を自分の環境のものに合わせて指定してください。

  6. Nextをクリックします。

  7. 問題なければFinishをクリックしてConnectionを作成します。

2. Jobを作成する

次に、ETL処理を行うJobを作成しましょう。

  1. AWS Glueのコンソール画面の左メニューからJobsを選択します。
  2. Add jobボタンをクリックします。
  3. 表示されたフォームに以下の通りに入力します。

    Name 任意
    IAM role Glue実行用のサービスロール
    This job runs An existing script that you provide
    S3 path where the script is stored 最初にアップロードしたS3のjob.pyファイル
    Temporary directory 任意のS3パス
  4. Nextをクリックします。

  5. Connectionの一覧から先ほど作成したConnectionを探し、Addをクリックします。

  6. Nextをクリックします。

  7. 問題なければFinishをクリックしてJobを作成します。

無事作成が完了するとスクリプトエディタが表示されるかと思います。

3. JobのPythonスクリプトを書く

ここからは具体的にETL処理を行うスクリプトを記述していきます。

データベースに接続する(Extract)

データベースに接続する際には、DynamicFrameというものを作成します。
AWS Glueでは、以下3つの方法で作成することができます。

create_dynamic_frame.from_catalog AWS Glueのデータカタログから作成します
create_dynamic_frame.from_rdd Resilient Distributed Dataset (RDD)から作成します
create_dynamic_frame.from_options JDBCやS3などの接続タイプを指定して作成します

今回は、DataCatalogは使用しないのでcreate_dynamic_frame.from_optionsを使用します。

glueContext.create_dynamic_frame.from_options(
    connection_type='mysql',
    connection_options={
        'url': 'JDBC_URL',
        'user': 'USER_NAME',
        'password': 'PASSWORD',
        'dbtable': 'TABLE_NAME'
    })

connection_typeは、jdbcと思ってしまうところですが、どうやらjdbcではエラーになってしまうようですので、接続先データベースのベンダー名を指定します。今回で言えば、mysqlです。
connection_optionsについては、直接記述するのもいいですが、せっかくConnectionを登録しているので、その情報を利用してみましょう。

Connectionから接続情報を取得

以下のようにして、AWS Glueに登録したConnectionの情報を取得することができます。

glueContext.extract_jdbc_conf(connection_name='CONNECTION_NAME')

この関数を使用することで、次のような情報が取れます。

url JDBCのURL
vendor vendor名
user ユーザ名
password パスワード

ただ、ここで注意があります。
AWS GlueのConnectionに登録されているJDBCのURLは、jdbc:[ベンダー名]://[ホスト名]:[ポート番号]/[データベース名](Oracleの場合は、jdbc:[ベンダー名]:thin://@[ホスト名]:[ポート番号]/[データベース名])となっているのですが、この関数で取れるURLは、jdbc:[ベンダー名]://[ホスト名]:[ポート番号]とデータベース名の部分が欠けていることに注意してください。また、Oracleのみ、jdbc:oracle:thin://@のところが、jdbc:oracle://という形で取得されるので、ここも注意してください。

この情報を用いて、以下のようにDynamicFrameを作成できます。
urlの部分は仕方なく、取得したURLにデータベース名を追加してあります。

job.py
# Connectionの情報を取得
jdbc_conf = glueContext.extract_jdbc_conf(connection_name='CONNECTION_NAME')

# DynamicFrameを作成
dynamicframe = glueContext.create_dynamic_frame.from_options(
                   connection_type='mysql',
                   connection_options={
                       'url': "{0}/{1}".format(jdbc_conf['url'], 'DB_NAME'),
                       'user': jdbc_conf['user'],
                       'password': jdbc_conf['password'],
                       'dbtable': 'TABLE_NAME'
                   })

マスキングしてみる(Transform)

次に、データのマスキングを行なってみましょう。
AWS GlueのビルトインTransformに、Mapというものがありますので、それを使ってマスキングを行います。
今回は、passwordカラムのデータをすべて****************に変えちゃいましょう。

job.py
# マスク用の関数
def mask(dynamicRecord):
    dynamicRecord['password'] = '****************'
    return dynamicRecord

# DynamicFrameにマスク用の関数を適用
masked_dynamicframe = Map.apply(frame=dynamicframe, f=mask)

データをCSV形式で出力する(Load)

最後は、データをCSV形式で書き出してみましょう。
データの書き出しは、以下3つの方法で行えます。

write_dynamic_frame.from_catalog AWS Glueのデータカタログを指定して書き出します
write_dynamic_frame.from_options JDBCやS3などの接続タイプを指定して書き出します
write_dynamic_frame.from_jdbc_conf JDBCオプションを指定して書き出します

今回は、CSV形式でS3に書き出すので、write_dynamic_frame.from_optionsを使用します。
S3のバケットは任意のものを指定してください。

job.py
# DynamicFrameをCSV形式でS3に書き出す
glueContext.write_dynamic_frame.from_options(
    frame=masked_dynamicframe,
    connection_type='s3',
    connection_options={'path': "s3://{0}/{1}".format('CSV_BACKET_NAME', 'TABLE_NAME')},
    format='csv')

最終的なコードはこのようになりました。

job.py
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job

# マスク用の関数
def mask(dynamicRecord):
    dynamicRecord['password'] = '****************'
    return dynamicRecord

## @params: [JOB_NAME]
args = getResolvedOptions(sys.argv, ['JOB_NAME'])

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)


# Connectionの情報を取得
jdbc_conf = glueContext.extract_jdbc_conf(connection_name='CONNECTION_NAME')

# DynamicFrameを作成
dynamicframe = glueContext.create_dynamic_frame.from_options(
                   connection_type='mysql',
                   connection_options={
                       'url': "{0}/{1}".format(jdbc_conf['url'], 'DB_NAME'),
                       'user': jdbc_conf['user'],
                       'password': jdbc_conf['password'],
                       'dbtable': 'TABLE_NAME'
                   })

# DynamicFrameにマスク用の関数を適用
masked_dynamicframe = Map.apply(frame=dynamicframe, f=mask)

# DynamicFrameをCSV形式でS3に書き出す
glueContext.write_dynamic_frame.from_options(
    frame=masked_dynamicframe,
    connection_type='s3',
    connection_options={'path': "s3://{0}/{1}".format('CSV_BACKET_NAME', 'TABLE_NAME')},
    format='csv')


job.commit()

4. Jobの実行

最後にRun JobボタンをクリックしてJobを実行してみましょう。

出力されたCSVを確認するとしっかりとデータがマスクされています!

output.csv
id,username,password
1,alice,****************
2,bob,****************
3,charlie,****************
4,dave,****************
5,ellen,****************

おわりに

AWS Glueを使用してRDSインスタンスのデータを特定のカラムのみマスキングしてから、CSV形式でS3に抽出してみました。
今回はCSV形式での出力を行いましたが、RDSインスタンスのデータベースにデータを書き出すこともできます。ただ、この場合は書き出し先のテーブルの末尾にデータが追加されてしまいますので、もしテーブル内のデータをTruncateしてから書き出したい場合はPySparkのAPIを使用する必要があります。
AWS GlueのDynamicFrameは、PySparkのDataFrameをラップしていますので、DataFrameへの変換が可能です。DataFrameに変換すればもっと柔軟なETL処理を行うことができます。

AWS Glueは、新しいサービスということもあり日々進化しています。昨日できなかったことが今日できたりします。今後、DynamicFrameでもっと多くのことができるようになり、AWS Glueがさらに便利に使えることを期待しています。

参考

(公式)AWS Glue Documentation

続きを読む

AWS 上に JobScheduler を構築した話

AWSには自前でジョブ実行用のサービスがあるので、こういうのはあまりやらないと思いますが、色々とハマったのでメモ。

インストール

JobScheduler インストール

  1. インストーラーをここからダウンロードします。

    スクリーンショット 2017-06-08 0.33.37.png

  2. 解凍した中にある jobscheduler_install.xml を以下の形に修正します。

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!--
    XML configuration file for JobScheduler setup
    
    The JobScheduler is available with a dual licensing model.
    - GNU GPL 2.0 License (see http://www.gnu.org/licenses/gpl-2.0.html)
    - JobScheduler Commercial License (see licence.txt)
    
    The setup asks you for the desired license model
    (see <entry key="licenceOptions" .../> below).
    
    If you call the setup with this XML file then you accept
    at the same time the terms of the chosen license agreement.
    -->
    <AutomatedInstallation langpack="eng">
    <com.izforge.izpack.panels.UserInputPanel id="home">
        <userInput/>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="licences">
        <userInput>
    
            <!-- Select the license model (GPL or Commercial) -->
            <entry key="licenceOptions" value="GPL"/>
    
            <!-- If you selected GPL as license model than the licence must be empty.
                 Otherwise please enter a license key if available.
                 It is also possible to modify the license key later. -->
            <entry key="licence" value=""/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.HTMLLicencePanel id="gpl_licence"/>
    <com.izforge.izpack.panels.HTMLLicencePanel id="commercial_licence"/>
    <com.izforge.izpack.panels.TargetPanel id="target">
    
        <!-- SELECT THE INSTALLATION PATH FOR THE BINARIES AND LIBRARIES
             The installation expands this path with the Scheduler ID as subdirectory.
             The path must be absolute!
             Default paths are
             /opt/sos-berlin.com/jobscheduler for Unix
             C:Program Filessos-berlin.comjobscheduler for Windows -->
        <installpath>/opt/sos-berlin.com/jobscheduler</installpath>
    
    </com.izforge.izpack.panels.TargetPanel>
    <com.izforge.izpack.panels.UserPathPanel id="userpath">
    
        <!-- SELECT THE DATA PATH FOR CONFIGURATION AND LOG FILES
             The installation expands this path with the Scheduler ID as subdirectory.
             The path must be absolute!
             Default paths are
             /home/[user]/sos-berlin.com/jobscheduler for Unix
             C:ProgramDatasos-berlin.comjobscheduler for Windows -->
        <UserPathPanelElement>/home/joc/jobscheduler</UserPathPanelElement>
    
    </com.izforge.izpack.panels.UserPathPanel>
    <com.izforge.izpack.panels.PacksPanel id="package">
    
        <!-- SELECT THE PACKS WHICH YOU WANT INSTALL -->
    
        <!-- Package: JobScheduler
             JobScheduler Basic Installation
             THIS PACK IS REQUIRED. IT MUST BE TRUE -->
        <pack index="0" name="Job Scheduler" selected="true"/>
    
        <!-- Package: Database Support
             Job history and log files can be stored in a database. Database support is
             available for MySQL, PostgreSQL, Oracle, SQL Server, DB2.
             THIS PACK IS REQUIRED. IT MUST BE TRUE -->
        <pack index="2" name="Database Support" selected="true"/>
    
        <!-- Package: Housekeeping Jobs
             Housekeeping Jobs are automatically launched by the Job Scheduler, e.g. to send
             buffered logs by mail, to remove temporary files or to restart the JobScheduler. -->
        <pack index="5" name="Housekeeping Jobs" selected="true"/>
    
    </com.izforge.izpack.panels.PacksPanel>
    <com.izforge.izpack.panels.UserInputPanel id="network">
        <userInput>
            <!-- Network Configuration -->
    
            <!-- Enter the name or ip address of the host on which the JobScheduler is operated -->
            <entry key="schedulerHost" value="localhost"/>
    
            <!-- Enter the port for TCP communication -->
            <entry key="schedulerPort" value="4444"/>
    
            <!-- Enter the port for HTTP communication -->
            <entry key="schedulerHTTPPort" value="40444"/>
    
            <!-- To enter a JobScheduler ID is required.
                 The IDs of multiple instances of the JobScheduler must be unique per server.
                 The JobScheduler ID expands the above installation paths as subdirectory.
                 Please omit special characters like: /  : ; * ? ! $ % & " < > ( ) | ^ -->
            <entry key="schedulerId" value="schedulerId_XXXX"/>
    
            <!-- It is recommended to enable TCP access for the host where the JobScheduler will install,
                 optionally enter additional host names or ip addresses. To enable all hosts in your
                 network to access the JobScheduler enter '0.0.0.0'. -->
            <entry key="schedulerAllowedHost" value="0.0.0.0"/>
    
            <!-- Choose (yes or no) wether the JobScheduler should be started at the end of the installation -->
            <entry key="launchScheduler" value="yes"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="cluster">
        <userInput>
            <!-- Cluster Configuration -->
    
            <!-- The JobScheduler can be installed independent of other possibly JobSchedulers,
                 as a primary JobScheduler in a backup system or as a backup JobScheduler.
                 Use '' for a standalone, '-exclusive' for a primary
                 or '-exclusive -backup' for a backup JobScheduler.
                 A database is required for a backup system. All JobSchedulers in a backup system
                 must have the same JobScheduler ID and the same database.
                 Further you can set '-distributed-orders' for a load balancing cluster.
                 For more information see
                 http://www.sos-berlin.com/doc/de/scheduler.doc/backupscheduler.xml
                 http://www.sos-berlin.com/doc/de/scheduler.doc/distributed_orders.xml -->
            <entry key="clusterOptions" value=""/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="smtp">
        <userInput>
            <!-- Mail Recipients Configuration / SMTP Authentication -->
    
            <!-- Enter the ip address or host name and port (default: 25) of your SMTP server -->
            <entry key="mailServer" value=""/>
            <entry key="mailPort" value="25"/>
    
            <!-- Configure the SMTP authentication if necessary. -->
            <entry key="smtpAccount" value=""/>
            <entry key="smtpPass" value=""/>
    
            <!-- Enter the addresses of recipients to which mails with log files are automatically
                 forwarded. Separate multiple recipients by commas -->
    
            <!-- Account from which mails are sent -->
            <entry key="mailFrom" value=""/>
    
            <!-- Recipients of mails -->
            <entry key="mailTo" value=""/>
    
            <!-- Recipients of carbon copies: -->
            <entry key="mailCc" value=""/>
    
            <!-- Recipients of blind carbon copies -->
            <entry key="mailBcc" value=""/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="email">
        <userInput>
            <!-- Mail Configuration / Event Handler -->
    
            <!-- Choose in which cases mails with log files are automatically forwarded. -->
            <entry key="mailOnError" value="no"/>
            <entry key="mailOnWarning" value="no"/>
            <entry key="mailOnSuccess" value="no"/>
    
            <!-- The Housekeeping package is required for configure JobScheduler as event handler
                 Choose this option if you intend to use JobScheduler Events and
                 - this JobScheduler instance is the only instance which processes Events
                 - this JobScheduler instance is a supervisor for other JobSchedulers which submit Events -->
            <entry key="jobEvents" value="off"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="database">
        <userInput>
            <!-- JobScheduler Database Configuration -->
    
            <!-- Choose the database management system. Supported values are 'mysql' for MySQL,
                 'oracle' for Oracle, 'mssql' for MS SQL Server, 'pgsql' for PostgreSQL,
                 'db2' for DB2 and 'sybase' for Sybase. -->
            <entry key="databaseDbms" value="mysql"/>
    
            <!-- You can choose between 'on' or 'off' to create the database tables.
                 If you have modified the initial data of an already existing installation,
                 then the modifications will be undone. Data added remains unchanged.
                 This entry should be only 'off', when you sure, that all tables are already created. -->
            <entry key="databaseCreate" value="on"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="dbconnection">
        <userInput>
            <!-- JobScheduler Database Configuration -->
    
            <!-- Enter the name or ip address of the database host -->
            <entry key="databaseHost" value="the host of RDS"/>
    
            <!-- Enter the port number for the database instance. Default ports are for MySQL 3306,
                 Oracle 1521, MS SQL Server 1433, postgreSQL 5432, DB2 50000, Sybase 5000. -->
            <entry key="databasePort" value="3306"/>
    
            <!-- Enter the schema -->
            <entry key="databaseSchema" value="jobscheduler_data"/>
    
            <!-- Enter the user name for database access -->
            <entry key="databaseUser" value="databaseUser"/>
    
            <!-- Enter the password for database access -->
            <entry key="databasePassword" value="databasePassword"/>
    
            <!-- You have to provide the MySQL, MS SQL Server, Sybase or DB2 JDBC driver respectively if you selected
                 corresponding DBMS type. For license reasons MySQL, Sybase and MS SQL Server JDBC Drivers are
                 not provided. Alternatively you can use the mariadb JDBC Driver for MySQL and
                 the jTDS JDBC Driver for MS SQL Server and Sybase which is provided. -->
    
            <!-- You can choose between 'yes' or 'no' for using the jTDS JDBC Driver
                 This entry affects only MS SQL Server or Sybase -->
            <entry key="connectorJTDS" value="yes"/>
    
            <!-- You can choose between 'yes' or 'no' for using the mariadb JDBC Driver
                 This entry affects only MySQL -->
            <entry key="connectorMaria" value="yes"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="jdbc">
        <userInput>
            <!-- JobScheduler Database Configuration -->
    
            <!-- Configuration for JDBC Driver
                 This entry is only necessary if you selected a DBMS type such as MySQL,
                 MS SQL Server, Sybase ot DB2 in the previous <userInput> element. -->
    
            <!-- You have to provide the MySQL, MS SQL Server, Sybase or DB2 JDBC driver respectively if you selected
                 corresponding DBMS type. For license reasons MySQL and MS SQL Server JDBC Drivers are
                 not provided. Specify the JDBC Driver source (e.g. mysql-connector-java-*.jar for MySQL,
                 sqljdbc.jar for MS SQL Server, jconn3.jar for Sybase). Alternatively you can use the mariadb
                 JDBC Driver for MySQL and the jTDS JDBC Driver for MS SQL Server and Sybase which is provided. -->
    
            <!-- Select the path to JDBC Driver -->
            <entry key="connector" value="/usr/share/java/mariadb-connector-java.jar"/>
    
            <!-- Only for DB2: Select the path to DB2 license file for JDBC Driver -->
            <entry key="connectorLicense" value=""/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="reportingDatabase">
        <userInput>
            <!-- Reporting Database Configuration
                 NOT SUPPORTED FOR SYBASE AND DB2 -->
    
            <!-- Set 'yes' if the JobScheduler and the Reporting database are the same.
                 If 'yes' then further Reporting database variables are ignored. -->
            <entry key="sameDbConnection" value="yes"/>
    
            <!-- Choose the database management system. Supported values are 'mysql' for MySQL,
                 'oracle' for Oracle, 'mssql' for MS SQL Server, 'pgsql' for PostgreSQL. -->
            <entry key="reporting.databaseDbms" value="mysql"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="reportingDbconnection">
        <userInput>
            <!-- Reporting Database Configuration
                 NOT SUPPORTED FOR SYBASE AND DB2 -->
    
            <!-- Enter the name or ip address of the database host -->
            <entry key="reporting.databaseHost" value="qnet-jobscheduler.c58hqrvwigfw.ap-northeast-1.rds.amazonaws.com"/>
    
            <!-- Enter the port number for the database instance. Default ports are for MySQL 3306,
                 Oracle 1521, MS SQL Server 1433, postgreSQL 5432. -->
            <entry key="reporting.databasePort" value="3306"/>
    
            <!-- Enter the schema -->
            <entry key="reporting.databaseSchema" value="jobscheduler_data"/>
    
            <!-- Enter the user name for database access -->
            <entry key="reporting.databaseUser" value="reporting.databaseUser"/>
    
            <!-- Enter the password for database access -->
            <entry key="reporting.databasePassword" value="reporting.databasePassword"/>
    
            <!-- You have to provide the MySQL or MS SQL Server JDBC driver respectively if you selected
                 corresponding DBMS type. For license reasons MySQL and MS SQL Server JDBC Drivers
                 are not provided. Alternatively you can use the mariadb JDBC Driver for MySQL and the
                 jTDS JDBC Driver for MS SQL Server which is provided. -->
    
            <!-- You can choose between 'yes' or 'no' for using the jTDS JDBC Driver
                 This entry affects only MS SQL Server -->
            <entry key="reporting.connectorJTDS" value="yes"/>
    
            <!-- You can choose between 'yes' or 'no' for using the mariadb JDBC Driver
                 This entry affects only MySQL -->
            <entry key="reporting.connectorMaria" value="yes"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="reportingJdbc">
        <userInput>
            <!-- Reporting Database Configuration
                 NOT SUPPORTED FOR SYBASE AND DB2 -->
    
            <!-- Configuration for JDBC Driver
                 This entry is only necessary if the package 'Database Support' is chosen and you
                 selected a DBMS type MySQL or MS SQL Server in the previous
                 <userInput> element. -->
    
            <!-- You have to provide the MySQL or MS SQL Server JDBC driver respectively if you selected
                 corresponding DBMS type. For license reasons MySQL and MS SQL Server JDBC Drivers are
                 not provided. Specify the JDBC Driver source (e.g. mysql-connector-java-*.jar for MySQL,
                 sqljdbc.jar for MS SQL Server). Alternatively you can use the mariadb JDBC Driver for
                 MySQL and the jTDS JDBC Driver for MS SQL Server which is provided. -->
    
            <!-- Select the path to JDBC Driver -->
            <entry key="reporting.connector" value="/usr/share/java/mariadb-connector-java.jar"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="end">
        <userInput/>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.InstallPanel id="install"/>
    <com.izforge.izpack.panels.ProcessPanel id="process"/>
    <com.izforge.izpack.panels.FinishPanel id="finish"/>
    </AutomatedInstallation>
    
    
  3. setup.sh を実行します

    ./setup.sh jobscheduler_install.xml
    

JOC インストール

  1. JobScheduler と同様に JOC をダウンロードします。

    スクリーンショット 2017-06-08 0.35.42.png

  2. joc_install.xml を修正します。

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!--
    XML configuration file for JOC
    
    If you call the installer with this XML file then
    you accept at the same time the terms of the
    licence agreement under GNU GPL 2.0 License
    (see http://www.gnu.org/licenses/gpl-2.0.html)
    -->
    <AutomatedInstallation langpack="eng">
    <com.izforge.izpack.panels.UserInputPanel id="home">
        <userInput/>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.HTMLLicencePanel id="gpl_licence"/>
    <com.izforge.izpack.panels.TargetPanel id="target">
    
        <!-- SELECT THE INSTALLATION PATH
             It must be absolute!
             For example:
             /opt/sos-berlin.com/joc on Linux
             C:Program Filessos-berlin.comjoc on Windows -->
        <installpath>/opt/sos-berlin.com/joc</installpath>
    
    </com.izforge.izpack.panels.TargetPanel>
    <com.izforge.izpack.panels.UserInputPanel id="jetty">
        <userInput>
    
            <!-- JOC requires a servlet container such as Jetty.
                 If a servlet container already installed then you can use it.
                 Otherwise a Jetty will be installed in addition if withJettyInstall=yes.
                 You need root permissions to install JOC with Jetty. -->
            <entry key="withJettyInstall" value="yes"/>
            <entry key="jettyPort" value="4446"/>
            <!-- Only necessary for Windows -->
            <entry key="jettyStopPort" value="40446"/>
            <!-- Only necessary for Unix (root permissions required) -->
            <entry key="withJocInstallAsDaemon" value="yes"/>
            <!-- Path to Jetty base directory
                 For example:
                 /homer/[user]/sos-berlin.com/joc on Linux
                 C:ProgramDatasos-berlin.comjoc on Windows -->
            <entry key="jettyBaseDir" value="/home/joc/jetty"/>
    
            <!-- Java options for Jetty. -->
            <!-- Initial memory pool (-Xms) in MB -->
            <entry key="jettyOptionXms" value="128"/>
            <!-- Maximum memory pool (-Xmx) in MB -->
            <entry key="jettyOptionXmx" value="512"/>
            <!-- Thread stack size (-Xss) in KB -->
            <entry key="jettyOptionXss" value="4000"/>
            <!-- Further Java options -->
            <entry key="jettyOptions" value=""/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="reportingDatabase">
        <userInput>
            <!-- Reporting Database Configuration -->
    
            <!-- Choose the database management system. Supported values are 'mysql' for MySQL,
                 'oracle' for Oracle, 'mssql' for MS SQL Server, 'pgsql' for PostgreSQL. -->
            <entry key="reporting.databaseDbms" value="mysql"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="reportingDbconnection">
        <userInput>
            <!-- Reporting Database Configuration -->
    
            <!-- Enter the name or ip address of the database host -->
            <entry key="reporting.databaseHost" value="qnet-jobscheduler.c58hqrvwigfw.ap-northeast-1.rds.amazonaws.com"/>
    
            <!-- Enter the port number for the database instance. Default ports are for MySQL 3306,
                 Oracle 1521, MS SQL Server 1433, postgreSQL 5432. -->
            <entry key="reporting.databasePort" value="3306"/>
    
            <!-- Enter the schema -->
            <entry key="reporting.databaseSchema" value="jobscheduler_data"/>
    
            <!-- Enter the user name for database access -->
            <entry key="reporting.databaseUser" value="qnet_admin"/>
    
            <!-- Enter the password for database access -->
            <entry key="reporting.databasePassword" value="7sYne7aFEsFSK7xh"/>
    
            <!-- You have to provide the MySQL or MS SQL Server JDBC driver respectively if you selected
                 corresponding DBMS type. For license reasons MySQL and MS SQL Server JDBC Drivers are
                 not provided. Alternatively you can use the mariadb JDBC Driver for MySQL and
                 the jTDS JDBC Driver for MS SQL Server which is provided. -->
    
            <!-- You can choose between 'yes' or 'no' for using the jTDS JDBC Driver
                 This entry affects only MS SQL Server -->
            <entry key="reporting.connectorJTDS" value="yes"/>
    
            <!-- You can choose between 'yes' or 'no' for using the mariadb JDBC Driver
                 This entry affects only MySQL -->
            <entry key="reporting.connectorMaria" value="yes"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="reportingJdbc">
        <userInput>
            <!-- Reporting Database Configuration -->
    
            <!-- Configuration for JDBC Driver
                 This entry is only necessary if you selected a DBMS type such as MySQL and
                 MS SQL Server in the previous <userInput> element. -->
    
            <!-- You have to provide the MySQL or MS SQL Server JDBC driver respectively if you selected
                 corresponding DBMS type. For license reasons MySQL and MS SQL Server JDBC Drivers are
                 not provided. Specify the JDBC Driver source (e.g. mysql-connector-java-*.jar for MySQL,
                 sqljdbc.jar for MS SQL Server). Alternatively you can use the mariadb
                 JDBC Driver for MySQL and the jTDS JDBC Driver for MS SQL Server which is provided. -->
    
            <!-- Select the path to JDBC Driver -->
            <entry key="reporting.connector" value="/usr/share/java/mariadb-connector-java.jar"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="database">
        <userInput>
            <!-- JobScheduler Database Configuration -->
    
            <!-- Set 'yes' if the Reporting and the JobScheduler database are the same.
                 If 'yes' then further JobScheduler database variables are ignored. -->
            <entry key="sameDbConnection" value="yes"/>
    
            <!-- Choose the database management system. Supported values are 'mysql' for MySQL,
                 'oracle' for Oracle, 'mssql' for MS SQL Server, 'pgsql' for PostgreSQL,
                 'db2' for DB2 and 'sybase' for Sybase. -->
            <entry key="databaseDbms" value=""/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="dbconnection">
        <userInput>
            <!-- JobScheduler Database Configuration -->
    
            <!-- Enter the name or ip address of the database host -->
            <entry key="databaseHost" value="qnet-jobscheduler.c58hqrvwigfw.ap-northeast-1.rds.amazonaws.com"/>
    
            <!-- Enter the port number for the database instance. Default ports are for MySQL 3306,
                 Oracle 1521, MS SQL Server 1433, postgreSQL 5432, DB2 50000, Sybase 5000. -->
            <entry key="databasePort" value="3306"/>
    
            <!-- Enter the schema -->
            <entry key="databaseSchema" value="jobscheduler_data"/>
    
            <!-- Enter the user name for database access -->
            <entry key="databaseUser" value="qnet_admin"/>
    
            <!-- Enter the password for database access -->
            <entry key="databasePassword" value="7sYne7aFEsFSK7xh"/>
    
            <!-- You have to provide the MySQL, MS SQL Server, Sybase or DB2 JDBC driver respectively if you selected
                 corresponding DBMS type. For license reasons MySQL, Sybase and MS SQL Server JDBC Drivers are
                 not provided. Alternatively you can use the mariadb JDBC Driver for MySQL and
                 the jTDS JDBC Driver for MS SQL Server and Sybase which is provided. -->
    
            <!-- You can choose between 'yes' or 'no' for using the jTDS JDBC Driver
                 This entry affects only MS SQL Server or Sybase -->
            <entry key="connectorJTDS" value="yes"/>
    
            <!-- You can choose between 'yes' or 'no' for using the mariadb JDBC Driver
                 This entry affects only MySQL -->
            <entry key="connectorMaria" value="yes"/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="jdbc">
        <userInput>
            <!-- JobScheduler Database Configuration -->
    
            <!-- Configuration for JDBC Driver
                 This entry is only necessary if you selected a DBMS type such as MySQL,
                 MS SQL Server, Sybase ot DB2 in the previous <userInput> element. -->
    
            <!-- You have to provide the MySQL, MS SQL Server, Sybase or DB2 JDBC driver respectively if you selected
                 corresponding DBMS type. For license reasons MySQL and MS SQL Server JDBC Drivers are
                 not provided. Specify the JDBC Driver source (e.g. mysql-connector-java-*.jar for MySQL,
                 sqljdbc.jar for MS SQL Server, jconn3.jar for Sybase). Alternatively you can use the mariadb
                 JDBC Driver for MySQL and the jTDS JDBC Driver for MS SQL Server and Sybase which is provided. -->
    
            <!-- Select the path to JDBC Driver -->
            <entry key="connector" value="/usr/share/java/mariadb-connector-java.jar"/>
    
            <!-- Only for DB2: Select the path to DB2 license file for JDBC Driver -->
            <entry key="connectorLicense" value=""/>
    
        </userInput>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.UserInputPanel id="end">
        <userInput/>
    </com.izforge.izpack.panels.UserInputPanel>
    <com.izforge.izpack.panels.InstallPanel id="install"/>
    <com.izforge.izpack.panels.ProcessPanel id="process"/>
    <com.izforge.izpack.panels.FinishPanel id="finish"/>
    </AutomatedInstallation>
    
  3. setup.sh を実行します

    ./setup.sh joc_install.xml
    

続きを読む

AWS(EC2) で Python3 + Flask を Apache で動かす(mod_wsgi?)

はじめに

簡単なWebサービス新しく立ち上げるにあたり、勉強を兼ねて Python を使うことにしたので、その環境構築用のメモとして残す。ほとんど、他の記事を寄せ集めただけなのだけど、mod_wsgiのインストールで少し手間取ったので念のため。

参考:PythonのWebフレームワーク4種比較
https://qiita.com/Morio/items/8db3c9b3ba7f2c71ef27

AWS (EC2) + Apache2

下記のサイトを参考にしてテスト用のEC2サーバのインスタンスを作成後、Apache2 をインストール・起動します。なお、現時点(2017/12/06)でのバージョンは、2.2.34だった。

参考:AWS EC2でWebサーバーを構築してみる
https://qiita.com/Arashi/items/629aaed33401b8f2265c

Python3 + pip3

下記のサイトを参考にして Python3 をインストールし、pip を設定。現時点(2017/12/06)では、python3.6がリリースされているので、3.5ではなく3.6をインストール。

参考:Amazon Linux (EC2)上でPython3と…
https://qiita.com/KeijiYONEDA/items/f9cf37cfc359aa893797
※「3.5」は「3.6」に、「35」は「36」に読み替え

実際に実行したコマンド.
sudo yum install python36-devel python36-libs python36-setuptools
sudo /usr/bin/easy_install-3.6 pip

Flask + mod-wsgi

Flask のインストールは簡単。下記のサイトの最初の5分だけを実行すれば、Hello Flask! を表示するWebサイトは完成!

参考:PythonのFlaskを初めて触ってから30分で…
https://qiita.com/yoshizaki_kkgk/items/3cd785b4a670deec0685
※ 環境構築からHello Flask!まで(5分)だけを実行

・・・でも今回は Apacheサーバを利用するので、mod_swgi のインストールが必要となる。だけど、上記サイトの次の10分以降に書いてあるようにソースから configure・make なんて 前時代的 というか 面倒くさい というか、難しいことは避けたいので、pipに頼ることにする

pip3 install mod-wsgi

このコマンドでインストールできそうなのだけど、下記のエラーが出て前に進まなくなった。apxs って何さ?

Collecting mod_wsgi
  Using cached mod_wsgi-4.5.22.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-4lb9c3ll/mod-wsgi/setup.py", line 301, in <module>
        INCLUDEDIR = get_apxs_config('INCLUDEDIR')
      File "/tmp/pip-build-4lb9c3ll/mod-wsgi/setup.py", line 273, in get_apxs_config
        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
      File "/usr/lib64/python3.6/subprocess.py", line 707, in __init__
        restore_signals, start_new_session)
      File "/usr/lib64/python3.6/subprocess.py", line 1333, in _execute_child
        raise child_exception_type(errno_num, err_msg)
    FileNotFoundError: [Errno 2] No such file or directory: 'apxs'

apxs = Apache extension tool

ググってみたら、apxs は「APache eXtenSion tool」のことだそうで、apache-dev に含まれるそうな。早速、以下のコマンドで apache-dev をインストール。しかし、省略難しすぎ。

sudo yum install httpd-devel

これで大丈夫!と思ったけど、次は、gcc が入っていなくて怒られました。仕方ないので、gcc を yum 経由でインストール。

sudo yum install gcc
sudo /usr/local/bin/pip3 install mod_wsgi

デプロイ

必要なものは揃った!ってことで、Apache の設定とサンプル用のサイトを作ってみる。

サイトの作成

Hello Flask!をそのまま流用させていただく。

/var/www/flask/app.py
# coding: utf-8
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, Flask!"

if __name__ == "__main__":
    app.run()

swgi ファイル

Apache というか、mod_swgiに読み込ませるファイルを作成

/var/www/flask/adapter.wsgi
# coding: utf-8
import sys
sys.path.insert(0, '/var/www/flask')

from app import app as application

Apacheの設定

Apache の conf.d に設定ファイルを追加し、app.py と adapter.wsgi が有効になるように設定。

/etc/httpd/conf.d/flask.conf
# /etc/httpd/modules には配置されないので指定が必要
LoadModule wsgi_module /usr/local/lib64/python3.6/site-packages/mod_wsgi/server/mod_wsgi-py36.cpython-36m-x86_64-linux-gnu.so

<VirtualHost *:80>
  ServerName ec2-xxxxxxx.amazonaws.com:80
  DocumentRoot /var/www/flask
  WSGIScriptAlias / /var/www/flask/adapter.wsgi
  <Directory "/var/www/flask/">
    options Indexes FollowSymLinks +ExecCGI
  </Directory>
</VirtualHost>

まとめ

python3、pip 辺りは他にも利用するだろうから、すでにインストールされている場合が多そう。なので、実質面倒なのは Apache(mod_wsgi)の設定周りかな。パッケージ化が進んでいると思っていたし、yum / pip といった便利なものがあるにも関わらず、gccが必要になるとは思わなかった。

次は、flask + DataBase + Template Engine ってとこかな。

続きを読む

Chaliceを使ってみた

AWS+Lambda+Pythonでサーバレス開発をするためのAWS謹製フレームワーク

インストール

pip install chalice
chalice new-project helloworld
cd helloworld

ローカルで起動

chalie local

リロードさせたい

npm install -g nodemon
nodemon --exec "chalice local" --watch *.py

sqlalchemyでMySQLにつなぐ

pip install SQLAlchemy
pip install PyMySQL
app.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import os

app = Chalice(app_name='helloworld')
app.debug = True
Base = declarative_base()

#Userモデル
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(40))

#session取得
def getSession():
    #環境変数から接続文字列を取得
    engine = create_engine(os.environ["CONNECTION_STRING"], echo=True)
    session = sessionmaker(bind=engine)()
    return session

@app.route('/')
def index():
    session = getSession()
    user = session.query(User).one()
    return {'hello': user.name}

.chalice/config.json へ環境変数を定義
http://chalice.readthedocs.io/en/latest/topics/configfile.html

chalice/config.json

  "stages": {
    "dev": {
      "api_gateway_stage": "api",
      "environment_variables": {
        "CONNECTION_STRING": "mysql+pymysql://userid:passowrd@host/database?charset=utf8",
        "OTHER_CONFIG": "dev-value"
      }
    }

#デプロイしてみる
pip freeze > requirements.txt
chalice deploy

#依存関係をvendorに入れとけって怒られる
Could not install dependencies:
itsdangerous==0.24
typing==3.5.3.0
MarkupSafe==1.0
SQLAlchemy==1.1.15
You will have to build these yourself and vendor them in
the chalice vendor folder.

#入れる
pip install -U itsdangerous==0.24 -t ./vendor/
pip install -U SQLAlchemy==1.1.15 -t ./vendor/
pip install -U MarkupSafe==1.0 -t ./vendor/
pip install -U typing==3.5.3.0 -t ./vendor/

chalice deploy

#接続を試す・・・成功!
curl https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/
{"hello": "hirao"}

続きを読む