backcheck を TypeScript に移行するまでの流れ

はじめに

皆さん、こんにちは。株式会社ROXX、backcheck開発チームの山口と申します。 backcheckフロントエンドのTypeScript移行がある程度軌道にのってきたので、ここまでの過程を文書化することにしました。

この記事ではTSの導入までの過程についてかいつまんでお話ししようと思います。

やっていること

Nuxt2系(JavaScript)で書かれたbackcheckのフロントエンドをTypeScriptへマイグレーションしています(2021/11時点で現在進行中)。 IEのサポートの終了+Nuxt3がStableになるタイミングで、Nuxt3+Composition APIへの乗り換えを予定していることから、vue-class-componentやvue-property-decoratorは導入せず、Options APIを使用したまま、TypeScriptのみを導入することとしました。

TypeScript 移行で目指すゴール

「完全 TypeScript 化ではなく、最速で8割 TypeScript 化を目指す」

これをスローガンとして、まずは、普段の開発作業で触る箇所に対して TypeScript でかける状態をなるべく早く用意することをゴールとして考えました。

なぜ TypeScript に書き換えるのか

主には以下を目的としてTypeScriptを導入することにしました。

  • 静的解析により、型安全に開発できる
    • 型がドキュメントがわりになる
    • 早い段階でエラーに気づける

TS移行完了までに相応のコストがかかりますが、移行が完了していなくても部分的に恩恵を受けられることや、長期的にみてコードの品質や開発速度が向上することが移行への後押しになりました。

メンバーのTSキャッチアップ

弊チームでは、実務でTSを書いたことがあるメンバーが12人中7人でした。 このままだと残った5人がフロントのコードが書けなくなる + 実務で使ったことのあるメンバーでも理解度がまちまちであったため、慣れるまでは移行作業を4人参加のモブプロで進めることでキャッチアップすることとしました。

また、モブプロ以外の施策として、TS移行のキックオフ前に、TS未経験者に向けたTypeScriptワークショップを行いました。これにより、モブプロ開始のタイミングで最低限の基本知識は全員が聞いたことがある状態とすることができました。 (TypeScript説明会ではTypeScript Deep Diveをベースに、概要や基本的な機能などについて解説を行いました)

TS環境構築

TypeScript、eslintの設定

キックオフの時点から"strict": trueの状態としています。 vueファイルでmixinsを呼び出している箇所など、型の適用が難しい箇所については、将来的にリファクタリングすることとして、マイグレーションのタイミングではts-ignoreすることでエラーを回避するようにしました。

また、TSにマイグレーションする上で、リファクタリングしたくなるコードは、影響範囲が大きくなってしまうので別でタスクを用意し、このプロジェクトではなるべくリファクタリングをしないように決めました。

eslintの設定は、TSマイグレーション未対応のjsやvueファイルでもエラーが出てしまうような項目については、overrideして設定をOFFにしています。

サンプル実装の作成

移行作業の着手前に、1ファイルだけTS化したファイルを用意することで、他ファイルを移行する際の判例としました。実装の例を用意したことで、実装イメージがチームの共通認識としてもてたので、よかったと思います。

TSに移行したファイルのリグレッションテスト

該当画面の挙動にデグレがないかを確認するための、ブラックボックスなテストをQAとして行うこととしました。 その他に、utilsの関数や共通コンポーネントなど、全体影響があるものに関しては、正常系フローの動作確認を行うテストを別途実施することで、デグレが起きていないことを確認することにしました。

移行計画の作成

内容としては開発フローへの乗せ方、作業の進め方の2つを事前に決めました。

開発フローへの乗せ方

backcheck のフロントエンドは JS, vue ファイルあわせて約550ファイル・5万行のコードがあります。 これを80%TSに移行するための超概算で以下の数字がでました。

たとえば... 1スプリントあたりのベロシティの20%を移行作業にわりあてたとすると → 33週かかる 1年は約52週 つまり... 完了までに7ヶ月くらいかかります。

なかなかかかりますね...

TSの移行作業の割合を、1スプリントあたりのベロシティの20%以下に落としてしまうと完了までに年単位でかかってしまうため、プランニングする量としては1スプリントあたりのベロシティの20%を固定枠で設けて、メインのストーリータスクと並行して進めていくというやり方にしました。

移行作業の進め方

当初の計画では、こちらのポッドキャストで説明していた進め方を参考に、影響範囲の少ないところから着手していく計画でした。

api→utils→middleware→vueファイルの順に、細かくマイグレーションしていくことで、依存ファイルの多いvueファイルに着手するタイミングには、依存ファイルが全てTSに置き換わっているイメージです。

しかし、普段の開発の中で触る箇所はある程度絞られています。そのため、TypeScriptマイグレーション専任の担当者を設けずに進めている弊チームでは、影響範囲の少ない箇所からマイグレーションをしても日頃の開発フローの中で恩恵を受けられるまでに相応の時間がかかることに気がつきました。

そのため、普段の開発で触る頻度の高い箇所で、よりはやく恩恵をうけられるように、画面(pagesディレクトリ)単位でチケットを立て、画面に依存しているファイルは全てそのチケットの中でマイグレーションする方針に変更しました。 また、constantsで定義していた定数に関しては事前に一括でTS化を行ってしまいました。

おわりに

実際に移行作業をスタートすると、キックオフ時点に考慮が漏れていて後から決定した内容などもあったりしました。導入準備だけでも3ヶ月ほどかかったので、TS移行の計画を立てる場合はある程度長い目で見ながら進めるのがいいかと思います。

その他、ここはどう進めたの?この設定はどうした?こうした方がよさそう。などご意見、ご質問がありましたらぜひお声がけください。

また、現時点でチーム全員がTSでの実装イメージが持てている状態までTSのキャッチアップが進んだので、今後はモブプロをやめて移行作業の速度アップを考えています。 その辺についてもお時間のある時に記事にしようと思うのでお楽しみに。

MinIO を使ってローカルでの開発環境の外部ストレージを Amazon S3 からローカルのコンテナへ置き換える

この記事は個人ブログと同じ内容です

www.ritolab.com


ローカルでのアプリケーション開発は、できるだけ外部のサービスに依存させたくないものです。

API が提供されている外部サービスであればモックすれば良いですし、ストレージに関しても、開発時はローカルにファイルを設置するようにして、確認環境以降は外部ストレージに置くようにする事は可能です。

一方で、AWS SDK を使ってストレージ操作をしてるとそのソースコードがきちんと動くものなのか、動作確認がローカルで出来ない状態にはなります。(ローカルでのファイル操作には AWS SDK を用いないため)

今回はそれらを払拭するために、MinIO を使って、ローカルでの開発環境の外部ストレージを S3 から MinIO に置き換えてみます。

MinIO

MinIO は、Amazon S3 互換のオブジェクトストレージサーバーです。

min.io

MinIO は S3 と互換性を持つので、AWS SDK を通じてアクセス・操作が可能です。

そして MinIO には、Docker Image が公式から提供されています。

hub.docker.com

つまり、ローカル環境の外部ストレージを「S3(インターネット上のサービス)」から「MinIO のコンテナ」に置き換えることができれば、インターネットへの通信がなくせて(外部サービスに依存せず)、閉じた開発が可能になります。

開発環境

MinIO を導入する環境として、docker compose で作成されたローカルでのコンテナ環境を想定します。

docker-compose.yml

MinIO のコンテナを定義します。

minio:
    image: 'minio/minio'
    container_name: minio
    environment:
        MINIO_ROOT_USER: minioadminuser
        MINIO_ROOT_PASSWORD: minioadminpassword
    entrypoint: bash
    command: -c "/opt/bin/minio server /export --address :9999 --console-address :9001"
    volumes:
        - ./docker/minio/data:/export
    ports:
        - '9000:9999'
        - '9001:9001'

command で MinIO の起動を行なっていますが、ポイントが 2 点あります。

  • --address :9999 で、MinIO の API のポートを指定しています。API のデフォルトポートは 9000 ですが、ポート番号を変更する必要がなければここを記載する必要はありません。
  • --console-address :9001 でコンソールの ポートを指定しています。コンソール(GUI)の IP に関しては起動時に自動的に決定されます。docker compose で動かす場合はコンソールの IP をマッピングしてあげる必要があるため IP を指定して固定にするようにしています。

volumes では MinIo ストレージの永続化を行なっているのですが、この場合だと data ディレクトリ配下に切ったディレクトリがそのままバケットや階層になるので永続化しておくとかなり使いやすかったです。

イメージはこんな感じ。(ホスト側に docker/minio/data というコンテナ用のディレクトリを作ってその配下をつなげた場合)

docker
└─ minio
    └─ data
        ├─ my-bucket-1
        │   └─ sub-1
        │       └─ sample.png
        ├─ my-bucket-2
        │   └─ sample.png
        └── my-bucket-3

my-bucket-(n) はディレクトリですが、MinIO のコンソールを開くとそのままバケットとして認識されます。(起動後に追加しても認識されるのでバケットや階層の追加はかなり簡単)

ports では API と コンソール(GUI)のポートをマッピングしています。

これらの設定で起動させると、MinIO のストレージが動作します。

コンソールへは、localhost:9001 でアクセスします。

f:id:ro9rito:20211110083724p:plainf:id:ro9rito:20211110083721p:plainf:id:ro9rito:20211110083719p:plain

Laravel Flysystem での設定

環境は用意できましたが、Laravel の Flysystem を用いてストレージ操作を行なっている場合の設定のポイントも残します。

確認すべきは 3 点です。設定ファイルに urlendpoint そして use_path_style_endpoint の設定項目があるかを確認して、なければ追加します。

config/filesystems.php

's3' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION'),
    'bucket' => env('AWS_BUCKET'),
    'url' => env('AWS_URL'), // <- これがあるか確認
    'endpoint' => env('AWS_ENDPOINT'), // <- これがあるか確認
    'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), // <- これがあるか確認
],

最近のバージョンならありますが、バージョンアップしながら使い続けている場合だとこの辺のプロパティは手動で追加しない限りは付いていない場合もあるので、もしなければつけます。

あとは、ローカルの .env の項目です。

.env

AWS_ACCESS_KEY_ID=minioadminuser
AWS_SECRET_ACCESS_KEY=minioadminpassword
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=YOUR_BUCKET_NAME 
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_ENDPOINT=http://minio:9999
AWS_URL=http://localhost:9000
  • AWS_USE_PATH_STYLE_ENDPOINT は ture で設定します。
  • AWS_ENDPOINT は、同一ネットワーク内からの MinIO コンテナへのアクセス URL を指定します。(例えばバックエンドからの取得はこっち)
  • AWS_URL は、外部からリソースとして参照する(例えばブラウザからの参照)場合の URL を指定します。

これらを設定すると、MinIO ストレージでの操作ができるようになります。

署名付き URL を取得する際のつまづき

実際に操作してみて 1 点躓いたポイントがありました。

temporaryUrl() メソッドを用いて署名付き URL を取得すると、出力される URL は http://minio:9000/〜 となるのですが、この URL は同一ネットワークからのアクセス用の URL なので、例えば HTML で img タグに指定する場合は http://localhost:9000/〜 の URL を使いたい。となります。

そこで Filesystem のソースを掘っていくと、「temporary_url っていう設定値を持っていたらベースの URL を置き換えるよ」という記述があり、これは助かると思って設定してみたのですが、確かにベースの URL は変更されましたが、その URL では SignatureDoesNotMatch となってしまい使えませんでした。

ソースを見てみると、署名を付加されたものに対して単純にベースの URL を置換している(今回でいえば minio:9000 を localhost:9000 に置換)だけなので、署名を再生成しているわけでもなくただの URL 改ざん状態になっているため機能しない状態でした。

最終的には署名付き URL 取得用の disk 設定を config 側に 1 つ追加して事なきを得ましたが、これはこれで冗長な気がしないでもないので、docker compose だからこそ出てくる差分ではあるもののもっと上手く設定で吸収できればやりたいところでした。

まとめ

ローカルにベタで保存でも問題ないとは思いつつ、それでも環境によって切り替える手間が省ける、AWS SDK を用いたファイル操作の動作確認が行えるという点ではローカル環境のストレージを S3 から MinIO に置き換えるのは一定アリかなとは思いました。

取得や保存、削除といった操作に関しては問題なく、既存の実装をほぼ変更する事なく S3 から MinIO への切り替えは行えました。(署名付き URL の取得は確認が必要)

何よりローカルの開発環境がインターネット上にあるサービスに依存しなくなるのはうれしいですね。

Laravelに1行足すだけでOpenAPIを吐き出せるものを作った

この記事は個人ブログと同じ内容です。

kotamat.com

SPA + API構成でLaravelを使っているところも多いかと思いますが、双方の通信のインターフェースを担保するのに皆さんはどうしているでしょうか。

REST APIスキーマ定義としてOpen APIが現在主流かとは思いますが、スキーマ定義言語そのものが最初に作ったら終わりというものではなく、サービスが続く限りメンテナンスをし続けなければならないものであるため、特にサービスロンチ前のプロダクトでは得られる恩恵よりもコストが上回ってしまうために嫌煙されてしまうこともあるかと思います。

一方でOpenAPIを書いておくと、インターフェースから型定義を生成できるため、通常開発以上のコストを払わなくて済むのであれば、ぜひとも導入しておきたいものかと思います。

今回は過去に作った kotamat/laravel-apispec-generator を改修し、1行書くだけでOASjsonファイルを吐き出せるものを作ってみたので紹介します。

https://github.com/kotamat/laravel-apispec-generator

ざっくりとした仕様

LaravelにはPHPUnitを拡張したテストがあり、特に$this->json()ないしはそれの拡張メソッドを使うことによってエンドポイントに対するリクエスト、レスポンスのテストを容易に書くことができます。

今回作ったライブラリは、このテストを通している全エンドポイントに対して、OASを吐き出せるものとなっています。

使い方

個々のテストケースごとにOASを吐き出す

当該パッケージをインストールし

composer require --dev kotamat/laravel-apispec-generator

対象としたいクラスにて、下記のようにuseを差し込むだけです。

class SomeTestCase extends TestCase
{
+    use ApiSpec\ApiSpecOutput\ApiSpecOutput;
    //...
}

その後、テストを実行すると storage/app 配下に各エンドポイントのOAS定義ファイルがjson形式で出力されます。 (例えば http://example.com/api/hoge/fuga へのGETリクエストで200が帰ってくるテストであれば、storage/app/api/hoge/fuga/GET.200.json が書き出されます )

全エンドポイントに集約したOASを吐き出す

吐き出されるものはあくまでそのテストで使用したエンドポイントのOASのみです。OASを使いたいケースは全エンドポイントに対してのOASがほしい事が多いと思うので、今回はartisanコマンドにて、一つのjsonファイルに集約する処理も追加しました。

集約する場合は

php artisan apispec:aggregate

を実行すると、上記で生成したjsonファイルを読み込み storage/app/all.jsonOASを吐き出します。

集約する際は、リクエスURI、HTTPメソッド、レスポンスのステータスコードごとにユニークなものを集計します。もし同じリクエスURI、HTTPメソッド、レスポンスのステータスコードのテストがある場合は、あとから実行されたもので上書きされます。

(今後は anyOf とかを使って複数のリクエストボディ、レスポンスボディで使えるようにしたいとは思っているが、codegen側で不具合があり、適切な型定義が吐き出せなかった。)

生成されたOASはどうする?

jsonの中身をSwaggerEditorにコピペしてみるのもいいし、下記コマンドをLaravelのルートディレクトリで実行してみてTypeScriptの型定義をだしてみるのもいいかなと思います。

target=storage/app/all.json

docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate -i /local/${target} -g typescript-fetch -o /local/dist

まとめとか

今回はLaravelのフィーチャーテストからOASを吐き出すパッケージに関して紹介させてもらいました。 このパッケージを使えば追加メンテ工数ほぼゼロでOASの運用ができるようになるかと思います。興味あればぜひ使ってみてください。

正直8時間くらいで作ったものというのもあり、現状まだ全仕様を網羅しているわけではないため、もしかしたらエラー出ちゃうところもあるかもしれないです。修正したほうがいいところあれば気軽にPR出してもらえると嬉しいです。

エンジニア3年目の僕がスクラムマスターはじめました。

この記事は個人ブログと同じ内容です

エンジニア3年目の僕がスクラムマスターはじめました。


こんにちは YamaguchiSota です。

まず対象読者と、この記事で書くことをご紹介します。

対象読者:

  • これからスクラムマスターになる人・なりたい人
  • スクラムマスターってどんなことやるの?な人

この記事で書くこと:

自己紹介

山口 壮太(YamaguchiSota) 

1993年生まれ。 茨城県 水戸市 在住。 高卒→工場勤務→SES(Coder)→受託開発ベンチャー→株式会社 ROXX(現在 在籍中)

2021年4月から株式会社 ROXX で backcheck というリファレンスチェックサービスのエンジニアとして従事していますが、この度10月から開発エンジニアおよびスクラムマスターを兼任することになりました。

なぜスクラムマスターになったのか

なぜ、僕がスクラムマスターになったのか。

チームの背景として、元々スクラムマスターをやっていた方が PM に転向する上で、スクラムマスターと PM の兼任は難しいということで新スクラムマスターを探すことになりました。

とはいえ、前任のスクラムマスター達はポジションは変えどチームには在籍しているということ。チームとしてアジャイル関連の書籍の輪読会を行ったり学習の機会を持つことで、ある程度メンバーの自己管理が成り立っていること。から、スクラムマスター未経験のメンバーをアサインしてスクラムマスターとして実践を通して育てるくらいの余裕はある状態だったと思います。

そんな中で主に2つの要因から「You, スクラムマスターやっちゃいなよ!」とお声がけいただくことになります。

1つは、最近ジョインしてきたメンバーと古参メンバーの中間くらいのタイミングで僕がこのチームにジョインしたため、どちらの立ち位置の人の気持ちもわかりそうという期待から。

もう1つは、チームの中で行ったドラッガー風エクササイズの期待値からメンバーのサポート的な役割を期待されていたことからです。

ドラッガー風エクササイズとは?

以下、ドラッガー風エクササイズの成果物

ドラッガー風エクササイズの成果物

とはいえなんとなくのイメージでスクラムマスターとはファシリテーターやったり、スクラムの進行が捗るようにする人だ!くらいの認識だったので具体的に何をするか分からずだったので調べて自分なりに解釈した内容を簡単にまとめます。

そもそもスクラムとは?

スプリントと呼ばれる毎回固定された時間軸を繰り返していく中で透明性、検査、適用を徹底することで改善し、チームの安定した生産性を基盤とした生産性の向上を促進していくフレームワークです。

個人的には透明性、検査、適用はそれぞれ以下のように解釈しています。

  • 透明性:メンバーが持っている情報・作業の意思決定などをそれぞれのメンバー間でサイロ化せず、チームの共通認識とするように努めること

  • 検査:スプリント計画、スプリント期間中の日々の進捗や、作ったもの、スプリントのやり方など様々な振り返り

  • 適用:振り返りの内容に応じて出てきた課題に対して解決策の反映

詳細については公式の Scrum Guide 2020 にてご確認ください。

スクラムマスターとは?

スクラムチームの内訳はスクラムマスター1 ⼈、プロダクトオーナー1 ⼈、複数⼈の開発者で構成されます。

ではスクラムマスターってどんなことをする役割なのでしょうか?

スクラムマスターの定義については Scrum Guide 2020 にのっているのでこの記事ではお話しません。

個人的な解釈としては、抽象的ですがチームの模範行動をとる人であり、メンバーが行き詰まった時に解決・回避策の案を提示して背中を押してあげられるリーダーの枠割と捉えています。

それぞれ実践するためには以下のことが必要と考えます。

  • スクラムメンバーとしての模範行動を行うためには、アジャイルを理解する必要がある
  • コーチングを実践するためには、たくさんの知識の引き出しをもつ必要がある

これからスクラムマスターとして何をするのか?

引き継ぎの際に前任のスクラムマスターの方から「スクラムガイドに載っていることだけやればいいんだよ。」とアドバイスをいただきました。

とはいえ、スクラムガイドには具体的な方法は載っていないため、まずは自分なりに以下を意識して日々行動を起こしていけたらと考えています。

やらないこと

  • チームの管理、統率

やること

特にチームの模範行動については意識することで今から実践できることなので今日の業務から徹底できるよう心がけます。

Scrum Master Way を歩き出した新米スクラムマスターとして、より多くチームに貢献できるように精進していきたいと思います。

Nuxt3のuseFetchの型定義を探索してみたら結構面白かった話

この記事は個人ブログと同じ内容です

kotamat.com

先日10/12にNuxt3がpublic betaになりました!🎉 Nuxt2から抜本的に変更されたNuxt3では面白い変更点が多いのですが、今回はuseFetchの挙動に関して探索してみようと思います。

useFetchとは?

Data Fetchingにて紹介されている非同期データ取得APIのうちの一つ。

useFetch(url: string, options?)

というような形で呼び出す、非常にシンプルなAPIではあるのですが、useAsyncData$fetch のラッパーであったり、自動生成されたローカルAPIのレスポンスの型を提供するということで、型定義はかなり複雑な形になっています。

まずは型定義から

useFetchの型定義は下記のようになっています。

export declare function useFetch<ReqT extends string = string, ResT = FetchResult<ReqT>, Transform extends (res: ResT) => any = (res: ResT) => ResT, PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>>(url: ReqT, opts?: UseFetchOptions<ResT, Transform, PickKeys>): import("./asyncData").AsyncData<import("./asyncData").PickFrom<ReturnType<Transform>, PickKeys>>;

型パズル感すごいですね… ここからは一つずつ紐解いていくことにします

Transform剥がし

TransformはuseAsyncDataのオプションの一つである、返り値の変換器であるため、一旦削ってシンプルにしてみます。

export declare function _useFetch<ReqT extends string = string, ResT = FetchResult<ReqT>, PickKeys = KeysOf<ResT>>(url: ReqT, opts?: UseFetchOptions<ResT, (input: ResT)=> ResT, PickKeys>): import("./asyncData").AsyncData<import("./asyncData").PickFrom<ResT, PickKeys>>;

※この状況だとUseFetchOptionsの第3型引数がエラーになってしまいますが、型の抽象度を意図的に変えてしまったのが原因なので一旦無視します

返り値に注目

ここで返り値に注目してみます。

asyncData.d.tsでは下記のように定義されているため

// Tのうち、Kの配列の要素に指定されたkeyの要素だけを抽出して取り出す
export declare type PickFrom<T, K extends Array<string>> = T extends Record<string, any> ? Pick<T, K[number]> : T;
...
// Dataの型を、asyncDataが返してくれる型に変換する
export interface _AsyncData<DataT> {
    data: Ref<DataT>;
    pending: Ref<boolean>;
    refresh: (force?: boolean) => Promise<void>;
    error?: any;
}
export declare type AsyncData<Data> = _AsyncData<Data> & Promise<_AsyncData<Data>>;

返り値は「ResT から PickKeysで指定した要素だけをとりだし、asyncDataが返してくれる形に変換した型」となります。

ResTの探索

では ResT も探索してみます。

ResT = FetchResult<ReqT>;
export declare type Awaited<T> = T extends Promise<infer U> ? U : T;
export declare type FetchResult<ReqT extends string> = Awaited<ReturnType<$Fetch<unknown, ReqT>>>;

となっているため、「ResT$Fetch<unknown, ReqT>の返り値のPromiseを剥がしたもの、もしくはそれ自体」となります。

$Fetchの探索

$Fetchを見てみると下記の様になっています。

export declare interface $Fetch<T = unknown, R extends FetchRequest = FetchRequest> {
  (request: R, opts?: FetchOptions): Promise<TypedInternalResponse<R, T>>
  raw (request: R, opts?: FetchOptions): Promise<FetchResponse<TypedInternalResponse<R, T>>>
}

ResTを当てはめResTの算出に必要なものだけを残すと

export declare interface $Fetch<unknown, ReqT> {
  (request: R, opts?: FetchOptions): Promise<TypedInternalResponse<ReqT, unknown>>
}

となります。

TypedInternalResponseを見ていくと下記になっているので

export declare type TypedInternalResponse<Route, Default> =
  Default extends string | boolean | number | null | void | object
    // Allow user overrides
    ? Default
    : Route extends string
      ? MiddlewareOf<Route> extends never
        // Bail if only types are Error or void (for example, from middleware)
        ? Default
        : MiddlewareOf<Route>
      : Default

今回の型で置き換えると

export declare type TypedInternalResponse<ReqT, unknown> =
    ReqT extends string
      ? MiddlewareOf<ReqT> extends never
        // Bail if only types are Error or void (for example, from middleware)
        ? unknown
        : MiddlewareOf<ReqT>
      : unknown

となります。neverを一旦無視するとMiddlewareOf<ReqT>が返ってくるといえそうです。

残りの型定義は下記となるので

export declare interface InternalApi { }

export declare type ValueOf<C> = C extends Record<any, any> ? C[keyof C] : never

export declare type MatchedRoutes<Route extends string> = ValueOf<{
  // exact match, prefix match or root middleware
  [key in keyof InternalApi]: Route extends key | `${key}/${string}` | '/' ? key : never
}>

export declare type MiddlewareOf<Route extends string> = Exclude<InternalApi[MatchedRoutes<Route>], Error | void>

MiddlewareOf<ReqT>に当てはめて考えてみると

export declare interface InternalApi { }

export declare type ValueOf<C> = C extends Record<any, any> ? C[keyof C] : never

export declare type MatchedRoutes = ValueOf<{
  // exact match, prefix match or root middleware
  [key in keyof InternalApi]: ReqT extends key | `${key}/${string}` | '/' ? key : never
}>

export declare type MiddlewareOf = Exclude<InternalApi[MatchedRoutes<ReqT>], Error | void>

となります。ざっくりいうと、「InternalApiの中にReqTがキーとなる物があればそれのValueを返却する」というふうに解釈できますね。

つまり「ResTInternalApiのキーがReqTに相当するもののValueの型」と推察できます。

InternalApiの自動生成

上記型定義だとInternalApiはデフォルトで {}です。つまりこのままではなんの意味もないものになります。 この型定義をoverwriteしてくれるのがnitroというNuxt3のサーバーエンジンです。

nitroは色々な機能があるのですが、そのうちの一つが /server/ディレクトリに配置した関数の返り値を解釈し、型定義を生成してくれるというものです。

例えば/server/api/count.tsという下記のTSファイルを設置してみます。

let counter = 0;
export default (): { counter: number } => {
  counter++;
  return { counter };
};

すると.nuxt/nitro.d.tsという、下記の内容のファイルが生成されます。

declare module '@nuxt/nitro' {
  interface InternalApi {
    '/api/count': ReturnType<typeof import('../server/api/count').default>
  }
}
export {}

InternalApiが拡張され、/api/countに対する型定義が出現しました。 これにより、「ResTInternalApiの中にあるReqTに相当するもののValueの型」は 「ResTReqT/api/countのとき/server/api/countの返り値」となることができました。

useFetchの返り値

ここでuseFetchを実際に使ってみてこの効果を探ってみます。

const {data} = await useFetch('/api/count')

としてみたとき、dataの型はシンプルに

Ref<Pick<{
    counter: number;
}, "counter">>

となります。 useFetchでは単にエンドポイントを指定しているだけに過ぎないのですが、その返り値がVue3で用いやすい型として抽出されている事がわかります。 \ InternalApiに型定義がなくても使えるような設計になっているため、useFetchの引数に対しての補完が効かないのが難点ではありますが、Nuxtのディレクトリ構造を探索すればファイル名だけである程度予想はできるので、便利に使えそうです。

とりあえず一旦まとめ

useFetchの返り値を探索するだけで結構長くなったので、一旦このへんで終わりにします。 \ useFetchはこれ以外にもuseAsyncData$fetchのオプションもサポートしているので、よかったら探索してみてください。オプションのサポートは上記に比べると大したことはないので、気構えずに見れると思います

Jupyter Notebook で Python を書くためのコンテナ開発環境を作成する

この記事は個人ブログと同じ内容です

www.ritolab.com


Python を使ったデータ分析や視覚化を気軽に試せるように、コンテナを使ってローカル開発環境を構築してみます。

Jupyter Notebook

Jupyter Notebook は、ブラウザ上で Python を記述して実行することができる web ベースの開発環境です。(言語については Python に限らない)

jupyter.org

開発環境

コンテナで環境を作成するので、Docker が入っている前提です。

  • Docker for Mac 4.1.0
    • Docker 20.10.8
    • Compose 1.29.2

また、最終的なディレクトリ・ファイル構成は以下になります。

.
├── Dockerfile
├── config
│   └── matplotlibrc
└─── docker-compose.yml

これから上記 3 つのファイルを作成していきます。

Dockerfile

まずは Dockerfile を作成してイメージを定義します。

Dockerfile

FROM python:3.9.7-buster

RUN apt-get update
RUN apt install -y locales && localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9

# 日本語 font のインストール
RUN apt-get install -y fonts-noto-cjk

# python ライブラリのインストール
RUN python -m pip install pandas
RUN python -m pip install numpy
RUN python -m pip install matplotlib
RUN python -m pip install jupyterlab
RUN python -m pip install notebook
  • Jupyter Notebook を日本語で使いたいので、locales を入れて言語の設定を行なっています。
  • グラフ描画の際に、タイトルや凡例を日本語で表示させたいので、日本語のフォントをインストールしています。
  • 最後に記述されている Python ライブラリのインストールは、必要最低限だけ記述してあります。

なお、私は DebianPython に関しては弱者なのでイメージに buster を選択しています。この辺に詳しい場合は、適宜最適なイメージを指定すると良いと思います。

docker-compose.yml

次に、docker-compose.yml を作成します。

docker-compose.yml

version: '3'
services:
  python3:
    build: .
    container_name: python3
    working_dir: /root/opt
    tty: true
    volumes:
      - ./src:/root/opt
      - ./config:/root/.config/matplotlib
    ports:
      - "8888:8888"

volumes には、以下の 2 つを指定しつなげています。

  • Jupyter Notebook で作成したファイルや、分析で使うデータを設置するための src ディレクト
  • matplotlib の設定ファイルを設置するディレクト

グラフの日本語化

解析結果をチャートに描画する際に、タイトルや凡例などに日本語を使いたい場合はデフォルトのフォントだと日本語が無く文字化けしてしまいます。 イメージ作成の際に日本語フォントをインストールしたので、それを設定ファイルに指定して、日本語が表示されるようにしてやります。

config/matplotlibrc

font.family:  Noto Sans CJK JP

動作確認

設定が終わったので、以下のコマンドを順に流してコンテナを作成します。

# 1. イメージ作成
docker compose build

# 2. コンテナ起動
docker compose up -d

コンテナが起動したら、以下のコマンドで Jupyter Notebook を起動します。

# Jupyter Notebook 起動
docker compose exec python3 jupyter notebook --allow-root --ip=0.0.0.0

コマンドを実行すると、以下のように URL が出力されます。

f:id:ro9rito:20211011175416p:plain

出力された URL にアクセスすれば、Jupyter Notebook を使い始めることができます。

f:id:ro9rito:20211011175434p:plain

まとめ

データを統計解析してみたりそれをグラフなどで視覚化してみようと思った時に、ローカル環境でその全てを実現しようとすると Python 初心者にはなかなか難しいですが、Jupyter Notebook の環境を作れば簡単にその両方を叶えられるので、統計を学習したりする場合には手早くできてとても便利なのでオススメです。

AWS API Gateway(HTTP API) + Lambda + DynamoDB でサーバレスアーキテクチャを構築する

この記事は個人ブログと同じ内容です

www.ritolab.com


AWSAPI Gateway(HTTP API) + Lambda + DynamoDB を使ってサーバレスアーキテクチャを構築してみます。

開発環境 - Terraform v1.0.2

DynamoDB テーブルを作成

データベースを用意します。DynamoDB にテーブルを作成します。

構成としては、date(年月日)をパーティションキーにして、ソートキーに time(時分秒)を起きたいと思います。

先に見せるとこんな感じのテーブルを作ります

f:id:ro9rito:20210909170857p:plain

main.tf

resource "aws_dynamodb_table" "event_log_table" {
  name           = "EventLog"
  billing_mode   = "PROVISIONED"
  read_capacity  = 1
  write_capacity = 1
  hash_key       = "date"
  range_key      = "time"

  attribute {
    name = "date"
    type = "S"
  }

  attribute {
    name = "time"
    type = "S"
  }
}

DynamoDB のテーブル作成自体は簡単で、ミニマムは上記のように数行書けば作成できます。

f:id:ro9rito:20210909171633p:plain

Lambda function 作成

次に DynamoDB に情報を記録していく Lambda function を作成します。

CloudWatch Log Group と Role の作成

関数に行く前に Lambda function 実行のログを CloudWatchLogs に流すためにロググループと、そこへの書き込み、および DynamoDB にアクセスできる Lambda 用のロールを作成しておきます。

main.tf

# CloudWatch ロググループ作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group
resource "aws_cloudwatch_log_group" "log_events_lambda_function" {
  name = "/aws/lambda/${var.log_events_lambda_function_name}"
}

# Iam Role 作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role
resource "aws_iam_role" "lambda_function" {
  name = "${var.app_name}-log-events-lambda-function-role"

  assume_role_policy = jsonencode({
    Version : "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

# Iam Policy 作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy
resource "aws_iam_policy" "lambda_function_put_log_events" {
  name        = "log-events-lambda-function-policy"
  description = "IAM policy for log events Lambda function."

  policy = jsonencode({
    Version : "2012-10-17",
    Statement = [
      {
        Action   = "logs:CreateLogGroup",
        Effect   = "Allow",
        Resource = "arn:aws:logs:${var.aws_region}:${var.aws_id}:*"
      },
      {
        Action = [
          "logs:CreateLogStream",
          "logs:PutLogEvents",
        ],
        Effect = "Allow",
        Resource = [
          "${aws_cloudwatch_log_group.log_events_lambda_function.arn}:*"
        ]
      },
      // AWS Lambda: Lambda 関数に Amazon DynamoDB テーブルへのアクセスを許可します
      // https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_examples_lambda-access-dynamodb.html
      {
        Action = [
          "dynamodb:Scan",
          "dynamodb:GetItem",
          "dynamodb:PutItem",
        ]
        Effect   = "Allow"
        Resource = aws_dynamodb_table.event_log_table.arn
      },
    ]
  })
}

# Policy を Role にアタッチ
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment
resource "aws_iam_role_policy_attachment" "lambda_function_put_log_events" {
  policy_arn = aws_iam_policy.lambda_function_put_log_events.arn
  role       = aws_iam_role.lambda_function.name
}

Lambda function 作成

function は node.js で、ソースコードは terraform 管理です。

index.js

const AWS = require("aws-sdk");

const dynamo = new AWS.DynamoDB.DocumentClient();

const tableName = "EventLog";

exports.handler = async (event, context) => {
    let body;
    let statusCode = 200;
    const headers = {
        "Content-Type": "application/json"
    };

    try {
        switch (event.routeKey) {
            case "GET /events":
                body = await dynamo.scan({
                    TableName: tableName
                }).promise();
                break;
            case "POST /events":
                let requestJSON = JSON.parse(event.body);
                await dynamo
                .put({
                    TableName: tableName,
                    Item: {
                        date: requestJSON.date,
                        time: requestJSON.time,
                        userId: requestJSON.userId,
                        eventType: requestJSON.eventType
                    }
                })
                .promise();
                body = `Put item ${requestJSON.userId}`;
                break;
            default:
                throw new Error(`Unsupported route: "${event.routeKey}"`);
        }
    } catch (err) {
        statusCode = 400;
        body = err.message;
    } finally {
        body = JSON.stringify(body);
    }

    return {
        statusCode,
        body,
        headers
    };
};

Lambda 関数自体は API Gatewayチュートリアルを参考にしています。

docs.aws.amazon.com

main.tf

# ファイルの ZIP 化
## https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/archive_file
data "archive_file" "log_events_lambda_function" {
  type        = "zip"
  source_dir  = "path/to/functions/${var.log_events_lambda_function_name}"
  output_path = "path/to/functions/upload/${var.log_events_lambda_function_name}.zip"
}

# Lambda function 作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function
resource "aws_lambda_function" "log_events" {
  filename      = data.archive_file.log_events_lambda_function.output_path
  function_name = var.log_events_lambda_function_name
  role          = aws_iam_role.lambda_function.arn
  handler = "index.handler"

  source_code_hash = filebase64sha256(data.archive_file.lambda_function.output_path)

  runtime = "nodejs14.x"

  depends_on = [
    aws_iam_role_policy_attachment.lambda_function_put_log_events,
    aws_cloudwatch_log_group.log_events_lambda_function
  ]
}
  • Lambda function を適用させる際に ZIP で上げるため、作成した js ファイルを ZIP 化しています。
    • source_code_hash で ZIP ファイルをハッシュ化しています(filebase64sha256() 関数は Terraform 0.11.12 以降。それ以前は base64sha256() と file() 関数を使う)
    • apply や plan 時に ZIP ファイルが作成される
  • role でに先程作成した IAM Role を指定しています。

apply すると Lambda 関数が作成されます

f:id:ro9rito:20210909171217p:plain

API Gateway エンドポイント作成

ここからは Amazon API Gateway を使って先程作成した lambda function を利用する際のエンドポイントを作成していきます。

今回は HTTP API で作成します。作成に必要なステップは以下になります。

  1. API 作成
  2. 統合設定
  3. ルート設定
  4. ステージ設定

API 作成

まずは大本になる API を作成します。

main.tf

# API 作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_api
resource "aws_apigatewayv2_api" "log_events" {
  name          = "xxxxxxxxxxxx"
  protocol_type = "HTTP"

  cors_configuration {
    allow_origins = ["*"]
    allow_methods = ["GET", "POST"]
    allow_headers = ["*"]
  }
}

cors_configuration - CORS の設定を行っています。設定できる項目は以下(すべて optional)

  • allow_origins
  • allow_headers
  • allow_methods
  • allow_credentials
  • expose_headers
  • max_age

HTTP API の CORS の設定

docs.aws.amazon.com

統合設定(API と Lambda function の関連付け)

先程作成した API と Lambda function を紐付けます。

main.tf

# 統合設定(API と Lambda function の紐付け)
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_integration
resource "aws_apigatewayv2_integration" "log_events" {
  api_id           = aws_apigatewayv2_api.log_events.id
  integration_type = "AWS_PROXY"
  integration_method = "POST"
  integration_uri = aws_lambda_function.log_events.invoke_arn
  payload_format_version = "2.0"
}

integration_type - 統合タイプを選択。HTTP API の場合は websocket もサポートしているので選択肢がいくつかあり、Lambda function と統合させる場合は AWS_PROXY を設定する。

選択できる項目

  • AWS(WebSocket API でのみサポート)
  • AWS_PROXY
  • HTTP(WebSocket API でのみサポート)
  • HTTP_PROXY
  • MOCK(WebSocket API でのみサポート)

docs.aws.amazon.com

integration_method - 統合先へのリクエストの HTTP method を指定。Integration_type が MOCK でない場合は指定は必須。Lambda との統合なので POST を指定。

docs.aws.amazon.com

ルート設定

作成した API に対してメソッドやリソースを設定します。

docs.aws.amazon.com

main.tf

# ルート設定
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_route
variable "routeList" {
  type    = list(string)
  default = ["GET /events", "POST /events"]
}
resource "aws_apigatewayv2_route" "log_events" {
  count  = length(var.routeList)

  api_id = aws_apigatewayv2_api.log_events.id
  route_key = element(var.routeList, count.index)

  target = "integrations/${aws_apigatewayv2_integration.log_events.id}"
}

target - アタッチする統合を指定する - Integrations/{IntegrationID} の形式で指定 - IntegrationID は aws_apigatewayv2_integration リソースの識別子

route_key - "GET /events" のように「<HTTP メソッド> <リソースパス>」で指定 (今回は 2 メソッド分まとめた記述になっている)

ステージ設定

ステージを設定します。

docs.aws.amazon.com

リクエストのログも残したいので CloudWatch のロググループも併せて作成して設定します。

main.tf

resource "aws_cloudwatch_log_group" "log_events_http_api" {
  name = "/aws/apigateway/${var.log_events_api_name}"
}

# ステージ設定
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_stage
resource "aws_apigatewayv2_stage" "log_events" {
  api_id      = aws_apigatewayv2_api.log_events.id
  name        = "$default"
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.log_events_http_api.arn

    format = jsonencode({
      requestId : "$context.requestId",
      ip : "$context.identity.sourceIp",
      requestTime : "$context.requestTime",
      httpMethod : "$context.httpMethod",
      routeKey : "$context.routeKey",
      status : "$context.status",
      protocol : "$context.protocol",
      responseLength : "$context.responseLength",
      errorMessage : "$context.error.message",
      errorResponseType : "$context.error.responseType"
      authorizerError : "$context.authorizer.error",
      integrationErrorMessage : "$context.integrationErrorMessage"
    })
  }
}

access_log_settings でアクセスをログに記録するための設定を行なっています。format でアクセスログ 1 行の形式を指定、$context 変数で値を指定します。

docs.aws.amazon.com

APIGateway に Lambda へのアクセスを許可

APIGateway 側の一通りの設定が済んだので、最後に APIGateway から Lambda へのアクセスを許可します。

main.tf

# APIGateway に Lambda へのアクセスを許可
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission
resource "aws_lambda_permission" "execution_log_access_api_gateway" {
  statement_id  = "test-AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.log_events.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_apigatewayv2_api.log_events.execution_arn}/*/*"
}

動作確認

ここまでで一連の構成は定義できたので、apply して環境を構築し、エンドポイントへリクエストを送信すれば API Gateway, Lambda, DynamoDB が連携して動作し、データが保存されます。

f:id:ro9rito:20210909172147p:plain

カスタムドメイン設定

API Gateway で作成されたエンドポイントの URL ですが、でデフォルトでは

https://<api_id>.execute-api.<region>.amazonaws.com/xxxxxxx...

という形式になっています。

カスタムドメインを設定すると、自分のドメインを使ってエンドポイントの URL を作成できるので、こちらの設定を行なってみます。

(ネイキッドドメインは Route53 に登録済みの前提です。

ACM 証明書発行

まずは、利用するサブドメインの証明書を ACM で作成しておきます。

main.tf

# 作成済みホストゾーン情報の取得
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone
data "aws_route53_zone" "myDomain" {
  name = var.domain_name
}

# ACM 証明書作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate
resource "aws_acm_certificate" "APIGatewayHttpApi" {
  domain_name       = "samle-htttp-api.ritolab.com"
  validation_method = "DNS"
}

## ACM 検証用 CNAME レコード
### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record
resource "aws_route53_record" "api_gateway_http_api_acm_c" {
  for_each = {
  for d in aws_acm_certificate.APIGatewayHttpApi.domain_validation_options : d.domain_name => {
    name   = d.resource_record_name
    record = d.resource_record_value
    type   = d.resource_record_type
  }
  }
  zone_id         = data.aws_route53_zone.myDomain.zone_id
  name            = each.value.name
  type            = each.value.type
  records         = [each.value.record]
  ttl             = 60
  allow_overwrite = true
}

## ACM 証明書 / CNAME レコード 連携
### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation
resource "aws_acm_certificate_validation" "APIGatewayHttpApi" {
  certificate_arn         = aws_acm_certificate.APIGatewayHttpApi.arn
  validation_record_fqdns = [for record in aws_route53_record.api_gateway_http_api_acm_c : record.fqdn]

  depends_on = [
    aws_acm_certificate.APIGatewayHttpApi,
    aws_route53_record.api_gateway_http_api_acm_c
  ]
}

カスタムドメイン設定

カスタムドメインを設定します。

main.tf

## カスタムドメイン登録
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_domain_name
resource "aws_apigatewayv2_domain_name" "http_api" {
  domain_name = "sub.domain.com"

  domain_name_configuration {
    certificate_arn = aws_acm_certificate.APIGatewayHttpApi.arn
    endpoint_type   = "REGIONAL"
    security_policy = "TLS_1_2"
  }

  // 証明書作成が完了してから
  depends_on = [
    aws_acm_certificate_validation.APIGatewayHttpApi,
  ]
}

## API マッピング
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_api_mapping
resource "aws_apigatewayv2_api_mapping" "http_api" {
  api_id      = aws_apigatewayv2_api.log_events.id
  domain_name = aws_apigatewayv2_domain_name.http_api.id
  stage       = aws_apigatewayv2_stage.log_events.id
}

# route53 A レコード作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record
resource "aws_route53_record" "http_api" {
  name    = aws_apigatewayv2_domain_name.http_api.domain_name
  type    = "A"
  zone_id = data.aws_route53_zone.myDomain.zone_id

  alias {
    name                   = aws_apigatewayv2_domain_name.http_api.domain_name_configuration[0].target_domain_name
    zone_id                = aws_apigatewayv2_domain_name.http_api.domain_name_configuration[0].hosted_zone_id
    evaluate_target_health = false
  }
}

カスタムドメインを登録し、API マッピングを行なった後に、route53 へ使用するサブドメインの A レコードを作成しています。

ポイントとしては、カスタムドメイン登録のときに depends_on を指定し、証明書の作成を待ってからカスタムドメインの登録を行うようにします。 依存関係を指定しておかないと証明書が有効になる前にカスタムドメインを登録しようとしてエラーになります。

これでカスタムドメインでエンドポイントへアクセスできるようになります。

% curl https://sample-http-api.ritolab.com/events
{"Items":[{"date":"2021-08-28","eventType":"test","time":"14:19:00.000","userId":"EX8C9MRZfhL"},{"date":...

IAM 認証

現状ではエンドポイントは開放されている状態なので、IAM 認証をかけてアクセス制限を掛けます。(既に Cognito とか Auth0 とかで認証のあるサービスなら IAM 認証ではなく Lambda オーソライザー使うのが良さそう)

API Gateway のルートの設定において、認証を IAM に変更します。

main.tf

resource "aws_apigatewayv2_route" "log_events" {
  .
  .
  .
  authorization_type = "AWS_IAM"
}

これだけです。あとはリクエスタとしての IAM ユーザーを作成して、適宜必要な権限を設定してあげます。

main.tf

# IAM Role for ApiGateway
resource "aws_iam_policy" "api_gateway_log_event_requester" {
  name        = "xxxxxxxxx"

  policy = jsonencode({
    Version : "2012-10-17",
    Statement = [
      {
        Effect: "Allow",
        Action: [
          "execute-api:Invoke"
        ],
        Resource: [
          "arn:aws:execute-api:${var.aws_region}:${var.aws_id}:${aws_apigatewayv2_api.log_events.id}/*/GET/events",
          "arn:aws:execute-api:${var.aws_region}:${var.aws_id}:${aws_apigatewayv2_api.log_events.id}/*/POST/events",
        ],
      },
    ]
  })
}

# IAM User for ApiGateway execution
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user
resource "aws_iam_user" "api_gateway_log_event_requester" {
  name = "api-gateway-log-event-requester"
}

## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment
resource "aws_iam_user_policy_attachment" "api_gateway_log_event_requester" {
  user       = aws_iam_user.api_gateway_log_event_requester.name
  policy_arn = aws_iam_policy.api_gateway_log_event_requester.arn
}

これで、アクセスが許可されたユーザー以外からのリクエストは遮断されます。

f:id:ro9rito:20210909172439p:plain

一方で、許可された IAM ユーザーからのリクエストは受け入れられデータが取得できている事を確認できました。

f:id:ro9rito:20210909172457p:plain

まとめ

エンドポイントからバックエンド、DB まで一連をサーバレスアーキテクチャで構築しました。

レスポンスは遅くないので、これでサーバ運用の手間が減ると考えるとサーバレス化は手段としてはアリだなと感じました。

あとやはり個々のサービスが疎結合になるため、必要に応じて最適なプログラミング言語を選択できるのも場合によっては利点になり得る要素だと思いました。

それと、API Gateway でのエンドポイントを今回は HTTP API で構築してみましたが、オートデプロイが行えるのは現時点では HTTP API のみで REST API には無い機能だったので、開発時にデプロイを気にしなくて良いのはとても便利でした。

AWS Well-Architected フレームワークの「パフォーマンス効率の柱 設計の原則」においてサーバレスアーキテクチャが推奨されているように、サーバレス構成は適所で採用すればとても使えるアーキテクチャでした。

docs.aws.amazon.com