IntelliJ IDEA プロジェクト内移動を使いこなす
案外ちゃんと使いこなせていないソースの行き来機能
IntelliJ IDEAと言いつつ、スクリーンショットはGolandでやっています。
わりと規模大きめの開発が多くなってきて、様々なファイルを行き来することが増えてきて、結構ファイルを探すという無駄な時間が発生していることに気づいたので、今回は費用対効果高そうだったので、プロジェクト内移動のNavigation機能についてまとめたいと思う。
この記事に書いていることは以下の本に大体書いています。
ショートカットも基本的にわかる範囲で紹介します。(ideaVimを普段使っているのでわかる範囲で書く) また、私は相変わらずショートカットの記号が覚えられないので、ここではAltやCtrlと書きます。
コードジャンプ系
シンボル定義箇所へジャンプ
定義されている変数や関数の定義元にジャンプする。
- ideaVim
Ctrl
+]
- Mac
Command
+B
- Win
Ctrl
+B
一箇所しかない場合は、一発でジャンプしますが、複数定義箇所がある場合は、ポップアップが出てきます。
シンボルの利用箇所の一覧
- Mac
Option
+F7
- Win
Alt
+F7
何かのシンボルにカーソルを合わせて、使うと、その定義箇所から利用箇所が一覧で見れる。
ジャンプを戻る
ジャンプ先から戻る
- ideaVim
Ctrl
+t
- Mac
Command
+[
- Win
Ctrl
+Alt
←
ジャンプ先に戻る
- Mac
Command
+]
- Win
Ctrl
+Alt
+→
クラス間の移動
子クラスから親クラスへ移動。(Javaとかの継承)
- ideaVim クラス名に合わせて、シンボルへのジャンプで同じことができる
- Mac
Command
+U
- Win
Ctrl
+U
親クラスから、子クラスや実装クラスを見ることもできる
- ideaVim クラス名に合わせて、シンボルへのジャンプで同じことができる
- Mac
Option
+Command
+B
- Win
Ctrl
+Alt
+B
ファイル系
直近のファイルを開く
Ctrl
を押したまま、Tab
を押した回数だけ前のファイルに戻れます。
- Mac,Win
Ctrl
+Tab
直近のファイル一覧
上のTab
を何度も押す形式は正直だるいので、開きっぱなしにできるこっちのがおすすめ。
- Win
Command
+E
- Mac
Ctrl
+E
ディレクトリ系
ナビゲーションバーを使って移動
ナビゲーションバーを開いて、矢印キー
やEnter
とかをつかうと、ソースを開く事もできます。
- Mac
Command
+1
- Win
Alt
+1
ナビゲーションバーの操作型
新規ファイル作成
- Mac
Command
+N
- Win
Alt
+Insert
ファイル名変更
- Mac,Win
Shift
+F6
現在開いているファイルにフォーカスを当てる。
- Mac
Command
+↑
- Win
Alt
+Home
現在開いているファイルをナビゲーションバーと同期させる
いま開いているファイルってソースどこ?っていうのが案外わかりにくかったりするので、自分は同期させたりしています。
※上に出てる階層表示でわかるっちゃわかりますが
検索
Search Everywhere
色んな検索をサクッとできる。
- Mac,Win
Shift
2回押す
種別付き検索
ファイル名検索
- Mac
Command
+Shift
+O
- Win
Ctrl
+Shift
+N
クラス名検索
- Mac
Command
+O
- Win
Ctrl
+N
シンボル名検索
- Mac
Option
+Command
+O
- Win
Ctrl
+Shift
+Alt
+N
シンボル・検索除外
Excludedに指定すると検索対象として出なくなる。
よくあるケースとしては、jsのdistフォルダ出てくるなあああああ!とか。
TODO一覧
FIXME
TODO
をコード上で書くと、一覧して見ることが出来る。
- Mac
Command
+6
- Win わからなかった
チャットと対話の使い分け
コミュニケーションは難しい
社会人になって、早いもので数ヶ月が経ちました。
いま、自分は広告配信システムの開発(株式会社Zucks)をしているのですが、学生から社会人になってから、最も難しいと感じ、日々課題を感じているのがコミュニケーションです。苦労しながらも、最近わかってきたことの一つであるチャットと対話の使い分けどうすれば良いんだっけってところをまとめてみる。
※弊社では営業も開発も、Slackをチャットツールとして使っています
チャットと対話の質的な違い
チャット
- 証拠能力が高い
- 時間のある時に返せる
- 他の人が見える
- 書いたこと以上のことは伝わらない(前後関係とかで分かることもある)
- ひとまず雑に投げれる
対話
- 温度感が伝わる
- 深い議論が出来る
- 時間が取られる
- 体力使う
上に書いたこと以外にも色々あるとは思いますが、雑に思いついたものを書きました。
それぞれの立ち位置
どちらもした方が良いことには変わりはないのですが、あえて言うなら、チャットは マスト
対話はベター
という感じで、この差を生んでいるのは、証拠能力
です。
対話は話をしている人以外には議論が見えません。じゃあ、対話は駄目なのか!?っていう話ではなく、対話をしたことをざっくりで良いのでチャットとかに残す。または結構な分量になるなら、Googleドキュメントとかに議事録残す。
「という話をした」はチャットで共有する
弊グループ全体がどうかは知りませんが、少なくともZucksでは、「という話をした」というワードがSlackにかなり出てくる。これは、誰とどういう話をしたとか、対話で決まったこととかをSlackに流す習慣がある。これはかなり良いことで、議論に参加していない人でも、ひとまずチームがどういう動きをしているかを知ることが出来る。知りたい情報なら、チャットとかで聞けば良いということが出来る。これがどこにも残っていないと、それすら出来ないので、この習慣は非常にありがたい。(これは別に誰にも指示されていない)
slack上で「という話をしたで2000件も出てきた」
中にはこんな雑な「という話をした」
チャットを投げる時はどういう時
自分の中で、2つの条件が決まっていて、以下の2つです。
- 何を質問したら良いか分からない
- 質問を完全に言語化出来る自信がある
何を質問したら良いか分からないのにチャット!???
チャットなのに、そんな雑で良いのか??!!!って感じかもしれませんが、まず前提として、このフェーズの時は、誰かにメンションをつけて投げるのではなく、雑にチャットに書く。作業をしている中でのモヤモヤとかはissue経由で全部チャットに流れるので、GitHub経由で流すこともかなり多い。
これは、いまここらへんで悩んでいるけど、誰に質問したらいいかも分からないし、質問するべき項目も分からない。そんな時に、チーム内の人に相談をしても、時間ばっかり食ってしまうので、こういうフェーズの時はチャットに書くようにしている。分かる人がいたら、返してくれることもあれば、自己解決することもよくある。
訳が分かっていないことしか分かっていないことをボヤく
強いおじさんが助けてくれることがよくある
質問を完全に言語化出来る自信がある
さっきとは違って、完全に理解しているフェーズの時は、チャットで投げることが多い。というのも、言語化が出来ているレベルになっている時は、ばちっと!っとチャットで投げる。このフェーズに入っている時は、短いチャットのやり取りで終わる。しかし、思ったよりちゃんと話した方がいいかも?ってなった時は、対話をして解決するということもある。そして、話が終わったら、「という話をした」で質問内容の答えをチャットで共有する。
チャットで無理な時は丸テーブルで話そう「マルテ」が発動する
対話をする時
対話をする時もチャットと同様で条件が2つあって
チャットだけだと長くなりそう
これは、質問を完全に言語化出来る自信がある
に書いたように、話した方が良さそうだ!って思った時に対話する。
ペアプロ、ペア作業、ペアhoge
ペアで一緒に進めた方が早そう。ということもあって、弊社では結構ペアプロをすることがあります。その理由としては、プロダクトコードの構成とかを理解するのに時間がかかりそうとか、言語ごとにあるパラダイムとかをサクッと理解しようみたいな時、あとは削除作業のような怖い作業はペアで確認しながらやったり、あとは何で落ちているか本当に分からないコードとかを一緒に読むこともあったりします。
etc...
- issueに自分の意思決定は全て書く
- GUI操作はスクリーンショットを貼る
- 質問は短くわかりやすくすることを頑張る
- 事実と事実からの推測かを明確にする
- 数字が出せるなら、多いとか少ないではなく数字を書く
なんか、他にも色々ある気がする。
今回書いたこと
チームとして「こうしなさい!」ということは一切言われていません。うちのチーム的にこうやって動いた方がいい感じになりそうと思って、日々やってることをまとめたものになります。書いてて思ったのが、こういう文化ってどうやって作ってるんだろうなと思いました。
シェル、シェルスクリプトの使い方まとめ
自分向けメモ。たまに更新していく。
Shellとは
ユーザーが入力したコマンドを解釈し、該当コマンドを実行する。シェルにはモードが2つある。
上記がどういったものかは割愛する。
単純コマンド
$ echo HelloWorld HelloWorld
コマンド名と任意の引数で構成されるもの。
リダイレクト
- 標準入力
- 標準出力
- 標準出力標準エラー出力
- 任意のファイル記述子
上記をリダイレクトさせることが出来るのが、リダイレクトです。(良い言い方がわからない)
標準入力にファイルを入力する例
$ cat stdin HelloWorld $ < stdin HelloWorld
標準出力をファイルに書き込む例
$ cat stdin HelloWorld $ cat stdin > out $ cat out HelloWorld
標準出力をファイルに追記する例
$ cat stdin HelloWorld $ cat stdin >> out2 $ cat stdin >> out2 $ cat out2 HelloWorld HelloWorld
標準出力・標準エラーを別ファイルに書き込む
ファイル記述子
- 1: 標準出力
- 2: 標準エラー出力
div: echo 1 2
$ make div 1> log_1 2> log_2 $ cat log_1 echo 1 1 2 $ cat log_2 make: 2: No such file or directory make: *** [div] Error 1
標準出力・エラーまとめてファイルに書き込む例
err: echo HelloWorld a
$ make err > log 2>&1 $ cat log echo HelloWorld HelloWorld a make: a: No such file or directory make: *** [err] Error 1
パイプ
構文の組み合わせ
treeの出力をlessに渡す
$ tree / | less
標準エラー出力もパイプに渡す
hoge 2>&1 | less
環境変数の一時変更
LANG=C man ls
&&(AND) ||(OR)
パイプライン1 && パイプライン2はパイプライン1の終了ステータスが成功(0)なら、パイプライン2が実行される。
パイプライン1 || パイプライン2はパイプライン1か2のどちらかが成功(0)すればおk.
err: echo HelloWorld a
$ make err && echo OK echo HelloWorld HelloWorld a make: a: No such file or directory make: *** [err] Error 1 $ make err || echo OK echo HelloWorld HelloWorld a make: a: No such file or directory make: *** [err] Error 1 OK
シェルスクリプト
行頭便利フォーマット
#!/usr/bin/env bash # Fail on unset variables and command errors set -ue -o pipefail # Prevent commands misbehaving due to locale differences export LC_ALL=C
#!/bin/sh -e # オプションつきシェルスクリプト
行頭の意味
シェルスクリプトの行頭に記述する #!
で始まる行shebangという
#!/bin/bash
bashをenvを使って指定
#!/usr/bin/env bash
基本構文
if
引数によっての分岐
#!/bin/bash if [ $# -lt 2 ];then # 引数が2個未満 echo "Usage: $0 file1 file2" 1>&2 # 標準エラー出力に出力 exit 1 else echo "eeyan" fi
$ sh if.sh Usage: if.sh file1 file2 $ sh if.sh ls file1 eeyan
case
unameを使ったOS判定の例
$ uname -sr Darwin 15.6.0
#!/bin/bash case `uname -sr` in Linux*) ls -l --full-time "$@";; FreeBSD* | NetBSD* | OpenBSD* | Darwin*) # Darwinから始まる任意の文字列 ls -lT "$@";; # ここが実行される SunOS' '5.*) ls -E "$@";; *) # 何もひっとしない場合 echo unknown OS 1&>2;; esac
$ sh case.sh total 88 -rw-r--r-- 1 jumpei staff 13 7 5 08:23:24 2018 2 -rw-r--r-- 1 jumpei staff 202 7 5 08:24:23 2018 case.sh -rw-r--r-- 1 jumpei staff 158 7 5 08:18:11 2018 if.sh -rw-r--r-- 1 jumpei staff 48 7 4 08:44:16 2018 sample.sh -rw-r--r-- 1 jumpei staff 56 7 4 08:44:35 2018 sample2.sh -rw-r--r-- 1 jumpei staff 87 7 4 08:52:39 2018 sample3.sh -rw-r--r-- 1 jumpei staff 47 7 4 20:14:09 2018 sample4.sh -rw-r--r-- 1 jumpei staff 55 7 4 20:13:57 2018 sample5.sh -rw-r--r-- 1 jumpei staff 198 7 5 08:03:53 2018 sample6.sh -rw-r--r-- 1 jumpei staff 69 7 5 07:52:58 2018 sample7.sh -rw-r--r-- 1 jumpei staff 81 7 5 07:55:30 2018 sample8.sh
for
*
はカレントディレクトリのファイル名
#!/bin/bash for file in *; do echo $file done
$ sh for.sh LICENSE README.md for.sh for_arg.sh for_bash.sh for_jot.sh for_seq.sh sample sh
引数 $@
を使ったループ
#!/bin/bash for arg in "$@";do echo $arg done for arg;do # 上と同じ意味 echo $arg done
$ sh for_arg.sh a b c a b c a b c
#!/bin/bash for i in {1..10};do echo $i done
$ sh for_bash.sh 1 2 3 4 5 6 7 8 9 10
jotを使ったループ
#!/bin/bash for i in `jot 10`;do echo $i done
$ sh for_jot.sh 1 2 3 4 5 6 7 8 9 10
for i in `seq 1 10`;do echo $i done
1 2 3 4 5 6 7 8 9 10
サブシェル
リストを()で囲むとサブシェルになり、サブシェルはもとのシェルとは別扱いで実行されます。サブシェルの中での変数の変更、umask値を変えても、サブシェルから出ると戻ります。この挙動を利用して、何らか変更をさせて終わったら、元の状態で処理もしたいなどのシチュエーションでは便利です。シェルスクリプトをリダイレクト、パイプでつないでも同じことはできます。
シェル変数 IFS
を :
に変更して、set
コマンドを実行すると :
が削除され、 PATH
の中身が $1 $2 $3
のように位置パラメータに設定されます。これらの変更はサブシェルを抜けると解除されます。
#!/bin/sh echo "IFS=$IFS" ( IFS=: echo "IFS=$IFS" set $PATH echo $3 ) echo "IFS=$IFS"
$ sh sub_shell.sh IFS= IFS=: /Users/jumpei/.anyenv/envs/rbenv/shims IFS=
グループコマンド
リストを{}でかこむとグループコマンドと呼ばれる複合コマンドになります。リダイレクトしたり、パイプに接続したり、次のシェル関数の本体として利用できる。グループコマンドはサブシェルと違って、終わった後も影響があります。
コマンドの結果をまとめてlogfileにリダイレクトしている例。
#!/bin/sh { hostname date who } > logfile
$ sh group.sh $ cat logfile jumpei-no-MacBook-Pro-3.local 2018年 7月 6日 金曜日 08時24分53秒 JST jumpei console Jul 4 07:49 jumpei ttys001 Jul 4 07:49
シェル関数
シェル関数内の変数は引数渡しのため、位置パラメータ以外はグローバル変数になりますそのため、関数内はローカルにしたい場合は、{}
ではなく ()
を使って、サブシェルにすれば可能です
#!/bin/bash greet() { echo "Hello" } greet greet2() { echo $1 "Hello" } greet2 A
$ sh func.sh Hello A Hello
エラー系TIPS
変数の設定漏れ防止
set -u
変数を宣言していないものを使った時に終了ステータスを失敗(0以外)にしてくれる。
#!/bin/bash VAL=foo echo $VAL_TYPO echo FINISH
$ sh sample.sh; echo $? FINISH 0 $ cat sample2.sh #!/bin/bash set -u VAL=foo echo $VAL_TYPO echo FINISH $ sh sample2.sh; echo $? sample2.sh: line 6: VAL_TYPO: unbound variable 1
エラーとしたくないケースを回避
途中まで処理して落としたい。デフォルト値が使える(${parameter:-word})
#!/bin/bash set -u if [ -z "${1:-}" ]; then echo "HOW TO hoge" >&2 exit 2 fi
$ sh sample3.sh HOW TO hoge
エラーになったら中断
set -e
何もつけないとエラーがあっても、最後まで実行される。しかも、ステータスコードも成功になる。
#!/bin/bash FOO=$(ls --l) echo $FOO echo "OK"
$ sh sample4.sh; echo $? ls: illegal option -- - usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...] OK 0
エラー時に途中で終了し、ステータスコードも失敗が帰ってくるようになる
#!/bin/bash set -e FOO=$(ls --l) echo $FOO echo "OK"
$ sh sample5.sh; echo $? ls: illegal option -- - usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...] 1
エラーを無視したいケース
grepで検索ヒットしない時にエラーステータスを返すので、それをいい感じにしたい。
以下のような感じにするか、コマンドでエラーとしないオプションとかを使うか
またはリストを使ったエラー無視などのパターンがあります。
#!/bin/bash set -e if find ./ | grep sample888.sh >/dev/null; then echo "Existing sample.sh" else echo "Nothing sample.sh" fi rm a || true rm a || : # trueの代わりに出来る echo "OK"
$ sh sample6.sh; echo $? Nothing sample.sh rm: a: No such file or directory rm: a: No such file or directory OK 0
パイプライン内のエラーで中断する
set -eはパイプラインの一番右のコマンドのエラーは正しくエラーとしてくれるが、途中のコマンドのエラーは無視されます。
$ cat sample7.sh #!/bin/bash set -e FOO=$(ls - l "$0" | wc -l ) echo $FOO echo "OK"
$ sh sample7.sh; echo $? ls: -: No such file or directory ls: l: No such file or directory 1 OK 0
set -e -o pipefail
をつけるとパイプライン中のエラーを検知してくれる。
#!/bin/bash set -e -o pipefail FOO=$(ls - l "$0" | wc -l ) echo $FOO echo "OK"
$ sh sample8.sh; echo $? ls: -: No such file or directory ls: l: No such file or directory 1
エラーは標準エラー出力に出力しよう
#!/bin/bash echo 'Expect stdout' echo 'Expect stderr' 1>&2
$ sh ./output.sh >/dev/null Expect stderr $ sh ./output.sh 2>/dev/null Expect stdout
bashに依存しているコマンドは#!/bin/bashと書かない
ubuntu上では以下のスクリプトは動きません。bashに依存しているなら、/bin/bashとしよう。
#!/bin/sh list=( ($ls) ) echo $list
環境変数によって動かないを避ける
export LC_ALL=C
参考文献
- ソフトウェアデザイン 2017年 07 月号 シェルのコーナー
- bash スクリプトの先頭によく書く記述のおさらい
AWS Serverless Application Model(SAM)を使ったサーバーレスアプリ構築
AWS Serverless Application Model(SAM)とは
AWS Serverless Application Model (以降AWS SAMとする) は、AWS公式のサーバーレスアプリケーションを構築するためのフレームワークです。
以前の記事のServerlessでAWS Lambdaのデプロイをいい感じにする - ぺいで、Serverlessというつーるを紹介したのですが、SAMは、公式サポートなので、AWS以外のサーバーレス使う予定ない人は、基本的にSAMを使う方向で良さそうな気がします。
SAMとServerless Frameworkの違い
今回使ってみて、感じた違いについてまとめました。
Serverless Frameworkは、全部乗せの万能ツールという感じ。AWSに限らず様々なクラウドで統一の設定が使える。また、デプロイまでが本当サクッと出来る。SAMを使って思ったことですが、裏で暗黙的色々いい感じにやってるんだなと思った。
あと、コミュニティが盛んなこともあり、プラグインとかも色々出ているので、
SAMはAWSに特化していることもあり、AWSしか使わないなら、SAM使っとけばいいやんと使ってみて思った。
SAM使ってみる
どんなもんかをひとまず動かして体感してみる。インストールはpipで出来ます。
❯ python -V Python 3.6.3 ❯ pip install aws-sam-cli ... ❯ sam --version SAM CLI, version 0.4.0
ちなみに、sam init
を実行すると見本となるテンプレートが作られる。
設定内容を追いかけてみる
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31
上記の定義は、定型的に入れろとのことをそのまま鵜呑みにすると、バージョン指定という認識で良さそう。
Globals: Function: Timeout: 5
関数全体に対する設定。今回はタイムアウトまで5秒となっている。
Resources: HelloWorldFunction: # 関数名 Type: AWS::Serverless::Function Properties: CodeUri: hello-world/ Handler: hello-world # Handler名 今回の場合、go build後のファイル名 Runtime: go1.x Tracing: Active # X-Rayによるトラッキングの有効化 Events: # どういうイベント(トリガー)を設定するか CatchAll: Type: Api Properties: Path: /hello Method: GET Environment: # 環境変数 Variables: PARAM1: VALUE
関数の定義が書かれている。
Outputs: HelloWorldAPI: Description: "API Gateway endpoint URL for Prod environment for First Function" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" HelloWorldFunction: Description: "First Lambda Function ARN" Value: !GetAtt HelloWorldFunction.Arn HelloWorldFunctionIamRole: Description: "Implicit IAM Role created for Hello World function" Value: !GetAtt HelloWorldFunctionRole.Arn
CloudFormationのOutputsセクションの定義。
出力 - AWS CloudFormation
main.go
package main import ( "errors" "fmt" "io/ioutil" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) var ( // DefaultHTTPGetAddress Default Address DefaultHTTPGetAddress = "https://checkip.amazonaws.com" // ErrNoIP No IP found in response ErrNoIP = errors.New("No IP in HTTP response") // ErrNon200Response non 200 status code in response ErrNon200Response = errors.New("Non 200 Response found") ) func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { resp, err := http.Get(DefaultHTTPGetAddress) if err != nil { return events.APIGatewayProxyResponse{}, err } if resp.StatusCode != 200 { return events.APIGatewayProxyResponse{}, ErrNon200Response } ip, err := ioutil.ReadAll(resp.Body) if err != nil { return events.APIGatewayProxyResponse{}, err } if len(ip) == 0 { return events.APIGatewayProxyResponse{}, ErrNoIP } return events.APIGatewayProxyResponse{ Body: fmt.Sprintf("Hello, %v", string(ip)), StatusCode: 200, }, nil } func main() { lambda.Start(handler) }
今回の関数。
main_test.go
package main import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/aws/aws-lambda-go/events" ) func TestHandler(t *testing.T) { t.Run("Unable to get IP", func(t *testing.T) { DefaultHTTPGetAddress = "http://127.0.0.1:12345" _, err := handler(events.APIGatewayProxyRequest{}) if err == nil { t.Fatal("Error failed to trigger with an invalid request") } }) t.Run("Non 200 Response", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) })) defer ts.Close() DefaultHTTPGetAddress = ts.URL _, err := handler(events.APIGatewayProxyRequest{}) if err != nil && err.Error() != ErrNon200Response.Error() { t.Fatalf("Error failed to trigger with an invalid HTTP response: %v", err) } }) t.Run("Unable decode IP", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) })) defer ts.Close() DefaultHTTPGetAddress = ts.URL _, err := handler(events.APIGatewayProxyRequest{}) if err == nil { t.Fatal("Error failed to trigger with an invalid HTTP response") } }) t.Run("Successful Request", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) fmt.Fprintf(w, "127.0.0.1") })) defer ts.Close() DefaultHTTPGetAddress = ts.URL _, err := handler(events.APIGatewayProxyRequest{}) if err != nil { t.Fatal("Everything should be ok") } }) }
関数に対するテスト。
.PHONY: deps clean build deps: go get -u ./... clean: rm -rf ./hello-world/hello-world build: GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world
packageする
次に作成した関数をデプロイ用にパッケージ化します。
❯ aws s3 mb s3://hoge-fuga --region ap-northeast-1
パッケージの置き場所として、S3のバケットが必要なので、任意のバケットを用意するか、既にあるものを使います。作成するときは上のようなコマンドで作成する。
❯ sam package \ --template-file template.yaml \ --output-template-file packaged.yaml \ --s3-bucket hoge-fuga
sam package
コマンドで、作成したテンプレートから、パッケージングを実行する。デプロイに必要な情報は、今回の場合だと、outpu-template-fileで指定したpackaged.yamlで作成される。
デプロイ
❯ sam deploy \ --template-file packaged.yaml \ --stack-name sam-app \ --capabilities CAPABILITY_IAM
sam deploy
コマンドで実際にデプロイをします。--template-fileには先程出てきたpackaged.yamlを指定します。--stack-nameはCloudFormationのスタック名で、任意の名前を指定すれば良さそう。--capabilitiesはロールをいい感じにしておいて的なやつ。
デプロイ出来た!便利。
ローカル実行
SAMはDockerを使うことで、ローカル実行を可能にしている。
❯ sam local start-api 2018-07-03 08:19:00 Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET] 2018-07-03 08:19:00 You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template 2018-07-03 08:19:00 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit) 2018-07-03 08:19:02 Invoking hello-world (go1.x) 2018-07-03 08:19:02 Found credentials in shared credentials file: ~/.aws/credentials Fetching lambci/lambda:go1.x Docker container image...... 2018-07-03 08:19:05 Mounting /Users/jumpei/go/src/github.com/pei0804/serverless-sample/sam/sam-app/hello-world as /var/task:ro inside runtime container START RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b Version: $LATEST END RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b REPORT RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b Duration: 1180.19 ms Billed Duration: 1200 ms Memory Size: 128 MB Max Memory Used: 10 MB 2018-07-03 08:19:08 No Content-Type given. Defaulting to 'application/json'. 2018-07-03 08:19:08 127.0.0.1 - - [03/Jul/2018 08:19:08] "GET /hello HTTP/1.1" 200 -
❯ curl 127.0.0.1:3000/hello Hello, 103.2.249.5
ログはCloudWatchライクなものが出てくる。これは強い。
ServerlessでAWS Lambdaのデプロイをいい感じにする
Serverless Frameworkとは
通常AWS Lambdaを使う上で面倒な手順としてある。
- 関数を作成する
- AWS マネジメントコンソールからLambdaの管理画面にアクセスする
- 関数をアップロードする(圧縮するとかもある)
- トリガーとなるイベントの設定など
上に書いたような手間を解消してくれるのが今回紹介する「Serverless Framework」(以下Serverless)です。
http://tikasan.hatenablog.com/entry/2018/06/27/083316
AWSしか使わない人へ
Serverless Frameworkは様々なクラウドへのサーバーレスをアプリケーションの展開を考えている人にはうってつけですが、AWSしか使わないなら、AWS SAMがおすすめでした。
インストール
Serverlessのインストールにはnode(v4以上)の環境が必要です。
❯ node -v v10.0.0 ❯ npm install -g serverless ... + serverless@1.27.3 added 310 packages in 19.063s ❯ serverless -v 1.27.3
IAMユーザーの作成
ServerlessはAWS APIを使って操作をします。なので、Adminstrator Acessを付与したServerless用のユーザーの作成をして、そのユーザーを使って色々な操作をすることになります。(defaultの自分のアカウントでも良いですが)
❯ vim ~/.aws/credentials [default] aws_access_key_id = hoge aws_secret_access_key = fuga [serverless] aws_access_key_id = hoge2 aws_secret_access_key = fuga2
❯ vim ~/.aws/config [default] output = json region = ap-northeast-1 [serverless] output = json region = ap-northeast-1
credentialsどうする問題は、公式が結構頑張って書いてくれてるので、そこ見たら良さそう。
Serverless Framework - AWS Lambda Guide - Credentials
今回は、専用のユーザーを作成するパターンでやっていきます。
事始め
さっそくServerlessのプロジェクトを作成してみましょう。ここからは長ったらしい serverless
ではなく、短縮形の sls
を使っていきます。slsには、templeateを作成する機能があり、これを使うと良い感じのsampleを見ることが出来ます。
選べるテンプレートは以下の通り。(すごい)
Template "true" is not supported. Supported templates are: "aws-nodejs", "aws-nodejs-typescript", "aws-nodejs-ecma-script", "aws-python", "aws-python3", "aws-groovy-gradle", "aws-java-maven", "aws-java-gradle", "aws-kotlin-jvm-maven", "aws-kotlin-jvm-gradle", "aws-kotlin-nodejs-gradle", "aws-scala-sbt", "aws-csharp", "aws-fsharp", "aws-go", "aws-go-dep", "azure-nodejs", "fn-nodejs", "fn-go", "google-nodejs", "kubeless-python", "kubeless-nodejs", "openwhisk-java-maven", "openwhisk-nodejs", "openwhisk-php", "openwhisk-python", "openwhisk-swift", "spotinst-nodejs", "spotinst-python", "spotinst-ruby", "spotinst-java8", "webtasks-nodejs", "plugin" and "hello-world".
~/go/src/github.com/pei0804/serverless-sample/serverless master* ❯ sls create --template aws-go Serverless: Generating boilerplate... _______ __ | _ .-----.----.--.--.-----.----| .-----.-----.-----. | |___| -__| _| | | -__| _| | -__|__ --|__ --| |____ |_____|__| \___/|_____|__| |__|_____|_____|_____| | | | The Serverless Application Framework | | serverless.com, v1.27.3 -------' Serverless: Successfully generated boilerplate for template: "aws-go" Serverless: NOTE: Please update the "service" property in serverless.yml with your service name ~/go/src/github.com/pei0804/serverless-sample/serverless master* ❯ ls Makefile hello serverless.yml world
では、何が書かれているか見てみましょう。
hello/main.go
よくあるhello worldサンプル
package main import ( "github.com/aws/aws-lambda-go/lambda" ) type Response struct { Message string `json:"message"` } func Handler() (Response, error) { return Response{ Message: "Go Serverless v1.0! Your function executed successfully!", }, nil } func main() { lambda.Start(Handler) }
world/main.go
package main import ( "github.com/aws/aws-lambda-go/lambda" ) type Response struct { Message string `json:"message"` } func Handler() (Response, error) { return Response{ Message: "Okay so your other function also executed successfully!", }, nil } func main() { lambda.Start(Handler) }
デプロイ先でも動くようにというお気持ちの入ったbuildコマンドが書かれていました。
build: go get github.com/aws/aws-lambda-go/lambda env GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go env GOOS=linux go build -ldflags="-s -w" -o bin/world world/main.go
serverless.yml
slsに関する設定が色々書いてある。ひとまず、serviceの名前と、regionだけ変えておく。
# Welcome to Serverless! # # This file is the main config file for your service. # It's very minimal at this point and uses default values. # You can always add more config options for more control. # We've included some commented out config examples here. # Just uncomment any of them to get that config option. # # For full config options, check the docs: # docs.serverless.com # # Happy Coding! service: hello <-- ここ自分のつけたい名前を適当につける # service: aws-go # NOTE: update this with your service name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details # frameworkVersion: "=X.X.X" provider: name: aws runtime: go1.x # you can overwrite defaults here # stage: dev # region: us-east-1 region: ap-northeast-1 <-- 東京リージョンにしておく # you can add statements to the Lambda function's IAM Role here # iamRoleStatements: # - Effect: "Allow" # Action: # - "s3:ListBucket" # Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } # - Effect: "Allow" # Action: # - "s3:PutObject" # Resource: # Fn::Join: # - "" # - - "arn:aws:s3:::" # - "Ref" : "ServerlessDeploymentBucket" # - "/*" # you can define service wide environment variables here # environment: # variable1: value1 package: exclude: - ./** include: - ./bin/** functions: hello: handler: bin/hello world: handler: bin/world # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details # events: # events: # - http: # path: users/create # method: get # - s3: ${env:BUCKET} # - schedule: rate(10 minutes) # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx # - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: # event: # source: # - "aws.ec2" # detail-type: # - "EC2 Instance State-change Notification" # detail: # state: # - pending # - cloudwatchLog: '/aws/lambda/hello' # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp # Define function environment variables here # environment: # variable2: value2 # you can add CloudFormation resource templates here #resources: # Resources: # NewResource: # Type: AWS::S3::Bucket # Properties: # BucketName: my-new-bucket # Outputs: # NewOutput: # Description: "Description for the output" # Value: "Some output value"
デプロイと実行
~/go/src/github.com/pei0804/serverless-sample/serverless master* 16s ❯ make build go get github.com/aws/aws-lambda-go/lambda env GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go env GOOS=linux go build -ldflags="-s -w" -o bin/world world/main.go ~/go/src/github.com/pei0804/serverless-sample/serverless master* ❯ sls deploy Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Creating Stack... Serverless: Checking Stack create progress... ..... Serverless: Stack create finished... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service .zip file to S3 (4.5 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... ........................ Serverless: Stack update finished... Service Information service: hello stage: dev region: ap-northeast-1 stack: hello-dev api keys: None endpoints: None functions: hello: hello-dev-hello world: hello-dev-world ~/go/src/github.com/pei0804/serverless-sample/serverless master* ❯ sls invoke -f hello { "message": "Go Serverless v1.0! Your function executed successfully!" } ~/go/src/github.com/pei0804/serverless-sample/serverless master* ❯ sls invoke -f world { "message": "Okay so your other function also executed successfully!" } ~/go/src/github.com/pei0804/serverless-sample/serverless master* ❯ sls logs -f hello START RequestId: 7dbebc56-7995-11e8-be4f-4788beaf19d2 Version: $LATEST END RequestId: 7dbebc56-7995-11e8-be4f-4788beaf19d2 REPORT RequestId: 7dbebc56-7995-11e8-be4f-4788beaf19d2 Duration: 0.75 ms Billed Duration: 100 ms Memory Size: 1024 MB Max Memory Used: 27 MB
動いているっぽい。この時点で既に便利!!
マネジメントコンソールからも確認出来た。
stageとかprofileをcliからいじる
設定ファイルにハードコーディングせずに、stageとかprofile変えたいなーっていうアレ。やり方としては、sreverless.ymlのproviderのところに設定を加えれば良い感じになる。
内容としては、--profileに指定がなければdefaultを使う。--stageに指定がなければdevという感じ。
service: hello provider: name: aws runtime: go1.x profile: ${opt:profile, self:custom.defaultProfile} stage: ${opt:stage, self:custom.defaultStage} region: ap-northeast-1 custom: defaultStage: dev defaultProfile: default package: exclude: - ./** include: - ./bin/** functions: hello: handler: bin/hello world: handler: bin/world
cliでオプションで渡す。
~/go/src/github.com/pei0804/serverless-sample/serverless master* 15s ❯ sls deploy --profile serverless --stage production Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Creating Stack... Serverless: Checking Stack create progress... ..... Serverless: Stack create finished... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service .zip file to S3 (4.5 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... ........................ Serverless: Stack update finished... Service Information service: hello stage: production region: ap-northeast-1 stack: hello-production api keys: None endpoints: None functions: hello: hello-production-hello world: hello-production-world
いい感じ!!
とりあえず、今回はここまで、次回はロジックとハンドラ分けたり、もうちょっと踏み込んだ内容やってみる。
前はapexを使ったりしていたけど、serverlessもかなり良さそう。
AWS Athena + Glueを使ったデータ分析(パーティション自動化)
ログは見れる状態にして価値が出る
ログは雑に集めてるんだけど、見れる状態になっていないというのは、ストレージ料金を食ってるだけのゴミなので、破棄するか見れるようにするのですが、見れるようにするのは、そもそも結構大変だったりする。そこでFirehose + Athena + Glueを組み合わせて良い感じにしたいと思います。
データの保存形式をどうするか
色々ある。今回はJSONを使います。
Firehose ログをS3にまとめてPUTする
Athena 集めた生ログに集計をかける
AthenaはSQLでデータ集計を実行できるサーバーレスサービスです。使い方に関しては以下の公式ドキュメントを見ればわかります。
ざっくり説明すると、AthenaはCreate TableでS3のデータに対する参照を作成して、SQLクエリで集計が出来ます。なので、Drop TableやInsertクエリしたとしても、実データを操作することは出来ません。(READだけって割り切れるので安全といえば安全)
S3のデータをAmazon Athenaを使って分析する | Amazon Web Services ブログ
Glue テーブル自動作成
AWS Glue は抽出、変換、ロード (ETL) を行う完全マネージド型のサービスで、お客様の分析用データの準備とロードを簡単にします。AWS マネジメントコンソールで数回クリックするだけで、ETL ジョブを作成および実行できます。AWS Glue では、AWS に保存されたデータを指定するだけで AWS Glue によるデータ検索が行われ、テーブル定義やスキーマなどの関連するメタデータが AWS Glue データカタログに保存されます。カタログに保存されたデータは、すぐに検索、クエリ、ETL で使用できます。AWS Glue では、データ変換とデータのロードプロセスを実行するコードが生成されます。
そして、今回はこのAthenaのテーブルを作成するのが、Glueです。
Glueでよく出てくるETLというワードについて補足。これは、Extract Transform Loadのことです。一言でいうと、データ再集計という意味です。
Glueを使う理由
Athenaは検索をかける時に、パーティションを切ることで、スキャン範囲を狭めて、コストを軽減、検索完了時間を短縮が出来るのですが、このパーティションを切る作業が微妙に面倒。(パーティションの範囲が増えるごとに切らないといけない) そのあたりの面倒な作業をGlueは自動的に良い感じにしてくれる。
Glueクローラーの作成
Add information about your crawler
Crawler name
クローラーの名前をつけます。今回は雑に「firehose」とします。
Add a data store
Choose a data store
JDBCかS3を選べます。今回は「S3」を選択します
Include path
データを保存しているS3の場所。今回でいうと、「s3://put-by-firehose/firehose」になる。
Choose an IAM role
Create an IAM roleを選択して、適当な名前を指定するといい感じに出来上がります。
内容は、AWSGlueServiceRoleという公式のロールと、以下のようなS3に対しての権限が付与されたロールが作成されました。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::put-by-firehose/firehose/*" ] } ] }
Frequency
Glueをどれくらいの頻度で動かすかを指定出来ます。今回は、テスト的に動かすだけなので、「Run on demand」にします。
Database
どういう名前でAthenaのdatabaseを作成するか、または既にあるdatabaseを使うかを選択出来ます。今回はdatabaseを作成したいので、Add databaseで「firehose」を作成して、それをそのまま指定しました。
Review
実行
CrawlerをRunする
Athenaで確認する
テーブルは確かにいい感じになってるけど。。。。名前がああああ。 この名前をいい感じに出来る方法知ってる人居たら教えてください。
現状、僕が知っている方法だと、定期的にパーティションを切るLambdaを実行するという方法しか思いつかない。(それGlue使わなくていいやんという話になる)
Amazon Kinesis Data Firehose 使い方
Amazon Kinesis Data Firehose(長いのでFirehoseとします)
どういうものかというのは、公式の素晴らしい説明文で大体わかります。
ストリーミングデータをデータストアや分析ツールに確実にロードする最も簡単な方法です。ストリーミングデータをキャプチャして変換し、Amazon S3、Amazon Redshift、Amazon Elasticsearch Service、Splunk にロードして、現在お使いの既存のビジネスインテリジェンスツールやダッシュボードで、ほぼリアルタイムに分析することができます。
使ったユースケース
弊社のプロダクト(広告配信システム系)でまあまあなリクエストを受けているLambdaのログを見れる状態にしたいという場面で使いました。
Lambdaが各々でS3にログをPUTするぜみたいなことをしていた時に、ファイル数多すぎて、BQに入れれないし、Athenaで見るにしてもログ多すぎて検索終わるのを待つと、日が暮れる(正確にはエラーになった)となっていました。
そこに、Kinesis Firehose使って複数のLambdaが吐くログを固めてS3にPUTすることで、少し待てばAthenaでログを見れるようになりました。(ファイル数が減ってファイルIOが減ったから)
また、S3にPUTする時に、時間ごとにフォルダを自動で作ってくれるので、パーティションを切りたい時も同じルールを適用出来るのはいいなーと思いました。
Stream作成
Step 1: Name and source
Delivery stream name
ストリーム名。今回の設定値は「firehose」
Source
- Direct PUT or other sources
レコードを配信ストリームに直接送信する場合、またはAWS IoT、CloudWatchログ、CloudWatchイベントからレコードを送信する場合は、このオプションを選択します。 - Kinesis stream
Kinesis streamの流し先として、Firehoseを選べます。今回のユースケースでは使いませんでしたが、これも便利そう。
今回の設定値は、Lambdaから直接流す形なので、「Direct PUT or other sources」。
Step 2: Process records
Transform source records with AWS Lambda
Lambda 関数を呼び出して、受信した送信元データを変換してから送信先に配信できます。Kinesis Data Firehose のデータ変換は、配信ストリームの作成時に有効にすることができます。 とあるように、流れてくるデータを何か手を加えたい時は、これを使うと良さそう。
Amazon Kinesis Data Firehose のデータ変換 - Amazon Kinesis Firehose
今回の設定値は、「Disabled」。
Convert record format
データのフォーマットをJSONから何かへみたいな感じの変換が出来る。
以下の2つが用意されてる。また、AWS Glueとも連携が出来るようです。(別記事で調査する予定) - Apache Parquet - Apache ORC
今回の設定値は、「Disabled」。
Step 3: Choose destination
Select destination
保存先を設定する。保存先として以下の4つがある。
今回の設定値は、「Amazon S3」。
S3 destination
- S3 bucket
保存先のS3を選ぶ。 - Prefix
FirehoseがPUTする時のフォルダ名と認識すればおk。
今回は、Create newして、「put-by-firehose」というバケットを作成。また、Prefixは「firehose/」とした。こうすることで、put-by-firehose/firehose/firehoseがよしなにという形にしてくれる。
Step 4: Configure settings
S3 buffer conditions
受信レコードをS3バケットに転送する前にバッファリングしてくれる。その時の設定をする。条件のいずれかが満たされると、PUTされる。
- Buffer size
1-128 MBの間でバッファするサイズを決めれる
- Buffer interval
60-900秒でPUTする間隔を設定出来る
今回は、どちらも最小値の「1MB」と「60秒」とする。もし、リアルタイム制が要求されない場合は、もっと高い数値を設定することになる。
S3 compression and encryption
- S3 compression
どういう圧縮形式でS3にPUTするかというもの。 - S3 encryption S3上のデータを暗号化するか
今回は、「GZIP」で圧縮し、暗号化については「Disabled」にした。
Error logging
エラーをCloudWatchログを出力する。
今回は使わないので、「Disabled」とした。
IAM role
Create newして、いい感じにしてくれます。デフォで作成される権限は以下のような感じ。まあ、公式によるものだから、オーバーな権限はないし最低限になっている。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Action": [ "glue:GetTableVersions" ], "Resource": "*" }, { "Sid": "", "Effect": "Allow", "Action": [ "s3:AbortMultipartUpload", "s3:GetBucketLocation", "s3:GetObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::put-by-firehose", "arn:aws:s3:::put-by-firehose/*", "arn:aws:s3:::%FIREHOSE_BUCKET_NAME%", "arn:aws:s3:::%FIREHOSE_BUCKET_NAME%/*" ] }, { "Sid": "", "Effect": "Allow", "Action": [ "lambda:InvokeFunction", "lambda:GetFunctionConfiguration" ], "Resource": "arn:aws:lambda:ap-northeast-1:851669633371:function:%FIREHOSE_DEFAULT_FUNCTION%:%FIREHOSE_DEFAULT_VERSION%" }, { "Sid": "", "Effect": "Allow", "Action": [ "logs:PutLogEvents" ], "Resource": [ "arn:aws:logs:ap-northeast-1:hoge:log-group:/aws/kinesisfirehose/:log-stream:*" ] }, { "Sid": "", "Effect": "Allow", "Action": [ "kinesis:DescribeStream", "kinesis:GetShardIterator", "kinesis:GetRecords" ], "Resource": "arn:aws:kinesis:ap-northeast-1:hoge:stream/%FIREHOSE_STREAM_NAME%" }, { "Effect": "Allow", "Action": [ "kms:Decrypt" ], "Resource": [ "arn:aws:kms:region:accountid:key/%SSE_KEY_ARN%" ], "Condition": { "StringEquals": { "kms:ViaService": "kinesis.%REGION_NAME%.amazonaws.com" }, "StringLike": { "kms:EncryptionContext:aws:kinesis:arn": "arn:aws:kinesis:%REGION_NAME%:hoge:stream/%FIREHOSE_STREAM_NAME%" } } } ] }
Step 5: Review
StatusがActiveなったら使える状態になります。
Lambda -> Firehose
ロール作成
今回は、LambdaからFirehoseへPUTしたいので、AWSLambdaMicroserviceExecutionRole
やAWSLambdaBasicExecutionRole
のロール以外に、インラインで以下のような雑なロールを付加しました。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "firehose:PutRecord", "firehose:PutRecordBatch" ], "Resource": "arn:aws:firehose:*:*:deliverystream/*" }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "firehose:DescribeDeliveryStream", "firehose:ListDeliveryStreams" ], "Resource": "*" } ] }
関数作成
import boto3 def lambda_handler(event, context): firehose = boto3.client('firehose') firehose.put_record( DeliveryStreamName='firehose', Record={ 'Data': b'{"user_id": "a", "event": "AAA"}\n' } ) firehose.put_record( DeliveryStreamName='firehose', Record={ 'Data': b'{"user_id": "b", "event": "BBB"}\n' } ) firehose.put_record( DeliveryStreamName='firehose', Record={ 'Data': b'{"user_id": "c", "event": "CCC"}\n' } )
今回はLambdaから叩いていますが、もちろんローカルからの実行でも動きます。
PUTしてみる
Lambdaを実行すると、以下のようにサクッと出来上がりました!便利! PUTするだけで、ある程度固めてくれるのは相当ありがたい。
put-by-firehose/firehose/2018/06/24/05/firehose-1-2018-06-24-05-47-40-34054ccb-1fa3-47d6-b05f-749b51d032a5.gz
集めたデータどうする
記事の冒頭でも書きましたが、Athenaでいい感じに見れようにしたりも出来るんですが、体力が尽きたので、次の記事で紹介することにします。