前回は入り口の準備をしたので、楽しい中身の話をしていくのですが、実はそんなに大した話ではありません。普通にWEBサイトを作るだけです。
API Gateway を使って軽量なWEBサイトを作れたら、半分はこんな良い感じのことがあるかもよってのと、半分は暇を持て余したエンジニアの娯楽じゃねーかくらいに見てもらえれば幸いでござんす。
目的
軽量情報の取り扱い
わりと長く、INFOレベルの情報の扱いに悩みがありました。例えばすぐ見る必要はないけど、たまに見るために残したいレベルのアラート。毎日収集・集計してるパフォーマンス関連の記録だけど、見るのは月イチくらい。そういう、残さないわけにはいかないけど、残す場所に困る情報をどうするか。
そういう情報の例を上げてみます。
SNS → Email のMLに送る
各ユーザーに無駄に蓄積されるし、実際必要なのはほぼ最新数件のみ。アドレスと受信者リストの管理も面倒だし、ていうかEメールとかそろそろ排除したくねっていう。手動でダウンロードして閲覧
とりあえずS3とかに蓄積だけしておいて、必要なときに手元にダウンロードして、テキストエディタとか表計算ソフトで表示する。マジださくねって。WEB用フレームワークとインスタンスで管理画面
アプリケーションをサクッと作るのはいいけど、インスタンスが無駄。T系のEC2すら起動したくないレベルにCPUリソースが無駄無駄無駄。などなど。必要とする頻度が低いので、グループチャットに送るとかも、SRE的観点では日々のノイズとなって良くない。こいつらを、なんとかしてぇなぁと思いつつ、そっとしておくよくあるヤツでした。
ピコーン(AA略
年末年始に期限が切れそうなリフレッシュ休暇をくっつけて長期休暇してたら、開幕やる気がでなくてPythonで気分転換でもすっかなーって考えてたら、ふと電球が光って API Gateway + Lambda Python でWEBサイト作ったら楽しくね?ってトイレで思いついて、ついでに役に立って負債にならないよーにできそう!ってことで手掛けた次第。つまり俺のヤル気の礎でもある。
流れだけみたら完全に手段先行型のクソエンジニア:-)
WEBサイトの仕組み
API GatewayでWEBサイト!?
AWS API Gateway の『API』とは──昨今だと、システムとシステムが情報をやり取りしたり、特定の処理を命令するような、プログラムの一部であるイメージが強いと思います。実際私も、API Gatewayを最初に使った時は、JSONでリストを返すようなものを作りました。
名前に『API』がついてるからHTMLを返すと違和感があるかもだけど、中身はなんでもやりたい放題のただのPythonだし、むしろAPI GatewayがHTTP対応してきた、その真の意味を俺が与えてやる!くらいの勢いでやったるでごわす。
自分、クラウドでお茶目なことすることに定評ありやす😎
サーバーレスWEBサイト
公式だとこの辺。一般の人でも、似たようなことやってそうなの見ましたが、それらは参考にしません。なぜなら、俺がやりたいだけだから。
クライアントがブラウザで、サーバーがAPI Gateway HTTP版。
リクエストがHTTP(S)で、レスポンスがHTML + Javascript。
Content-Type は text/html が基本。必要があれば、text/plain や application/json も返せるようにしとけばよろし。
使用するもの
構築に使うものは以下の感じ。意地でも常駐課金はしないスタイル。S3 + Lifecycle | JSONデータ置き場。定期削除で整理・節約 |
ACM | API GatewayのSSL/TLS |
API Gateway v2 HTTP + Authorizer | 認証付きのHTTPS入り口 |
Lambda Python3 | 認証・フレームワーク・データ処理など |
jinja2 | Viewのテンプレート |
Tabulator | JSONを表で表示 |
これだけで何したいか大体わかるでしょう。PHPやRubyでViewをイジったことはあるけど、Pythonではなかったから、単にやってみたかったという。ある意味、正しいエンジニアリング仕草である(キリッ
S3にデータ保管
今回は、RDSのスロークエリを管理画面で簡単に見れるようにするとしましょう。元々、1日に1回、1日分のスロークエリログを取得・集計したデータを作成していました。それを、適当に成形して SNS -> Email に送っていたのを、JSONにしてS3に保存するように変更しました。
- s3://$BUCKET/slowquerylog/$SERVICE/YYYYMMDD_HHII/slowquerylog.json
これを取得して、見やすくしてあげるだけです。
(スロークエリの集計については、別途記事にするかもしないかも)
ルーティング
ほぼ、前記事と似た内容なので、端折りますがlambda_handler をこんな風にして、リクエストパスと関数名を紐付けて実行したり、適切なレスポンス形式で返したりします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
TEMPLATE_ROUTE = "templates/example-console" DEFAULT_CONTENT_TYPE = "text/html; charset=UTF-8" def lambda_handler(event, context): api = ApiGatewayV2Manager(event) auth = api.authorizer() if auth: return auth path = api.getPath() if not path: print("Bad Request, not found path.") return api.returnBadRequest() if path not in globals(): print("Forbidden, not found %s." % path) return api.returnForbidden() res = eval(path)(api) print("OK, %s" % path) content_type = api.getContentType() if api.getContentType() else DEFAULT_CONTENT_TYPE return api.returnOK(res, content_type) |
今回のは小規模なので、同ファイルに def を追記しましたが、もう少し規模が大きい場合は、ファイルを分けてクラスを読み込む形にするなど、お好みで改良していくことになるでしょう。
もしくは、既存の軽量なフレームワークをうまいこと組み込むとか、そういうチャレンジも楽しいかもしれません。
トップページ
path = / の場合は、default としているので、こんな感じ。boto3でデータを読み込んで、Viewに必要な変数を作成し、HTMLを返します。(細かい処理部は独自のクラス分けしてるので割愛)
1 2 3 4 5 6 7 8 9 10 11 12 |
def default(api): ~snip~ # S3からサービスリストやファイルパスのリストを生成 title = "サービス一覧" template_file = "default.tpl" values = { "title" : title, "results" : results, "slowquerylogs": slowquerylogs, } return _template(TEMPLATE_ROUTE, template_file, values) |
View生成
Viewはここでまとめていて、
1 2 3 4 5 6 |
from modules.TextTemplate import TextTemplate def _template(route, template_file, values): template = TextTemplate() body = template.render(route, template_file, values) return body |
その先の module はこんな感じ。テンプレートファイルを読み込んで、値を渡してHTMLを生成します。
1 2 3 4 5 6 7 8 9 10 11 12 |
from jinja2 import Template, Environment, FileSystemLoader class TextTemplate(): def __init__(self): return def render(self, route, template_file, values): env = Environment(loader=FileSystemLoader(route), trim_blocks=True) template = env.get_template(template_file) body = template.render(values) return body |
なので、Lambda で使うライブラリとして、./python/ 配下に jinja2 をインストールしておきます。(私の場合はレイヤーとしてアップロードしています)
1 |
pip3 install boto3 pytz jinja2 -t python/ |
テンプレート作成
メインのテンプレートでは、共通テンプレートを読み込みつつ、メインコンテンツを生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{% extends "base.tpl" %} {% block title %}{{title}}{% endblock %} {% block body %} <ul class="list-first"> {% for service,result in results.items(): %} <li>{{service}}</li> <ul class="list-second"> {% for datetime,hosts in result.items(): %} {% if slowquerylogs.get(service): %} <li><a href="slowquerylog?service={{service}}&datetime={{slowquerylogs.get(service)}}">SlowQueryLog</a></li> {% endif %} ~snip~ {% endfor %} </ul> {% endfor %} </ul> {% endblock %} |
共通となるテンプレートで上下を記述。tabulator のCSSだけ、面倒になってここに記述。(必要なときにBODYで読む技術がワイになかった)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!DOCTYPE html> <html> <head> <title>{% block title %}{% endblock %} | ExampleConsole</title> <link rel="icon" href="data:,"> <link href="https://unpkg.com/tabulator-tables@4.1.4/dist/css/tabulator.min.css" rel="stylesheet"> <style> ~snip~ </style> </head> <body> <div><h3><a href="/">ExampleConsole</a> > {{ self.title() }}</h3></div> <div> {% block body %}{% endblock %} </div> </body> </html> |
jinja2 のドキュメントをみたりググりながらやれば、記法はとてもシンプルですぐ慣れます。Pythonのようで微妙にPython3じゃない感じがあるけど、まぁほぼPythonなので。
仕上がりView
シンプルなリストの出来上がり。このくらいが最初の練習としてはほどよい成功体験。
スロークエリログ
トップページからSlowQueryLogに遷移したら、そのサービスの最新データを表示しつつ、過去データも選択できるくらいにしときます。いちいち表示用の表をデザインするとか時間の無駄なので、Tabulator を使ってJSONを記述してカスタマイズするだけでいいようにします。
JSONデータ作成
S3からデータを持ってきて、json.loads してゴニョゴニョ成形とかしてから、json.dumps で一行の文字列にしてViewに渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
def slowquerylog(api): s3 = S3Manager() params = api.getQueryParameters() service = params.get("service") datetime_value = params.get("datetime") key = "%s%s/%s/slowquerylog.json" % (SLOWQUERY_LOG_KEY, service, datetime_value) json_data = s3.getObject(BUCKET, key) json_load = json.loads(json_data) ~snip~ title = "スロークエリログ" template_file = "slowquerylog.tpl" values = { "title" : title, "json" : json.dumps(results), "graph_max": graph_max, "bucket" : BUCKET, "log_key" : SLOWQUERY_LOG_KEY, "service" : service, "datetime" : datetime_value, "datetimes": datetimes, } return _template(TEMPLATE_ROUTE, template_file, values) |
Viewコンテンツ
ガツンとJSONを記述しつつ、表のカスタマイズを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
{% extends "base.tpl" %} {% block title %}{{title}}{% endblock %} {% block body %} {% include "tabulator.tpl" %} <div id="breadcrumbs" style="margin-bottom: 4px;"> s3://{{bucket}}/{{log_key}}<span style="color:red"">{{service}}</span>/ <select name="datetime" onChange="location.href=value;"> {% for d in datetimes: %} <option value="slowquerylog?service={{service}}&datetime={{d}}"{% if d == datetime: %} selected{% endif %}>{{d}}</option> {% endfor %} </select> /slowquerylog.json </div> <div id="slowquerylog"></div> <script type="text/javascript"> var slowquerylog_json = {{json}} var table = new Tabulator("#slowquerylog", { data:slowquerylog_json, //load row data from array layout:"fitDataFill", //fit columns to width of table responsiveLayout:"collapse", //hide columns that dont fit on the table addRowPos:"top", //when adding a new row, add it to the top of the table history:true, //allow undo and redo actions on the table pagination:"local", //paginate the data paginationSize:100, //allow 7 rows per page of data placeholder:"No Data Available", initialSort:[ //set the initial sort order of the data {column:"total_query_time", dir:"desc"}, ], headerSort:false, headerSortTristate:false, columns:[ //define the table columns {title:"Rank", formatter:"rownum", align:"center", headerSort:false}, {title:"TotalTime", field:"total_query_time", formatter:"money", formatterParams:{precision:1}}, {title:"RankLevel", field:"total_query_time", hozAlign:"left", formatter:"progress", formatterParams:{ min:0, max:{{graph_max}}, color:["#0aea28", "#8ef41c", "#edf411", "#f4af21", "#f45e1c"], } }, {title:"QueryCount", field:"count", align:"right"}, {title:"QueryTime", field:"query_time_range", align:"center", headerSort:false}, {title:"LockTime", field:"lock_time_range", align:"center", headerSort:false}, {title:"TargetRows", field:"rows_range", align:"center", headerSort:false}, {title:"Host", field:"host", headerSort:false}, {title:"Database", field:"database", headerSort:false}, {title:"Found Time", field:"found_datetime_range", headerSort:false}, {title:"Query", field:"unique_query"}, ], }); </script> {% endblock %} |
include “tabulator.tpl” には、Javascriptを外部から読み込まさせてもらっています。
1 2 3 |
<script type="text/javascript" src="https://unpkg.com/tabulator-tables@4.1.4/dist/js/tabulator.min.js"></script> <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script> |
JSONの中身
Tabulator に読み込ませたJSONは一行ですが、jq で表示するとこんな感じ。各サービスで集計した基礎データに、日時文字列など Tabulator で成形できないものはPythonで作って追加しました。Tabulator でも前後の行のデータと照らし合わせて if とか色々使えるので、見た目を整えるためにどっちでやるか、を考えるのが楽しいところ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
[ { "hash": "57a13391", "count": 15372, "host": "example-db-03-01", "src_ip": "10.100.x.x", "database": "example-heavy-01", "unique_query": "SELECT `heavy`.* FROM `heavy` WHERE `heavy`.`user_id` = ? AND `heavy`.`slow` = ?", "oldest_unix_time": 1611540002, "latest_unix_time": 1611627927, "min_query_time": 0.500048, "max_query_time": 0.961357, "min_lock_time": 2e-05, "max_lock_time": 2e-05, "min_rows": 1842, "max_rows": 5587, "total_query_time": 8558.328979999991, "found_datetime_range": "2021-01-25T11:00:02+09:00 - 2021-01-26T11:25:27+09:00", "query_time_range": "0.5 - 1.0", "lock_time_range": "0.0 - 0.0", "rows_range": "1842 - 5587" }, ...snip... ] |
仕上がりView
ブログ用にデータと見た目は適当に編集しましたが、ゲージで色表示したり、項目の並び替えができたり、ページングがついたり、いろんな機能がついています。表のテンプレート選択を筆頭に、行単位・セル単位、プログラムちっくなカスタマイズなど、本当に多機能ですが、ドキュメントを眺めて遊んでいれば、すぐ慣れてたいていのことはできるようになります。
自分でHTMLやCSSを書く必要がほぼない、というだけで、優先順位の高くないデータをさらりと見れるようにしたりする気力が湧きやすくなると思いませんか。「あぁ、JSONでS3に置いてくれたらいいよ」みたいな軽いフットワーク。
RDS管理画面にスロークエリログを取りにいくのってかなりダルい部類なのに中身もアレなわけで。それを全サービス分、いつでも好きなときに、集計結果を極小コストで見れるように!なんということでしょう~♪
今回の仕上げたモノによって、自分が満足するという1番の目的と、軽量データの処遇というお悩み解決は果たしたわけですが── これを軸に改良していくことで、EC2インスタンスを排除できる可能性を感じるわけです。完全に後付けですけど。
たとえT系インスタンスでも、EC2があるだけで起動コストと管理コストが大なり小なり発生するわけで、それをこういった少しの工夫で無くせるかもしれない、というのは意外に大きい気がしています。
開発環境やフレームワーク的には、規模に合わせて、より整備する必要はあるので、インフラエンジニアに数少ないプログラムの楽しみの1つとして温めていきたいと思います:-)