Ponz Dev Log

開発のことから合間に読んだ本の感想まで、マイルドに書きます

AWSによるサーバーレスアーキテクチャ 読了後感想

サーバーレスアーキテクチャのまとまった書籍ないかなと探して見つけた一冊を読んでみたので、感想を含めてメモ書きです。

AWSによるサーバーレスアーキテクチャ

AWSによるサーバーレスアーキテクチャ

TL;DR

Youtubeクローンのサービス作成を通して、サーバーレスアーキテクチャでサービスを作るためのアーキテクチャ、考え方、コンポーネントの使い方と実装・テストといった設計からサービスの公開までの一連の要素を学べます。

特にアーキテクチャの原則や各章で説明されているコンポーネントの組み合わせ・実装はAWSに限らず汎用的に適用できるものだと手を動かしながら感じました。 それと個人的にはちゃんとテストを書こうって書いてあったのは好印象。

また、サーバーレスのコンポーネントとしてAWSのサービスを多用していますが、類似したサービスは他のクラウドベンダーやOSSにも存在するので置き換えて作ることもできそうです。

以下、この書籍からピックアップして所感を書き連ねます。

サーバーレスアーキテクチャの原則

この書籍で一貫して適用される原則です。具体的には以下の5つになります。

  1. オンデマンドでコードを実行するために、サーバーを自分で立てるのではなくコンピューティングサービスを使う
  2. 目的が1つでステートレスな関数を作る
  3. プッシュベースのイベント駆動パイプラインを設計する
  4. より厚く強力なフロントエンドを作る
  5. サードパーティサービスの活用する

1つのアプリケーションサーバーで実現していたことをサーバーレスアーキテクチャで実装するにはどれも重要な原則です。

(2)を突き詰めると(4)や(5)も自然と実施するようになるので、それぞれ独立した原則ではなく相互関係があるように思えます。

(3)のイベント駆動パイプラインはAPI Gatewayから接続されるようなサービスではなく、非同期サービス側だけで使うアーキテクチャ・実装に見えます。ですが、背後にこのようなアーキテクチャのパイプラインがあることを意識すると、同期的に処理するAPIインターフェイスや、APIとパイプラインの接続部分の実装が変わります。

サーバーレスのコンポーネント

この書籍で使用したコンポーネントは以下の通りです。

  • AWS Lambda ... Computing Service
  • Amazon S3 ... Storage and Event
  • Auth0 ... OAuth style authentication
  • Amazon SNS ... Notification
  • Amazon SES ... Email
  • Amazon CloudWatch ... Monitoring
  • Amazon CloudTrail ... Auditing
  • Amazon API Gateway ... Relate API with Lambda
  • Firebase Realtime Database ... Realtime datastore
  • AWS Step Functions ... State machine

こうやって使ったコンポーネントを見ると、サービスをたくさん使いました。 下の参考記事みたいにサーバーレスのサービスのアーキテクチャServiceful Serverlessと表現するのは的を得ていますね。

--> サーバーレスによる大変革

標準的だったり汎用的な機能はクラウドベンダーもしくはサードパーティーのサービスを使い、カスタムコードは単一の目的に特化した最低限だけ書く。そしてそのカスタムコードはサービスを繋ぐ(=グルーコードにする)。カスタムコードをいかに書かずに実現させるかを考えるのはゴリゴリコードを書くのとはまた違った楽しさがあって良いです。

Good Point

  • 実際にコードを書きながら学べる

    • これは言わずもがなですが、ただ読んでいるだけではなく手を動かしながらの方が理解できますね。
  • 自然とクラウドネイティブなサービスを作れる

    • まるでCNCFの定義のようなクラウドネイティブなアーキテクチャに結果的になっています。「管理しやすい」については疑問が残りますが、他の要素については概ね満たしていますね。
    • スケーラブルなサービスである
      • Lambdaやイベント駆動パイプラインを使用することで、特定部分がボトルネックにならない!
    • コンポーネントが互いに疎結合となっている
    • 耐障害性がある
      • 特定の部分が失敗したり応答不能になっても、他のサービスには影響しない。
    • 可観測性が高い
      • CloudWatch Metrics/LogsやCloudTrailを使うことでそれぞれのコンポーネントの状態を把握できるようになっている。
  • 上記の原則に沿って、サーバーを意識しない/自分で管理せずサービスをどのように作るのか明確であった。

    • 自分でEC2やベアメタルサーバーを立てずにLambdaを使う、フロントエンドを厚くする、サードパーティーサービスを活用して自分でコードを書かずに機能を実現できるといった具合です。
  • エラーハンドリングについてアーキテクチャ的解答があった。

    • 単純に実装としてDLQを使いましょうで済ませてないです。なんでDLQを使わないといけないんだっけ?の理由が自分の中ではフワッとした理解に留まっていたので、この解答には膝を打った。
      • (Why) マイクロサービスになるにつれて全てのサービスのトランザクションをまとめ上げるプロセスが存在しなくなるため、一連の処理の一部が失敗した場合のデータの整合性担保が難しくなる。
      • (What) アーキテクチャとしては、エラー通知のトピックとエラー処理サービスを立ててロールバックを実行する。
      • (How) 通知の具体的なコンポーネントとして、SNS/SQSを使ったDLQを使用する。

Bad Point

上記のように良書ではあるものの、残念 or ここは改善できそうというポイントもありました。

  • CI/CDパイプラインを作らず全てコマンド手動実行

    • package.jsonにパッケージングやデプロイ用のコマンドを書いてまとめてはいるものの、いちいち自分で実行しないといけないのは効率が悪いです。
    • 画像処理のためにffmpegを入れるLambdaのデプロイパッケージのZIPファイルは40MB近くあるため、ネットワークによってはupdate-function-codeが失敗します。これがすごいストレスです。S3にアップロードしてからコマンドを叩けばデプロイ可能ですが、AWS SAMやServerless Frameworkを使ったCI/CDパイプラインを作ればストレスフリーかつ時短になったはず。
    • 折角クラウドサービスを使ってるんだから、手作業でボトルネックができちゃうのは勿体無い点です。
  • 使用するサービスが古い

    • 面白みに欠けるという意味ではなく、すでにdeprecatedになっている機能や代替サービスの使用を前提としていたことがあって何度か心が折れそうでした笑
    • Auth0は認証用のウィジェットとして組み込みのUIではなく、Auth0がホストするUI(Universal UI)の使用が推奨されていました。こっちの方がライブラリ管理が少なくてベターに感じます。
    • Firebase Realtime Databaseの代わりに(beta版ではありますが)Firestoreを利用が最近は多いようです。
    • 認証は委譲トークン(Delegation Token)を使用する前提で記載されていましたが、この手法による認証方法は廃止されていました。
  • 長時間処理するバッチコンピューティングの利用や、既存のサーバーを使ったレガシーアプリケーションとの共存・マイグレーションについて言及がなかった。

    • 実際には一からサーバーレスで作ることは(特にエンタープライズ系のサービスだと)少ない訳ですし、サーバーありのサービスとの共存や割り切り方については何かしら意見が欲しかったところです。
    • 例えばAWS BatchやEMRを使うケース、ECRを併用するケースってどう切り分けているのかしら...??とか。

まとめ

上記のようにGood/Badポイントは双方あるもののが、総じてサーバーレスアーキテクチャと実装を理解し、イメージするには良書でした。 単にAWS Lambdaを使おうに止まらず、サービスを作るために必要なコンポーネントやそれぞれの役割、原則について非常によくまとまっていたので、これからサーバーレスに挑む人には是非おすすめしたい書籍です。

せっかくなので、IBM Cloudや最近認定を取ろうと目論んでいるGCP、もしくはOSSをふんだんに使ったケースでも同じ or さらに発展させられないか試してみようと思います。


以上。

ブラウザからREST APIでAmazon S3にファイルをアップロードするときに気をつけること

書籍『AWSによるサーバーレスアーキテクチャ』(翔泳社)を読み進めているときにS3のファイルアップロード関連でつまづいたところが多々あったので、気をつけるべきところとして手順と一緒にまとめます。

AWSに関わる動的な処理を全てJavaScriptで実装することを選択した場合、AWS Lambda上ではAWS SDK for JavaScript一択ではありますが、ブラウザの場合は生のJavaScriptで記述することも可能です。

今回はブラウザ側で実行されるS3関連のコードをSDKを使わずREST APIで扱った時のメモです。 (書籍に従って生JSでゴリゴリ書きましたけど、潔くSDKを使えばこういったところで躓かなかったのではと思わずにはいられない。)

www.shoeisha.co.jp

やること

  • 署名付きURLを発行する。
  • バケットのアクセス権限を設定する。
  • REST API経由でリクエストを投げる。

署名付きURLを発行する

AWS S3 署名バージョン4でリクエストする

  • ファイル名(オブジェクトのキー)、バケット名、有効期限を使って署名付きのURLを発行します。このとき、署名バージョン4でリクエストします。
    • V4以外は受け付けません。明示的に指定した方がいいかも。
    • ここはLambda + API Gatewayで実装すると扱いやすい。

AWS regions created before January 30, 2014 will continue to support the previous protocol, Signature Version 2. Any new regions after January 30, 2014 will support only Signature Version 4 and therefore all requests to those regions must be made with Signature Version 4

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sig-v4-authenticating-requests.html

以下、コード例。

"use strict";

const AWS = require("aws-sdk");
const S3 = new AWS.S3({
    signatureVersion: 'v4'
});
const crypto = require("crypto");

function getSignedUrl(fileName) {
    const prefix = crypto.randomBytes(20).toString("hex");
    const params = {
        Bucket: process.env.UPLOAD_BUCKET,
        Key: prefix + "/" + fileName,
        Expires: 600
    };
    return S3.getSignedUrl("putObject", params);
}

module.exports = {
    getSignedUrl
};
  • オペレーション名を一緒に付与すること

    • オペレーションを指定するときは、putObjectとキャメルケースで指定すること。PutObjectと先頭大文字で指定するとエラーで弾かれる。
    • 公式ドキュメントには上記は明記されていないが、Sampleにはちゃっかりキャメルケースで指定されている。
  • ここで返却される署名付きURLは仮想ホスト形式で返されます。

    • http://{bucket-name}.s3.amazonaws.com
    • http://{bucket-name}.s3-aws-region.amazonaws.com

バケットのアクセス権限を設定する

バケットポリシー

まずはパブリックアクセス権限が付いていたら外します。 今回は署名付きURLでダウンロード/アップロードを行うため、リクエストの度にリクエスト者に権限が付与される仕組みにするためです。

バケットを選択 -> アクセス権限 -> バケットポリシーを開いてドキュメントを削除する。バケット一覧で該当のバケットに"非公開"と記載されていればOK。

常に最小限の権限を付与することが推奨されているため、不用意にパブリックアクセスを許可することは回避した方が良いです。

CORS設定

AWSとは別ドメインからアクセスすることになるので、どこから/どんなオペレーションが許可されるかバケットに設定する必要があります。 バケットを選択 -> アクセス権限 -> CORSの設定から以下のように設定を追加します。これだけ。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

あと地味なTipsとしては、1行目の<?xml...の記載は設定のエディタに書かなくても勝手に追加されます。

REST API経由でリクエストを投げる

PUTリクエストで投げる。

オブジェクトの新規作成をするんだからPOSTリクエストじゃね?と直感的に思いつきますが、PUTリクエスで投げます。POSTメソッドでリクエストを投げてもAccess Deniedと403が寂しく返ってくるだけです。 思い返してみれば、オペレーション名はPutObjectだもんね。

おそらく同一キーのオブジェクトに対しては上書きもできることから、POSTメソッドではなくPUTメソッドのリクエストにしているのではと推測します。 --> REST API Reference: PUT Object

Content-Typeはファイルのものを指定する

画像や動画をHTMLから送信する場合の定石だとContent-Type: multipart/form-dataでデータをバックエンドに送信しますが、S3へのアップロードに関しては単一ファイルの場合はmultipartリクエストを行うのは間違いです。 ファイル本体のContent-Typeを指定しましょう。

例えば、MP4の動画をアップロードする場合は、Content-Type: video/mp4といった具合です。

"use strict";

/**
 * ファイルをS3バケットにアップロードする。
 * @param {string} url アップロード先S3バケットの署名付きURL
 */
function upload(url) {
    // MP4の動画を添付したと想定。
    const uploadBtn = document.getElementById("uploadButton");
    const file = uploadButton.files[0];

    // S3バケットにアップロード実行。
    $.ajax({
        url: url,
        type: "PUT",
        data: file,
        processData: false,
        contentType: "video/mp4"
    })
    .done(response => {
        console.log(response);
        alert("Upload Finished!");
    })
    .fail(err => {
        console.log(err);
        alert("Failed to upload...");
    });
}

実はこれもちゃっかり公式ドキュメントに記載されています。日本語版にはないですけどね。 以下のExample2にJPEG画像の例が参考になります。 https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/RESTObjectPUT.html?shortFooter=true#RESTObjectPUT-responses-examples


ブラウザに含めるJSファイルのサイズを気にしてSDKを入れられない場合などに参考にしていただければと思います。

以上。

技術書店6 Knative本 読了後感想

実はタイトルの技術書展6には実は行けなかったのですが、 気になっていた本があったので思わずBoothでポチって読んで、手を動かした感想です。

感想を書く対象の本はこちらになります。 『Knativeの歩き方 KubernetesからServerlessを訪ねて』

toshi0607.booth.pm

感想

まず50ページ弱の文字通り薄い本なので、かなり読みやすいです。 構成は最も理解しやすいであろう順序で書かれているので、頭から読んで苦になりません。技術書として最高です。

特に、Serving --> Build --> Build + Serving --> Eventing + Servingの流れは素晴らしい。Serving + Buildのあたりなんか、もう明日には使えそう。

また、プラットフォーム依存の説明が少ないので、他のプラットフォームでも応用させやすい本ですね。

今やk8sはどのプラットフォームもサービスとして提供してるから当然ではあるのだが、GKE特有の説明もさらっと終わらせるので良い。公式にインストール方法が書かれているので、IBM CloudのIKS、AWSのEKS、AzureのAKS、Openshiftでも同様に動かせるはずです。

さらに言うと、説明が丁寧!読者を置いてけぼりにせずスラスラ読めます。

KnativeはCRD(Custom Resource Definition)を使って独自のKubernetesオブジェクトを作成することで様々な機能を拡張してますが、各CRDごとの役割を省かず丁寧に説明されています。独自の用語ばかりだとついていけなくて積み本行きになりがちですが、全くそんなことなかった。

Knativeについて思うこと

正直な話、コンテナ周りって扱うのつらさを感じるんですよね。便利なのは使ってて分かりますけど。 コンテナ単品だとランタイムやアプリをラップしているだけだし、Kubernetesでコンテナオーケストレーションすると今度はクラスタの管理やYAMLの記述に圧殺される。ネットワークやサービスメッシュは別プロダクトのIstioやLinkerdを使わないといけない。

こんなもどかしさを抱えているときに、「開発に集中する」ことを実現できるKnativeはかなり魅力的なプロダクトですね。 コンテナをビルドできる、そのままデプロイしてサービスを公開できる、イベントドリブンな構成も1つのプラットフォームでできてしまうのは驚き。 HTTPベースだけじゃなく、イベントを扱えるところも今までにない。

ただ、build, deploy, and manage modern serverless workloads と、サーバーレスを自称しているのどういうことだろうと考えこんでしまったのも事実。 なぜなら、今までServerless = FaaSという認識を持っていたから。CNCF Serverless Whitepaper v1.0でもFaaSを重点的に取り上げるくらいですしね。 思うに、文字通りプラットフォーム(インフラ構成やら、クラウドプロバイダーがどれやら、どの言語を使っているやら) = サーバーを意識させないところなんだろうな。もしくはプラットフォームの制約を受けないところ。

「開発に集中する」ことを実現している点では、開発者側から見ると既存のPaaS~FaaSと変わらないです。 CloudFoundryやHerokuで体験したコードを書いてコマンド一発でアプリができるという素晴らしい体験を、コンテナ・Kubernetesでオープンに実現しようというのがKnativeなんじゃないかなとこの本を読みながら考えさせられました。


以上。

Node.jsアプリでもAngularみたいにDIしながらアプリを作る

AngularではDependency Injection(DI)で自分で定義したServiceを各コンポーネントで使えるようにできます。Javaの世界でもSpring Frameworkを筆頭に、インターフェイスに対してどのような実装を紐づけるかをDIで実現しています。

ただ、、、サーバーサイドやCLIでNode.jsのアプリを作るとなると途端にDIをすることって少ないですよね。TypeScriptで型やインターフェイスを手に入れたのにClassをいちいちnewしたりimportするのは辛い...とても辛い!!

このポストではInversifyJSを使ってサーバーサイドのNode.jsのアプリでDIを行います。

概要

実現したいこと

  • Node.jsアプリでDIを使ってInterfaceと実装クラスを疎結合にする。
  • 生のJSではなく、TypeScriptを使ってClassとInterfaceの恩恵を受ける。
  • 題材はCLIアプリケーションとする。
  • InterfaceへのDIだけでなく、実装クラスのDIについてもやってみる。

題材

最近お仕事で自分でやったことを例とします。題材とシチュエーションは以下の通りです。ちょっとした自動化アプリです。

  • Java(Spring Framework)のWebAPIを作っています。
  • 入力チェックのテストコードを全項目網羅して正しく入力チェックエラーを返すことを確認したい。
  • 手で書いて目Grepで過不足がないか確認するのは効率がよくない&何かしら見落とすリスクがあるので自動生成する。
  • この自動生成をCLIインターフェイスとしたアプリケーションとして実装する。

InversifyJS

今回のように、JavaScript/TypeScriptでDIを実現するのに不可欠なライブラリです。

github.com

正体はTypeScriptで作られたIoCコンテナ(=DIコンテナ)のライブラリ。TypeScriptで使うことが推奨されています。

また、最近のTypeScriptの流れなのか、アノテーションを使っています。@Inject@InjectableといったJavaでもお馴染みのやつを使えます。 詳細はGitHubのREADME.mdを読んでいただければと。

実践

完成形

最初に最終形をお見せします。 ファイル・ディレクトリは以下の通りになります。

CLIのエントリーポイントがsrc/generator.tsです。 各ディレクトリはクリーンアーキテクチャもどきで構成しています。各ディレクトリと格納クラスの役割は以下の通りです。

Interfaceを定義する

まずはController、UseCase、RepositoryのInterfaceを定義します。全部書いているとキリがないので一部だけ。

import { CreateScriptRequest } from "./CreateScriptRequest";
import { CreateScriptResponse } from "./CreateScriptResponse";

export interface ICreateScriptUseCase {
  generate(request: CreateScriptRequest): CreateScriptResponse;
}

実装は何も入れていません。どのようにこのgenerate関数が実現されるかは実装クラスに委ねられます。そして使用する側は実装クラスではなく、Interfaceにのみ依存します。

実装クラスを書く

Interfaceを実装します。前述した通り、JavaのInjectアノテーションが出てきますね。 Interfaceに実装クラスを注入する対象は @Inject() 、他のクラスで注入させたい対象は @Injectable() を使用すればあとはただのアプリです。

import { ICreateScriptUseCase } from "./UseCaseInterface";
import { CreateScriptRequest } from "./CreateScriptRequest";
import { CreateScriptResponse, CreateScriptResponseDetail } from "./CreateScriptResponse";
import { CaseScriptListBlg } from "../business/CaseScriptListBlg";
import { CaseScriptBlg } from "../business/CaseScriptBlg";
import { injectable, inject } from "inversify";
import { TYPE } from "../common/Types";

/**
 * スクリプト生成ユースケースの実装クラス
 */
@injectable()
export class CreateScriptInteractor implements ICreateScriptUseCase {
  private readonly _listBlg: CaseScriptListBlg;
  private readonly _scriptBlg: CaseScriptBlg;

  constructor(
    @inject(TYPE.CaseScriptListBlg) listBlg: CaseScriptListBlg,
    @inject(TYPE.CaseScriptBlg) scriptBlg: CaseScriptBlg
  ) {
    this._listBlg = listBlg;
    this._scriptBlg = scriptBlg;
  }

  /**
   * スクリプト生成を実行する.
   */
  generate(request: CreateScriptRequest): CreateScriptResponse {
    // テストケースのリストを生成
    const caseList = this._listBlg.createCaseList(request);
    // ケース名を元にテストケーススクリプトを生成
    this._scriptBlg.generate(request, caseList);

    // レスポンス生成
    return this._createResponse(request.getOutputFilePath(), caseList.length);
  }

  private _createResponse(path: string, num: number): CreateScriptResponse {
    let detail = new CreateScriptResponseDetail();
    detail.setCaseName(path);
    detail.setCaseNum(num);
    return new CreateScriptResponse("OK.", [detail]);
  }
}

実装クラスと紐づける

最後に、inversify.config.ts にInterfaceと実装クラスの紐付けを定義します。ここで紐付けはこのクラスでしか行いません。 to()で実装クラスを紐づけるのですが、toSelf()なる関数があるらしい。(使い方は分からない...)

import "reflect-metadata";
import { Container } from "inversify";
import { TYPE } from "./Types";
import { IBaseController } from "../controller/IBaseController";
import { CreateScriptController } from "../controller/CreateScriptController";
import { ICreateScriptUseCase } from "../usecase/UseCaseInterface";
import { CreateScriptInteractor } from "../usecase/CreateScriptInteractor";
import { CaseScriptBlg } from "../business/CaseScriptBlg";
import { CaseScriptListBlg } from "../business/CaseScriptListBlg";
import { ICaseListRepository, IScriptRepository } from "../repository/RepositoryInterface";
import { CaseListRepository } from "../repository/CaseListRepository";
import { ScriptRepository } from "../repository/ScriptRepository";

const container = new Container();

// Controller
container
  .bind<IBaseController>(TYPE.CreateScriptCtrl)
  .to(CreateScriptController);

// UseCase
container
  .bind<ICreateScriptUseCase>(TYPE.CreateScriptUseCase)
  .to(CreateScriptInteractor);

// Business Logic
container.bind<CaseScriptListBlg>(TYPE.CaseScriptListBlg).to(CaseScriptListBlg);
container.bind<CaseScriptBlg>(TYPE.CaseScriptBlg).to(CaseScriptBlg);

// Repository
container.bind<ICaseListRepository>(TYPE.CaseListRepo).to(CaseListRepository);
container.bind<IScriptRepository>(TYPE.ScriptRepo).to(ScriptRepository);

export { container };

そして使いたいクラスをDIコンテナから取り出せば完成!

import { container } from "./common/inversify.config";
import { IBaseController } from "./controller/IBaseController";
import { TYPE } from "./common/Types";
const controller = container.get<IBaseController>(TYPE.CreateScriptCtrl); // <-- Here!!

まとめ

InversifyJSを使うと、Node.jsのアプリでもDIやるのは結構簡単ですね。 今回はCLIアプリで一からコードを書きましたが、Webアプリでフレームワークを使うときも同じように書けるはず。

Serverless FrameworkでKotlin & LambdaのサーバーレスAPIを作る

朝活.Kotlinで作ったServerless Framework × Kotlin × LambdaのサーバーレスAPIまとめです。

justincase.connpass.com

serverless.com

概要

  • 題材

    • 松屋のメニューの写真をひたすら貼るSlack Bot. (ちょっとしたジョークアプリです)
  • 上記のSlackBotのバックエンドをLambda関数で実装する.

    • API管理: API Gateway
    • ロジック: AWS Lambda <--ここをKotlinで書いた
  • CI/CDファーストを目標に、なるべくも手作業を減らしてビルド&デプロイを簡単にする.

やったこと

まずアプリケーションを作成するときに悩むのが、ボイラープレートコード(フレームワーク等で必要になる典型的なコード)の扱い。 ビジネスロジックを書くことに集中できるのがサーバーレスアプリケーションの強みの1つなので、LambdaとAPI Gatewayの接続部分や、エントリーポイントのHandlerクラスはなるべく書きたくないところ。

実はServerless Frameworkのテンプレートから雛形は自動生成可能です(プラットフォームによってはない)。 sls create --helpを打って調べる限り, 使えるテンプレートは3つありますね。KotlinでJS使うテンプレートなんてあるのか...

それでは雛形をコマンドラインから作成。事前にnpm i -g serverlessでserverlessコマンドは入れておきましょう。今回は"aws-kotlin-jvm-maven"を使います。

serverless create \
    --template aws-kotlin-jvm-maven \
    --path matsuya-finder \
    --name matsuya-finder

5秒くらいでテンプレートは作成されます。指定した--path以下は以下のようなディレクトリ構成になっています。

.
 |- pom.xml
 |- serverless.yml
 |- src
    |- main
       |- kotlin
          |- com
             |- serverless
                |- ApiGatewayResponse.kt
                |- Handler.kt
                |- Response.kt
  • ApiGatewayResponse.kt ... Lambda --> API Gatewayにレスポンスを返す時のデータの格納クラス。弄らずにそのまま使う
  • Handler.kt ... アプリケーションのエントリーポイント。ビジネスロジックはここにInjectする。
  • Response.kt ... アプリケーションのレスポンス。data classになっている。
  • serverless.yml ... Serverless Frameworkに管理されたアプリケーションのデプロイ定義。裏ではCloudFormationを使っている。

あとは業務ロジックのコードを好きに書きます!

最終的なコードは以下の図のような構成になりました。 アプリのコードが重くなければ、KtorやJavaliなどのWeb Application Frameworkを使うよりもスッキリしてますね!

f:id:accelerk:20190323235930p:plain
ディレクトリ構造 最終形

また、serverless.ymlは以下の構成になっています。このファイルが置かれている場所で sls deploy を発行すればデプロイ完了です。

service: chom-matsuya-finder
  
# Base Definition
provider:
  name: aws
  runtime: java8
  stage: dev
  region: ap-northeast-1

# Packaging information
package:
  artifact: target/matsuya-finder-dev.jar

# Individual Function Definitions
functions:
  main:
    handler: net.ponzmild.Handler
    name: matsuya-finder
    description: Fetch today's Matsuya menu.
    events:
      - http:
          method: post
          path: matsuya

Tips

  • Serverless Frameworkのコマンドを打つときは, 権限のあるプロファイルを使おう

    • IAMロールをつけたのになぜかUnauthorizedとなってしまうことありますが、だいたいはIAMロールをつけたユーザーでCLIを叩いてないことが原因です。
    • (使用例) sls deploy --aws-profile your-serverless-agent
  • そのままデプロイすると統合リクエストのタイプが"Lambda Proxy"になる

    • このタイプはリクエストのコンテントタイプを細かく指定しづらいです。
    • また、雛形として自動生成されるApiGatewayResponse.ktはLambda Proxyタイプ用のレスポンス定義です!これ以外のタイプは別途レスポンスのクラスを用意しましょう。
    • serverless.ymlintegration:lambdaを指定するか、Handlerクラス内でパースのどちらかで解決しなければなりません。
    • 今回は後者にしています(以下のコード)
class Handler : RequestHandler<Map<String, Any>, ApiGatewayResponse> {
    override fun handleRequest(input: Map<String, Any>, context: Context): ApiGatewayResponse {
        // API GatewayのイベントのJSONからリクエストボディのみ取り出す
        val inputJsonString = input.get("body") as String

        // 全て文字列なので, Mapに詰め替える
        var inputBodyMap: HashMap<String, String> = hashMapOf()
        val kvStrings = inputJsonString.split("&")
        kvStrings.forEach {
            val kv = it.split("=")
            inputBodyMap.put(kv[0], kv[1]) // 詰め替え
        }

        // And more...
    }
}

Kotlinも結構簡単にサーバーレスなAPIを作れるんですね。他にもS3のイベントやCloudWatch Eventsにも対応できるのでできる幅も広そう。 また、Androidユーザーはフロントエンドと同じ言語を使えるからいいですね。iOSユーザーのためにSwiftでもできるのかな?

以上。

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だった。隣のセッションとはパーティション一枚でしか区切られてなかったから、結構声が隣から漏れてきてたし。

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


以上。