Ponz Dev Log

ゆるくてマイペースな開発日記

CloudWatch EventsからAWS Batchを叩くCfnテンプレートが書けないからCLI使う

CloudFormationテンプレートのYAMLファイルからAWS Batchを叩くCloudWatch Eventsのルール・トリガーを作ろうとしたらダメだったから代案としてCLIで作った話です。

CloudWatch Eventsのルールに対して、どのサービスをどのようなイベントに反応して/時間間隔で動かすのかを "ターゲット" として定義します。 折角なので他の環境でも使い回せるようにCloudFormationを使ってAWS Batchを起動するCloudWatch Eventsのターゲットを作れないかとドキュメントをみてる見ると、、、それらしい記述がない。

docs.aws.amazon.com

AWS APIとして定義はあるのかと見てみると、こっちにはTargetオブジェクトにBatchParametersというプロパティがあるから実は裏仕様としてCloudFormationを叩けるのでは?

docs.aws.amazon.com

以下のようにCfnテンプレートを作成して実行してみます。結論としては作成できませんでした。

AWSTemplateFormatVersion: "2010-09-09"
Description: AWS Batch Trigger template
Resources:
  PzBatchScheduledRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "Triggers AWS Batch Job"
      Name: "pz-aws-batch-trigger"
      ScheduleExpression: "cron(0 0/1 * * ? *)"
      State: "ENABLED"
      Targets:
        - Arn: !Sub "arn:aws:batch:${AWS::Region}:${AWS::AccountId}:job-queue/logger-queue"
          Id: "Pz-Batch-Target-test"
          RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/service-role/AWS_Events_Invoke_Batch_Job_Queue_1960059509"
          BatchParameters:
            JobDefinition: !Sub arn:aws:batch:${AWS::Region}:${AWS::AccountId}:job-definition/scala-args-logger:2
            JobName: "test-batch-job"
          Input: "\"{\\\"Parameters\\\":{\\\"jobType\\\":\\\"test\\\",\\\"languageCode\\\",\\\"ja\\\",\\\"app\\\":\\\"APP\\\"]}}\""

以下結果のスクリーンショットです。

f:id:accelerk:20190224191323p:plain
BatchParametersはサポートされていない

Encountered unsupported property BatchParameters ...なるほど🤔 無理なのか。 ここに時間をかけてる場合じゃない。リリースと同時に作ればいいと割り切ってCLIから作ることにします。

docs.aws.amazon.com

# ルールの作成
aws events put-rule --name "test-batch-rule" --schedule-expression "cron(0 0/1 * * ? *)"

# ターゲットの作成
aws events put-targets \
    --rule "test-batch-rule" \
    --target "Id"="batch-executor-rule-id",\
"RoleArn"="arn:aws:iam::${AWS:Account}:role/AWS_Events_Invoke_Batch_Job_Queue_1960059509",\
"Arn"="arn:aws:batch:${AWS:Region}:${AWS:Account}:job-queue/logger-queue",\
"BatchParameters"="{"JobDefinition"="arn:aws:batch:ap-northeast-1:${AWS:Account}:job-definition/scala-args-logger:2","JobName"="test-batch-name"}",\
"Input"="\"{\\\"Parameters\\\":{\\\"jobType\\\":\\\"test\\\",\\\"languageCode\\\",\\\"ja\\\",\\\"app\\\":\\\"APP\\\"]}}\""

これでなんとかCloudWatch EventsからAWS Batchを起動するルールを作成できました。今後CloudFormation経由で実行されることを期待します。。。

他にも罠があり、以下2つを見つけました。ご参考までに。

  • ジョブ定義のRefの個数とCWEのRefの数が一致しないとそもそもイベントがトリガーされなかった。。。
  • Refの後のキー名は先頭小文字にしないとトリガーされない。

以上。

JAWS DAYS 2019 参加レポート

日本のAWSユーザーグループであるJAWSが主催の "JAWS DAYS 2019" に行ってきました。お昼の時点で参加人数1,500人を超えていたこともあって、200人収容できるセッションでも立ち見が出るほどの盛況っぷりなのが印象的。

忘れないように会場で実際に聞いてきたセッションについて簡単にまとめておきます。


[Serverless] サーバレスで動かすトークン発行プラットフォーム

jawsdays2019.jaws-ug.jp

実際にトークン発行プラットフォームとして使っているEtheriumすごい的な話ではなく、如何に新しい技術を使って日々新しいものを作るかの過程の話だった。

新しいものを使うことはリスクになる上に、人・時間・知識がない中で最小限のプロダクトを作ってしまおう。サーバーレスは良いソリューションになるんだってのがこのセッションでの学び。

[Serverless] AWS Serverlessを活用したサービス監視

slide.seike460.com

サーバーレスを使いつつ、監視コンポーネントAWSのサービスの組み合わせをラップしたOSSを作った話。

最近本屋に平積みされているオライリーの "入門 監視" の内容を踏まえて、監視に必要な要素を明確にしながら自分でOSSを作ってしまおうというところがすごい。

AWSとのサービス統合でポイントになっていたのが、API Gatewayから直接Lambdaを叩かずSQSを間に挟んでいること。監視エージェントとストレージを疎結合にするための戦略なのは勉強になった。それにログやメトリクスとか膨大な量のデータを一気にAPI Gatewayで受けたらすぐにスロットリングしちゃうし、CloudWatch LogsにLambdaのサブスクリプション貼る時と同じことになるもんね。

API GatewayがLambda以外のサービスに直接リクエストできるのも初めて知った。。。

[EC2/DB] RDBリファクタリングと異種間DB移行の戦い – Amazon DMSを使った止めずにリファクタリングする手法

soudai.hatenablog.com

そーだいさん(id: Soudai)のRDBリファクタリングのセッション。

DBの寿命はアプリよりも長い。アプリが動いて数年経つと、謎テーブルの存在ややカラムに意図しない値、制約が貼ってない等々技術的な負債が貯まってくる。そんな状況でRDBリファクタリングされた時(現在進行形らしいですが)の挑戦記録を話されていました。

2ステップ大きく分けてされていたようで、どちらのステップも興味深い。

  1. 現状Aurora MySQL5.6で運用しているテーブルをリファクタリングするために、トリガー(SQLが発行されたイベントに反応して実行される処理)を使う。
  2. MySQL5.6だと1テーブル1イベントに対して1トリガーしか貼れないので、この制約がないPostgreSQL for RDSにマイグレーションする。

MySQLからPostgreSQLに移行するときに使用するのがAWS Database Migration Service (DMS)です。 なので、現行Aurora MySQL --(DMS)--> 旧スキーマPostgreSQL --(トリガー)--> 新スキーマPostgreSQL の流れでマイグレーションしたと。

参照はAPI経由でいきなり新しいスキーマPostgreSQLから実施することでModel単位に移行させるといったことは今後の業務で使えそうなTipsです。

さらに興味深かったのが、実運用のところ。平気で20~30秒(?)マイグレーションが遅延する、更新が激しいとDMSが単一障害点になってしまうといった使ってわかったデメリットなんだとか。なるほどな。

最後にそーだいさんが言っていたように、RDBリファクタリングは「覚悟」が必要とのこと。肝に命じておこう。

[Lunch Session] AWSからメール送るならSendGrid一択ですよね

jawsdays2019.jaws-ug.jp

自宅から歩ける範囲に平日にSendGridの勉強会をやっているのは知っていたけど、なかなか行けなかたので参加。

ランチタイムの15分セッション。SendGridそのもののアピールがほとんどでしたが、SESとSendGridの比較が乗ってて確実にメールを届けるなら良い選択肢だと認識。

[Lunch Session] クラウド時代のモニタリングといえばDatadogだよね

jawsdays2019.jaws-ug.jp

AWSだけでなく、一元的にモニタリング・メトリクス・ログ・APM収集できるのを再確認。2018のre:Inventで一番デカイブースを構えていたとのことだったので、ちょっと興味が湧いたな。

可愛いDatadogのステッカー貰えたし、使ってみようかしら。Mackerel、ElasticSearch/Kibanaと比較して要検討。

[DevEnv] 至高の CI/CD パイプラインを実現する5つの約束

speakerdeck.com

内容は上のスライドにまとまっているけど、肝心だと思ったとこだけ抜粋 + 当日のメモ。

  • パイプライン ファースト
    • アプリよりも先にまずはCI/CDパイプラインを作ることを強調。
    • 一発目のデプロイからアプリは雛形でいいからパイプラインでデプロイを書けることが、ROIの高い投資だと表現していた。
    • パイプラインも最初から整えなくても、手元のデプロイスクリプトをまとめるだけでひとまずOK

自動化されたパイプラインを維持

  • 自動化できない変更は避ける
  • パイプラインにアプリ都合の複雑なオペレーションを押し込まない、アプリで吸収しちゃう

柔軟なパイプラインの維持

  • 常にシンプルにキープ
  • パイプラインをコード化する

パイプラインのUXの継続的改善

  • CI/CDパイプラインを開発メンバーに提供するサービスとして考える
  • 何が実行されて、なぜデプロイが失敗したのか分かるよう
  • 時間短縮を図ろう
  • 作り込みすぎて安定性を失うことを避ける

パイプラインを唯一のデプロイ方法にする

  • "とりあえず手作業" は楽だが禁忌 (ビジネスが危機的な場合のみ許す)
  • パイプラインが有名無実の代物になるから

[Supporter Session] 三題噺「F-Secure 基幹システムは Serverless !あと IoTセキュリティとAWSセキュリティ」

jawsdays2019.jaws-ug.jp

F-Secureというセキュリティソフト・コンサルティング会社の営業さんがスピーカーのセッション。なんとLT3本立てという豪華な構成に加えて、話がめちゃくちゃうまかった。

会社の事業紹介しつつAWSを使いながらセキュリティサービスをどう提供しているかというお堅い感じのサービスにも関わらず新鮮なトークでした。

ユーザーがアンチウイルスソフトの利用登録とアクティベーションを可能な限り早く行うため、かつユーザーを逃さないようにサーバーレスにしたとのこと。AWSユーザー企業ケーススタディの1つにも乗っていたのでアーキテクチャ図を見てみましたが、Kinesis とLambdaの組み合わせで実現しているのが面白い。

aws.amazon.com

[Others] Infrastructure as Codeに疲れたので、僕たちが本来やりたかったことを整理する

speakerdeck.com

個人的に一番登壇者の苦労に共感できたセッション。(会場の人の反応を見る限り大多数が同じような悩み・つらみを抱えていそう)

リソース管理のためにTerraFormテンプレートを書いてたけど、本当はそこにばかり時間をかけている場合じゃなくて価値あるタスクをするべきなのに辛さばかり目立つという話。

ちょうど前日・前々日とCloudFormationからCloudWatch Eventsのルールを作れないとこでハマってたので、ただただ共感でした。

ここでのセッションの要点としては、ROIを考えてInfrastructure as Codeで管理すべきこととすべきではない(割りに合わない)ことを考えましょうという点でしょう。

ROIが低いならWebコンソールやCLIからでええやんとのことでしたが、私が考えている答えとしてはほぼ同じです。ただ、本番環境でも使うサービスなら失敗してもロールバックできるように出来る限りCLIがベターかなとは思います。

[CLI] AWS CLIではじめるコマンドラインライフ 〜 正しい「運用自動化」への第一歩

jawsdays2019.jaws-ug.jp

えーまじ〜GUI使うのは中学生までだよね〜をかなり本気で考えている(ような) JAWS CLI支部の方のセッション。

AWSを真に理解するとはAWS APIの働きを理解することである。AWS CLIならAWS APIのほとんどを操作できるのだから、CLIで操作することこそAWSを真に理解するための道であると説いていたのが印象的。

確かに本番環境でGUIをいじるのは流石にないけど、CloudFormationで時間かけて無駄に長いスタックのYAML書くよりもCLI書いて直接操作するのが直感的だ。

  1. 何はともあれ、公式リファレンスを読もう。
  2. CLI補完機能を使おう! aws_completer をセットするだけ。
  3. --queryオプションを使おう! 出力制御がCLIだけで完結できるから、jqよりもいいぞ! docs.aws.amazon.com

JAWS CLI支部の勉強会開催場所は会社のすぐそばだし行こうかしら。。。


上記以外にもEKSやDevSecOps/テスト自動化のセッションと懇親会の時のLTも聞きたかったけど、時間が被って断念。。。SlideShareやSpeakersDeck、Twitterに資料が上がることを期待します。

セッション以外も嬉しかったポイントがあって、広い会場でも確実にスピーカーの声が聞こえるように一人一人スピーカーホンが配られてたのはGJだった。隣のセッションとはパーティション一枚でしか区切られてなかったから、結構声が隣から漏れてきてたし。

あとはお弁当豪華でしたね。さすが "満願全席" がテーマなだけありました。(写真は取り忘れた。。。無念)


以上。

AWS Batchに渡したCommandがプログラムの引数に渡らない時はDockerfileを見よう

お仕事でも使ってるAWS BatchでCommandに渡した値がプログラム(Javaだとmain関数のargs部分)に渡ってこなくてハマった時の対象方法メモ。 AWSの公式ドキュメントが分かりずらいので、Dockerのドキュメントも参照しながら解決しました。

Commandとコマンドライン引数の関係

そもそもの話。AWS Batchのコンテナプロパティ"Command"はDockerfileに記述されたどの部分にマッピングされるのだろうか? AWS Batchのジョブ定義のパラメータのセクションを見ると以下のように書いてあります。

このパラメータは、Docker Remote API の コンテナを作成する セクションの Cmd にマッピングし、 COMMAND パラメータを docker run にマッピングします。Docker CMD パラメーターの詳細については、https://docs.docker.com/engine/reference/builder/#cmd を参照してください。

🤔???

分かりづらいので噛み砕いて説明します。。。AWS Batchに渡されたジョブ定義のCommandはdocker runの後に続くパラメータとして渡されます。Dockerfile内での記述でどうも引数の渡され方の挙動が変わる らしく、以下のような感じになります。

  • CMDでコマンドを指定した --> ジョブ定義で指定したCommandの1番目が実行可能バイナリ、2番目以降が引数となる
  • ENTRYPOINTでコマンドを指定した --> ジョブ定義で指定したCommandが全てENTRYPOINTのコマンドの引数となる
  • CMDとENTRYPOINTでコマンドを指定した --> ジョブ定義で指定したCommandが全てENTRYPOINTのコマンドの引数となる

3番目のパターンに関してはDockerのドキュメントの方に以下のようなコメントがありました。

If the image also specifies an ENTRYPOINT then the CMD or COMMAND get appended as arguments to the ENTRYPOINT

例えば下のようなDockerfileに対してジョブ定義のコマンドで "hoge fuga"と渡した場合は java -jar /app/application.jar hoge fuga となります。 混乱しないようにするためには、Dockerfile内はENTRYPOINTのみ指定して、ジョブ定義は引数扱いにした方が良いですね。

FROM openjdk:8-alpine
COPY target/scala-2.12/args-logger-1.0.0.jar /app/application.jar
ENTRYPOINT ["java", "-jar", "/app/application.jar"]

今回引っかかったところはDockerfileでCMDを指定したつもりでコマンドを書いていたら実はENTRYPOINTだったから引数が欠けてしまったというオチでした。。。

CommandとParameterの関係

ついでに、ジョブ定義にはCommandとParameterというパラメータがあります。関係性としては、Command内に変数を埋め込んだ場合に実際の値としてParameterの値を入れることになります。値を外から(CloudWatch Eventsで指定した値とか)Parameterとして注入することでアプリ内で動きを変えるといった使い方ができるようです。

DockerfileとCommandとParameterを設定して動かす

実際に動かして確かめます。渡された引数をログ出力するだけのScalaのアプリを使います。 commandパラメータはジョブ定義で以下のように ["Ref::jobType", "Ref::language"] として変数化しました。

f:id:accelerk:20190218002513p:plain

実際に渡したパラメータは下図の通り。

f:id:accelerk:20190218002550p:plain

実際に動かしてみると...

f:id:accelerk:20190218002637p:plain

確かにcommandパラメータにParameterを埋め込んだ値で引数が全て渡っていますね。

GCP Professional Data Engineer認定取得しました

GCP Professional Data Engineer認定試験に合格してきました! 認定取得にあたり、取得するまでのポイントや試験のことを書いておきます。

cloud.google.com

事前に準備したこと

  • Coursera   - 各サービスのコンセプトやらユースケースを細かく詳しくレクチャーしてくれる。
    • 分量が多いので、概要を掴むくらいを期待するなら重すぎるかも。   - 間に手を動かすパートと小テストがあったから少しは自信がつく

www.coursera.org

  • qwiklabs   - 手を動かす用. アカウントは制限時間ありで用意してくれる

押さえるべきサービス

データに関するサービスだけでなく、セキュリティやログ出力設定まで聞かれます(システムを作る/使う上では基本だもんね)。

ざっと列挙するだけでも以下のサービスを一通り抑える必要がありました。 もちろん、現時点で使っているサービスがあるならそれを掘り下げると良い。自分みたいにほとんど知らないなら、チュートリアルの一つ目だけやって触るだけでもしたほうがいいです。

特にBigQuery, IAM, ストレージ/データベースサービスの使い分け, 機械学習の方法(特徴量エンジニアリングとか)は押さえておく必要がありました。

  • IAM (Identity and Access Management)
    • 権限, 職掌分離を行うサービス。他のクラウドベンダーのIAMとほぼ同じ機能。
  • BigQuery
    • いわゆるデータウェアハウス, 引くレベルで高速にデータをSQLで探索できる。
    • テーブルやビューの作成を作成してデータを保存できるけど、データの検索に重きを置いている印象。
  • Google Cloud Storage
    • ストレージサービス。アクセス頻度やオブジェクトを置くリージョンによって最適なストレージタイプが異なる。(ここはAmazon S3と同じかも)
    • ストレージタイプはリージョンマルチリージョン/ 単一リージョン / 低頻度アクセス用 / アーカイブ用の4タイプ。
  • Cloud Pub/Sub
    • Publish-Subscribe型のメッセージングサービス。
    • プッシュ通知的なpush型, キュー的なpull型の2タイプあり。
  • Cloud Dataflow
    • データ処理のパイプライン(複数のデータ処理を順序づけて/ 並列にして実行する1セット)を構築するサービス。
    • ストリーミングもできるし、バッチ処理もできる。
    • GCPのサービス間(Pub/Sub -> BQ, GCS(Avroファイル) -> BigTable)の転送するだけならば事前に用意されたテンプレートを使えばOK!
    • カスタムの処理を組み込む場合は Apache Beam SDK(Java or Python)で書いてデプロイする。
  • Cloud Datalab
    • Jupyter Notebookのホスティングサービス。
    • Git連携(ungit)だけでなくBigQuery, ML EngineといったGCPのサービスとの連携もできる。
  • Cloud Dataproc
    • Spark / Hadoopホスティングサービス。
    • 一度クラスターを立てたらずっと起動したままではなく、一定時間未使用のまま経過したらクラスターをシャットダウンできるのでお財布に優しい!
    • 既存のSpark / Hadoopで組んだサービスをクラウドに移行するならDataprocに移すのが良いとされる。
  • Cloud Dataprep
    • GUIベースのデータ変換、データクリーニングサービス。
    • BigQueryのテーブルやGCSにおいたCSV等のデータの中身の欠損値を除去したり、テーブルJOINしたりできる。
    • 操作はレシピという形で実行されるけど、実態はDataflowのジョブのようです。
  • Cloud ML Engine
  • Vision API
    • 学習済みの画像認識機械学習モデルをAPI経由で呼び出せるサービス。
    • Amazon RekognitionやWatson Visual Recognitionに相当
    • 認定試験も問題を見る限り、自分でTensorflowでモデル作るよりも既に学習済みのモデルあるならそっち使った方が早いよという意図で扱っているようです。
  • Stackdriver Logging / Monitoring
    • 名前の通り、ログやモニタリングのサービス。
    • ほとんどのサービスでは組み込みでStackdriver Loggingでログを見れるようにはなってる。
  • Cloud Bigtable
    • ワイドカラム型のスキーマを持ったNoSQLデータベース。
    • 読み込み、書き込みのレイテンシが早い。
    • IoT機器からのデータの保存先で使ったり、時系列データをストリーミングして書き込んだ後に分析用にBQへデータを流し込むといったユースケースが考えられる。
  • Cloud SQL / Cloud Spanner
  • Cloud Composer
    • Apache Airflowベースの構成管理, ワークロード管理ツール
    • AWSのCloudFormation + Step Functionを合わせたような印象だけど、複数のサービスを組み合わせたジョブスケジューラーとして最適かも。
  • Cloud Storage Transfer Service
    • 特定のデータソースに格納されたデータをGCSに転送するサービス。
    • データソースには、HTTP/HTTPSのエンドポイントをもつソース / Amazon S3(!?) / GCSが使える!

試験受験したとき

大きく分けて以下2種類。出題数的には前者8割、後者2割でした。ケーススタディは試験の公式サイトに載っているやつそのままです。

  • 特定のユースケースに沿って、どのサービスをどのように使うか
  • ケーススタディに沿って各課題に対するソリューションとしてどのサービスを使うか

時間は2時間ありましたが、1.5時間で一通り解き終わりました。全部見直しするには少し時間が足りなかったです。。。 認定試験のサイトだとケーススタディを猛烈プッシュしていましたが、実際には各サービスについての一問一答形式が多くて焦ります。AWSの試験を受けたことがある方には同じ形式の出題だったと言えば分かりますかね笑

また、ケーススタディはビジネス要件(履歴データを集約して予測分析したい、本番環境を柔軟にスケールさせたい)、技術要件(出来るだけマネージドサービスを使う、Hadoopワークロードはそのまま移行したい etc...)が出題として問われたり、時にはヒントになっていました。 ただ、受験した会場特有なのか分かりませんが、左に問題、右にケーススタディの構成や要件が表示という2窓構成でした。めっちゃ見づらかった。。。

認定試験を通しての学び

Kubernatesを触るために最初に使い始めたGCPでしたが、今回の認定試験の学習を通してデータ分析や分析基盤の構築について学べました。 特に以下のことについては今まで触ったことのあるクラウド(IBM Cloud, AWS)以上に理解できたと自信を持てます。

  • 分析用途のデータ処理パイプライン構築にはどのようなサービスを組み合わせれば良いか。
  • 機械学習サービスは他のクラウドベンダーのAIとは何が違うのか。どのように使うのか。事前に使えるモデルは何か。
  • 分析に使うデータウェアハウスのBigQueryの特徴。
  • ストレージの使い分け。

個人的にはデータをSQLで分析するBigQueryやDataflow, Dataprepと他のクラウドで処理したデータを連携させたりできないかとアイデアを膨らませるいいきかっけになったかと。 例えばAWSGCPの2つを使ってアプリのログのデータ分析をマルチクラウドできるんじゃないか? (冗長かもしれないけど)

  • AWS ECSで動くアプリケーションのログをCloudWatch Logsに集約
  • CloudWatch Logsのログ生成をAmazon SNSのトピックに流し込む
  • SNSをサブスクライブするLambda経由Kinesis Firehoseにデータを入れる
  • Kinesis FirehoseからAWS S3にログをストリーミングで直接保存
  • Cloud Storage Transfer ServiceでS3からGCSにログデータを転送
  • DataflowでGCSからBigQueryに流し込む
  • BigQueryで分析

後日談

認定通ったら社内のPrimary Job Role Specialtyが Application Developer: Google Cloud Microservicesになってました⸜(* ॑꒳ ॑*  )⸝笑 今後はGCP Associate Engineer取ってみたいですね。あとはAWS SAAも!

BuildKitを使って docker build のビルド時間を半分以下にする

元ネタはAWS CodeBuildでBuildKitがサポートされたという話。

dev.classmethod.jp

BuildKitとは、Docker v18.06から搭載された "次世代 docker build" のようです。BuildKitを使わないよりも圧倒的にビルドパフォーマンスが高くなる&セキュアにビルドできるようなので試してみました。

BuildKit自体の説明は上の記事でも紹介されていたSlideShareが一番簡潔で分かりやすいので是非見ていただければと。

www.slideshare.net

実践

今回は以下のようなJavaのコードをGradleでJarに固めることを試します。 やっていることはSlackのWebhook URLにUUIDをメッセージとして投げつけているだけです。

package net.ponzmild;

import java.io.IOException;
import java.util.UUID;
import net.ponzmild.client.SlackClient;

/**
 * 処理のエントリーポイント
 * @author ponzmild
 */
public class Handler {

  public static void main(String[] args) {

    String uuid = UUID.randomUUID().toString();
    System.out.println("Message UUID is " + uuid);

    String webhookUrl = System.getenv("SLACK_URL");
    if (webhookUrl == null || webhookUrl.isEmpty()) {
      System.out.println("Environment variable 'SLACK_URL' doesn't set or is empty.");
      System.exit(1);
    }

    SlackClient slackClient = new SlackClient(webhookUrl);
    try {
      String responseStr = slackClient.postMsg(uuid);
      System.out.println("Response is " + responseStr);
    } catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
    }
  }

}

また、Dockerfileは以下のようにマルチステージビルドで何もしなくても比較的高速でビルドされるようにします。

FROM openjdk:8-jdk-alpine as build
COPY . /usr
WORKDIR /usr
RUN ./gradlew build

FROM openjdk:8-jre-alpine
COPY --from=build /usr/build/libs/dck-slack-messenger-1.0.jar /app/
ENV SLACK_URL https://hooks.slack.com/services/xxxxxx/yyyyyyyyy/zzzzzzzzzzzz
CMD java -jar /app/dck-slack-messenger-1.0.jar

BuildKitを使わない場合

まずは BuildKitを使わないdocker buildから。以下のように見たことあるようなログが出ますね。(ログが長かったのでGradleのビルド部分しか見えませんが...) ビルド所要時間は 約90秒 でした。

f:id:accelerk:20190109015058p:plain
BuildKitを使わない場合の docker build

BuildKitを使った場合

お次は BuildKitを使った場合です。 BuildKitの使い方は簡単で、ターミナル上で export DOCKER_BUILDKIT=1 をセットするだけ!ビルドしてみましょう。

f:id:accelerk:20190109015322p:plain
BuildKitを使った場合の docker build

おお!全然ログが違いますね。。。ビルド時間は 41秒 !! BuildKitを使わない場合の半分以下じゃん! それと自分の設定によるものかもしれませんが、ログ出力の観点では以下のような違いもありました。

  • 各ステップの所要時間が出力されるようになった。上の画像の右端に1/10秒単位で吐かれていますね。
  • Dockerfileで指定した EVN がログに出力されなくなった。
  • Gradleのビルド途中経過のログが一切出力されなくなった。
  • マルチステージビルドの場合は、ステージ名も出力されるようになった。

環境変数がログに出なくなったのは個人的には嬉しいです。デバッグしづらいかもしれませんが、秘密鍵とかトークンがそのままログに出るとセキュアではないですしね。

AWS以外でもDockerのバージョンさえ満たしていれば、他のクラウドやCIサービスで大いに活用できそうです。CI時間が長いとお困りの方は使ってみてはいかかでしょうか?

GCP BigQueryチュートリアルお触りメモ

GCPのData Engineer認定試験に向けて、データウェアハウスに分類されるBigQueryのチュートリアルを一周したのとドキュメントを読み漁った時のメモです。最近メモ続きで今回はコードは一行も出てきませんが、試験直前に見直すために書き残します。そろそろパイプラインとか自前で作ってみないと。。。

データロード方法

データを流し込むのは以下の方法がある。

データロードのTips

  • 直接ファイルをBQに入れると Upload -> Importの二段階が走って途中でこけると悲しい。GCS経由だと10倍程度早いようだ。
  • 圧縮データでロードする場合は、Avroが最適、次点がPurquet。圧縮率が高いのに、並列読み込みできるから。
  • CSVJSONファイルは、gzip圧縮よりも圧縮なしの方が早い。
  • 基本は文字コード UTF-8で入れること。ISO-8859-1でも読み込み可能だが、UTF-8に変換しながら読み込むため遅い。

テーブル構造

  • 構造化データを入れる
    • ただし、JSONのようにネストした形式のデータでもOK。この場合は、カラム名comitter.name,comitter.timestamp のようにドットつなぎ
    • スキーマは手動で定義するか、サポートされたファイルから読み込む場合は自動検出で定義可能。

ストリーム処理

  • ストリーミングの上限10.000行 / sec
  • ストリーミングインサートするときは、上記の上限に引っかからないようにDataflowを噛ませた方が良さそう。
  • DataflowならPub/Sub, GCS, JDBC(アプリ経由)のテンプレートが用意されているから使うハードルは低い。
  • バッチは強力な整合性、ストリーミングは結果整合性と整合性の性質が異なる。(チュートリアルでFluentd -> BQの場合は1分くらいラグがあった)

課金の話

  • 課金対象は、読み込んだ行数 で決まるので出力結果だけ絞っても金額は絞らない場合と変わらない。(よって、LIMIT句を加えても課金額は変わらない!!)
    • 全ての列を読み込む SELECT * は最低の所業。必要な列だけ取りなさい。
    • テーブルの中身を見るだけなら、プレビューを使おう。単に中身をチラ見するだけなのにクエリを投げるのは課金されるので無駄。
    • 特定の期間のデータのみクエリしたい場合は、時系列のパーティション属性(隠しフィールド扱い?)を使って、時系列パーティションでクエリを投げよう。
    • JOINが多い/共通のクエリがあるならば、事前に中間テーブルを作ろう。

権限設定周り

模擬試験でも出てきて焦ったところだけれども、この人にはこのデータセットは見せたくないとか権限設定は大事。

  • アクセス制御の基本はここを参照
  • 権限設定はプロジェクトレベルが適用されて、あとはデータセット単位(?)
  • 役割ごとに見せたいビューやデータセットを制限したい場合は、データセットを分けること。
  • データセットの権限付与と承認は新しいコンソール画面からじゃできない。"従来のUI”からのみ実行可能。

JavaScriptの2次元配列の展開・重複削除・集約をLodashを簡潔に書く

2018年末に書いたSlack Botを少しリファクタリングした時の話。2次元配列と配列要素の重複削除ってよく使いそうで意外とベストなやり方ってよく分からないですよね。自分の場合は2次元配列は mapforeach、重複削除は Set オブジェクトで実装していましたが、後からぱっと見て処理が訳わからんコードになりがちです。JSって flatMap ってないので気合いで書いちゃう。

以前書いたTrello/Slack/OpenWhiskで作ったBotでも同じような処理を実装しなければいけなくて、やっぱり読みづらいコードが爆誕してしまいました。正直コード書いてから1週間経った今でさえ分からんw

ponzmild.hatenablog.com

さて、こんなダメコードを書いていてはいけないと解決策を模索したところ辿り着いたのがLodash

lodash.com

ライブラリではよく中の実装で使われている印象があり今更かよ感が出ていますが、Lodashで関数を組み合わせて実装してみると思った以上に読みやすくなったのでBefore/Afterで見比べながら書き残します。

解決策

入力データ(Trello APIを呼び出した時のレスポンス)は以下のようなJSONです。

[
  {
    id: "dfhajsdfhalsdfhasrber",
    name: "task1",
    labels: [
      { id: "aaa", name: "hoge" },
      { id: "bbb", name: "fuga" }
    ]
  },
  {
    id: "ruweqioryqweryqweyr",
    name: "task2",
    labels: [
      { id: "aaa", name: "hoge" },
      { id: "ccc", name: "piyo" }
    ]
  }
]

この関数のアウトプットとしては、{name: "hoge", count: n} というオブジェクトの配列にしたい。

改善前

まずは年末の働かない頭で書いた以下のコードを見て欲しい。

function summarizeLabels(cards) {

  // 二次元配列でラベルを取り出して展開し、シンプルな文字列のみの配列にする ... (1)
  let labelLists = [];
  cards.map(card => {
    card.labels.forEach(label => labelLists.push(label.name));
  });

  // 配列内のラベル一覧を重複削除で取り出す ... (2)
  const labelSet = new Set(labelLists);

  // ラベルごとの件数を配列とラベル一覧から導出 ... (3)
  let labelResults = [];
  for (let targetLabel of labelSet) {
    const labelCount =
      labelLists.filter(cardLabel => cardLabel == targetLabel).length || 0;
    labelResults.push({ name: targetLabel, count: labelCount });
  }

  return labelResults;
}

やっていることとしては、以下3つ。文字に起こすと長いですね。

  1. まず label プロパティを取り出すと配列の中に配列が入れ子になった2次元配列の形になる。これは扱いづらいから展開しておく(1)。
  2. 次にキーを取り出すために(1)の配列を重複削除してキーのリストを取り出す(2)。
  3. 最後に(2)のキーのそれぞれに対して(1)の中で合致するラベルの数をカウントしてオブジェクトに突っ込む(3)。

いやー、、、 よ゛み゛つ゛ら゛い゛よ゛お゛

正直後で直したくないコードが見事に爆誕です。理解しづらい理由としては、2次元配列を展開していることを中から外に向かって処理を追わないと理解できない、Setってなんだっけ、filter関数と突っ込む先の配列定義が離れすぎて何の配列か分からない。こんなところでしょうか。

改善後

Lodashで関数チェーンを作り上げて直感的に書くことを試みます。最終的なアウトプットに値を集計する関数のみ自分で作成し、これ以外はLodashで定義された関数を使います。

const _ = require("lodash");

/**
 * 名称ごとの出現数を集計する.
 * @param {Object} stat 集計値
 * @param {String} name 集計キー ... 配列内の個々の値が入る
 */
const gatherNames = (stat, name) => {
  if (_.isUndefined(stat[name])) {
    stat[name] = { name: name, count: 0 };
  }
  stat[name].count++;
  return stat;
};

function summarizeLabels(cards) {
  const labelResults = _.chain(cards)
    .flatMap(_.property("labels")) // ... (1)
    .map(label => label.name)
    .reduce(gatherNames, {}) // ... (2)/(3)
    .values()
    .value(); // ... これを呼び出して初めて上記の関数チェーンが実行される
  return labelResults;
}

以前よりもコードが美しくなった気がします! 改善前のコードで分かりづらかった2次元配列の展開は _.flatMap() で一発で書けます。Lodashのメソッドもmap, reduceといった比較的聞き覚えのある名称なので理解しやすい。for文回さずシンプルに書けるところもポイント高いですね。 一番読みやすくなっているポイントは、処理を関数を組み合わせて関数チェーンで表現できていることでしょうか。

Lodashくん、、、君はすごいライブラリだったのか。。。色んなライブラリで多用される理由も納得です。