AWS CDKでLambdaをいい感じにIaC化する
AWS Lambdaに対してのIaCのやり方を模索していて、AWS CDKが今のところ一番いい感じだと思ったので試してみた記録です。
各種方法との比較
AWS LambdaをIaC化するやり方は主に以下のやり方があると思います。
他にもいろいろあると思いますが、自分が経験したものの範囲になります。
AWS SAMは割と以前から存在するLambdaをIaC化するためのAWS公式の方法です。AWS CDKと比較するとAWS SAMテンプレートと呼ばれるAWS CloudFormationテンプレートを拡張した長大なYAMLを利用しなければいけないところが辛いところです。
Serverless Frameworkは公式ではないところの不安やLambdaのみのIaCの仕組みなので学習コストが気になるところです。AWS CDKと比較しても優位がない感じがします。
Terraformは公式ではないものの広くAWSのIaCで使われていて安心感があります。ただLambdaを管理するには手間がかかります。Terraformの場合、基本的にAWSのAPIをHCLでそのまま利用する形です。Lambda関数を作成する場合、zipファイルを用意するか、コンテナイメージを用意するかがあり面倒です。
こういったことを踏まえるとAWS CDKでのやり方は面倒なことがなく、公式で提供されている仕組みということもあり一番良いと考えました。AWS CDKではコンストラクトという要素でCloudFormationのリソースを利用可能ですが、それらを抽象化した高度なコンストラクトも用意されています。AWS Lambdaにおいては楽にリソースを作成する体験が得られます。
以下でどのようになるかを紹介します。
AWS CDKによるLambda関数の作成方法
以下のAWS CDK Workshopの通りにcdk deploy
ができていることを前提としています。
aws_lambda.Function
でLambda関数を作成できます。
lib/main.ts
import { aws_lambda, Duration, Stack } from 'aws-cdk-lib'; import { Construct } from 'constructs'; ... export class AwsLambdaCicdExampleStack extends Stack { constructor(scope: Construct, id: string, props: LambdaFunctionConfig) { super(scope, id); new aws_lambda.Function(this, `HelloFunction`, { functionName: `hello-function-${props.environmentValues.NODE_ENV}`, runtime: props.runtime, code: aws_lambda.Code.fromAsset(props.path), handler: props.handler, timeout: Duration.minutes(15), environment: props.environmentValues }); } }
Lambdaのコードを指定するにはcode
のパラメータにaws_lambda.Code.fromAsset
メソッドでLambdaのコードが配置してあるディレクトリパスを指定するだけです。zipファイル化が不要なので楽です。
AWS CDKではTypeScriptのLambda関数も扱えます。aws_lambda_nodejs.NodejsFunction
を使います。
lib/main.ts
import { aws_lambda, aws_lambda_nodejs, Duration, Stack } from 'aws-cdk-lib'; import { Construct } from 'constructs'; ... export class AwsLambdaCicdExampleStack extends Stack { constructor(scope: Construct, id: string, props: LambdaFunctionConfig) { super(scope, id); new aws_lambda_nodejs.NodejsFunction(this, `HelloFunction`, { functionName: `hello-function-${props.environmentValues.NODE_ENV}`, runtime: props.runtime, entry: props.path, handler: props.handler, timeout: Duration.minutes(15), environment: props.environmentValues }); } }
TypeScriptのLambdaのコードを指定するにはentry
のパラメータにTypeScriptのエントリーファイルのパスを指定するだけです。
NodejsFunctionでは内部的にTypeScriptのトランスパイルなどを実行してLambda関数を作成してくれます。
まとめ
LambdaをIaC化する場合、主要な要素がコードになるのでアプリケーション開発者がIaCできると良さそうです。
その場合、AWS CDKのように汎用プログラミング言語を利用できるのは、アプリケーション開発者にとって取り組みやすいのではないかと思います。
また、AWS CDKでは今回紹介したように簡単にLambdaをIaC化できる仕組みになっているのも良いです。
参考
TypeScript版
FlutterのGet startedをRiverpodでやってみたのでStatefulWidgetの場合と比較してみる
FlutterのGet startedを一通りやってみたのでその備忘録です。
docs.flutter.dev
Flutterではstate管理にRiverpodが使われることが多いようなのでこちらも試してみました。
Riverpod版と通常のStatefulWidget版で比較をしつつ説明します。
Riverpodでやってみたソースはこちら github.com
説明
Riverpod版とStatefulWidget版それぞれの実装方法で異なる点について説明します。
無限スクロールの実装
Get startedの題材には無限スクロールがあります。StatefulWidget版のソースは以下の通りです。
StatefulWidget版
class _RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = <WordPair>{}; final _biggerFont = const TextStyle(fontSize: 18); @override Widget build(BuildContext context) { return Scaffold( ... body: ListView.builder( padding: const EdgeInsets.all(16.0), itemBuilder: (context, i) { if (i.isOdd) return const Divider(); final index = i ~/ 2; if (index >= _suggestions.length) { _suggestions.addAll(generateWordPairs().take(10)); } ... return ListTile( title: Text( _suggestions[index].asPascalCase, style: _biggerFont, ), ... ); }, ) ); } }
StatefulWidget版では、スクロールしてリストの要素が少なくなったら、Stateのインスタンス変数である_suggestions
にリスト要素を追加しています。
_suggestions
が無限スクロールのリストを描画するためのstateを持っています。
Riverpod版のソースは以下の通りです。
Riverpod版
class WordListNotifier extends StateNotifier<List<Word>> { ... Future<void> addAll(List<WordPair> wordPairs) async { await Future.delayed(const Duration(milliseconds: 1)); for (var wordPair in wordPairs) { state = [...state, Word(wordPair: wordPair)]; } } ... } final wordListProvider = StateNotifierProvider<WordListNotifier, List<Word>>((ref) { return WordListNotifier(); }); ... class RandomWordsState extends ConsumerState<RandomWords> { final biggerFont = const TextStyle(fontSize: 18); @override Widget build(BuildContext context) { final words = ref.watch(wordListProvider); return Scaffold( ... body: ListView.separated( padding: const EdgeInsets.all(16.0), itemCount: words.length, itemBuilder: (context, index) { if (index >= words.length - 1) { ref .read(wordListProvider.notifier) .addAll(generateWordPairs().take(10).toList()); } return ListTile( title: Text( words[index].wordPair.asPascalCase, style: biggerFont, ), ... ); }, ), ); } }
Riverpod版では、StateNotifierProviderというstateを管理するクラスとStateNotifierを継承したstateクラスを使用しています。
やっていることはStatefulWidget版と同じで、リストの要素が少なくなったらリストのstateに新しいリスト要素を追加しています。
Riverpodでstateを更新する際にはimmutableに扱うことを前提としているので、以下のように既存のリストと新しいリストを組み合わせた新しいstateを作成します。
state = [...state, Word(wordPair: wordPair)];
Future.delayed
している部分はWidgetをbuild中にstateを変更しているエラーを回避するためのおまじないです。
ちなみにListView.builder
とListView.separated
はDividerがリストの要素に含まれるか、含まれないかの違いです。
個人的にわかりづらかったのでListView.separated
に変えました。
お気に入りの実装
Get startedの題材ではリストの中からお気に入りを選択する機能があります。StatefulWidget版のソースは以下の通りです。
StatefulWidget版
class _RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = <WordPair>{}; final _biggerFont = const TextStyle(fontSize: 18); @override Widget build(BuildContext context) { return Scaffold( ... body: ListView.builder( padding: const EdgeInsets.all(16.0), itemBuilder: (context, i) { ... final alreadySaved = _saved.contains(_suggestions[index]); return ListTile( ... onTap: () => { setState(() { if (alreadySaved) { _saved.remove(_suggestions[index]); } else { _saved.add(_suggestions[index]); } }) }, ); }, )); } void _pushSaved() { Navigator.of(context).push( MaterialPageRoute<void>( builder: (context) { final tiles = _saved.map( (pair) { return ListTile( title: Text( pair.asPascalCase, style: _biggerFont, ), ); }, ); ... }, ), ); } }
_saved
のインスタンス変数にお気に入りとして選択したリストの要素を追加することで、リストの保存を実現しています。
_suggestions
の全体のリストに_saved
に保存したリストの要素があるかどうかでお気に入りに保存するか、削除するかを決めています。
_suggestions
と_saved
で変数が分かれていますが、どちらもリストのstateなので分かれていると扱いが面倒です。
Riverpod版のソースは以下の通りです。
class WordListNotifier extends StateNotifier<List<Word>> { ... void toggle(int idx) { state = [ for (int i = 0; i < state.length; i++) if (i == idx) Word(wordPair: state[i].wordPair, isFavorite: !state[i].isFavorite) else state[i], ]; } } class RandomWordsState extends ConsumerState<RandomWords> { final biggerFont = const TextStyle(fontSize: 18); @override Widget build(BuildContext context) { final words = ref.watch(wordListProvider); return Scaffold( ... body: ListView.separated( padding: const EdgeInsets.all(16.0), itemCount: words.length, itemBuilder: (context, index) { ... return ListTile( ... onTap: () { ref.read(wordListProvider.notifier).toggle(index); }, ); }, ), ); } void pushSaved() { final words = ref.watch(wordListProvider); final saved = words.where((word) => word.isFavorite == true); Navigator.of(context).push( MaterialPageRoute<void>( builder: (context) { final tiles = saved.map( (pair) { return ListTile( title: Text( pair.wordPair.asPascalCase, style: biggerFont, ), ); }, ); ... }, ), ); } }
WordListNotifierのtoggleメソッドでお気に入りのstateを処理しています。
前述のとおり、immutableに扱うため、お気に入りのstateを変更した要素だけでなく、全てのリスト要素を作成しています。
全体のリストのstate内にお気に入りのstateも保持しているので、stateの扱いがわかりやすくなっています。
まとめ
Riverpodでstateを扱うためにはimmutableを意識する、Providerを利用するなど気をつけるポイントがあります。
Riverpodでのテスト方法も学習しておいた方がよいテーマだと思います。次の機会にやってみたいと思います。
Redshift Serverlessを用いてdbtのGetting startedを実施する
本投稿では、Redshift Serverlessを用いてdbtのGetting startedを実施します。
dbtだとBigQueryを使っている事例が多い印象です。Redshiftでもできるということを確認してみたく実施してみました。
実施するのはdbtの公式ページにある下記の内容です。異なる点として、dbt Cloudではなく、dbt Coreを使用します。
dbtはSQLを使ってデータ変換処理を行い、データモデルを手軽に作成できるツールです。
dbtを試すにはdbtに対応したデータの環境が必要です。今回はRedshift Serverlessを使用します。
Redshift Serverlessには300ドル分の無料トライアルがありますので始めやすいと思います。
手順の詳細については参考情報も合わせてご覧ください。
手順
Redshift Serverlessを用意する
Redshift Serverlessの環境を作成する
AWSマネージメントコンソールを開いた状態です。
Amazon Redshiftの画面を開き、Redshift serverlessを選択します。
「Amazon Redshift サーバーレスの使用を開始する」画面では、「デフォルト設定を使用」を選択します。
関連づけられたIAMロール→IAMロールを作成→「デフォルトのIAMロールを作成する」画面では、「任意のS3バケット」を選択し、IAMロールを作成します。そして、設定を保存します。
これでRedshift Serverlessが使えるようになります。
Redshift Serverlessに外部からアクセスできるようにする
Redshift Serverlessの画面から「default」のワークグループを選択し、設定画面を表示します。
ネットワークとセキュリティの項目で編集をします。
セキュリティグループを変更し、自身のローカルマシンからのアクセスを許可します。
「[パブリックにアクセス可能] をオンにする」に有効にして変更を保存します。
Redshift Serverlessの接続先情報をメモする
Redshift Serverlessの接続先情報をメモしておきます。
ワークグループの画面に表示されているエンドポイント名をメモしておきます。
名前空間の「default」を選択し、管理者パスワードを変更します。
管理者ユーザー名と管理者パスワードをメモしておきます。
これでRedshift Serverlessの準備は完了です。
dbtのGetting startedを実施する
dbtをインストールする
$ brew update $ brew tap dbt-labs/dbt $ brew install dbt-redshift
バージョンが表示できればインストールできています。
$ dbt --version Core: - installed: 1.2.1 - latest: 1.2.1 - Up to date! Plugins: - redshift: 1.2.1 - Up to date! - postgres: 1.2.1 - Up to date!
データを準備する
S3バケットを用意して、以下のファイルを該当のS3バケットにアップロードします。
Redshift Servelessの画面からRedshift query editor v2を表示します。
dev
データベースに対して以下のクエリを実行します。
create schema if not exists jaffle_shop; create schema if not exists stripe;
テーブルを作成します。
create table jaffle_shop.customers( id integer, first_name varchar(50), last_name varchar(50) ); create table jaffle_shop.orders( id integer, user_id integer, order_date date, status varchar(50), _etl_loaded_at timestamp default current_timestamp ); create table stripe.payment( id integer, orderid integer, paymentmethod varchar(50), status varchar(50), amount integer, created date, _batched_at timestamp default current_timestamp );
データをコピーします。
from
には先ほどS3にアップロードしたファイルのS3 URIを指定します。
IAMロールにはRedshift Serverlessの環境を作成したときに同時作成したデフォルトのIAMロールのARNを指定します。
copy jaffle_shop.customers( id, first_name, last_name) from 's3://dbt-data-lake-xxxx/jaffle_shop_customers.csv' iam_role 'arn:aws:iam::XXXXXXXXXX:role/RoleName' region 'ap-northeast-1' delimiter ',' ignoreheader 1 acceptinvchars; copy jaffle_shop.orders(id, user_id, order_date, status) from 's3://dbt-data-lake-xxxx/jaffle_shop_orders.csv' iam_role 'arn:aws:iam::XXXXXXXXXX:role/RoleName' region 'ap-northeast-1' delimiter ',' ignoreheader 1 acceptinvchars; copy stripe.payment(id, orderid, paymentmethod, status, amount, created) from 's3://dbt-data-lake-xxxx/stripe_payments.csv' iam_role 'arn:aws:iam::XXXXXXXXXX:role/RoleName' region 'ap-northeast-1' delimiter ',' ignoreheader 1 Acceptinvchars;
データが存在することを確認します。
select * from jaffle_shop.customers; select * from jaffle_shop.orders; select * from stripe.payment;
dbt CoreでGetting startedを実施する
dbtの初期セットアップをします。
$ dbt init jaffle_shop
Redshift Serverlessの接続情報に変更します。
host
にはRedshift Serverlessのエンドポイント名を記載します。
user
とpassword
にはRedshift Serverlessの管理者ユーザー名と管理者パスワードを記載します。
dbname
にはdev
データベースを指定します。
~/.dbt/profiles.yml
jaffle_shop: outputs: dev: dbname: dev host: default.xxxxxxxxxx.ap-northeast-1.redshift-serverless.amazonaws.com password: xxxxxx port: 5439 schema: dbt threads: 1 type: redshift user: xxxxxx target: dev
接続できるかを確認します。
$ dbt debug
ここまででdbtの環境が作れました。以降はdbtのドキュメントどおりです。
dbtを使ってデータモデルを作ります。
models/customers.sql
with customers as ( select id as customer_id, first_name, last_name from jaffle_shop.customers ), orders as ( select id as order_id, user_id as customer_id, order_date, status from jaffle_shop.orders ), customer_orders as ( select customer_id, min(order_date) as first_order_date, max(order_date) as most_recent_order_date, count(order_id) as number_of_orders from orders group by 1 ), final as ( select customers.customer_id, customers.first_name, customers.last_name, customer_orders.first_order_date, customer_orders.most_recent_order_date, coalesce(customer_orders.number_of_orders, 0) as number_of_orders from customers left join customer_orders using (customer_id) ) select * from final
dbtを実行します。
$ dbt run
これでデータモデルが作成されます。
データモデルの作り方を変更します。
デフォルトはViewの形式でモデルを作りますが、テーブルの形式で実態がある状態でモデルを作ることができます。
dbt_project.yml
models: jaffle_shop: +materialized: table example: +materialized: view
$ dbt run
dbt.customers
テーブルが新しく作成されていることが確認できます。
別のモデルを作成し、それを再利用する形でモデルを作ります。
select id as customer_id, first_name, last_name from jaffle_shop.customers
select id as order_id, user_id as customer_id, order_date, status from jaffle_shop.orders
models/customers.sql
with customers as ( select * from {{ ref('stg_customers') }} ), orders as ( select * from {{ ref('stg_orders') }} ), customer_orders as ( select customer_id, min(order_date) as first_order_date, max(order_date) as most_recent_order_date, count(order_id) as number_of_orders from orders group by 1 ), final as ( select customers.customer_id, customers.first_name, customers.last_name, customer_orders.first_order_date, customer_orders.most_recent_order_date, coalesce(customer_orders.number_of_orders, 0) as number_of_orders from customers left join customer_orders using (customer_id) ) select * from final
models/customers.sql
でref関数を使用してmodels/stg_customers.sql
とmodels/stg_orders.sql
を利用して、モデルを作成します。
各データモデルに対して、テストを実施します。
models/schema.yml
version: 2 models: - name: customers columns: - name: customer_id tests: - unique - not_null - name: stg_customers columns: - name: customer_id tests: - unique - not_null - name: stg_orders columns: - name: order_id tests: - unique - not_null - name: status tests: - accepted_values: values: ['placed', 'shipped', 'completed', 'return_pending', 'returned'] - name: customer_id tests: - not_null - relationships: to: ref('stg_customers') field: customer_id
$ dbt test 23:29:11 Running with dbt=1.2.1 23:29:11 Found 3 models, 7 tests, 0 snapshots, 0 analyses, 289 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics 23:29:11 23:29:12 Concurrency: 1 threads (target='dev') 23:29:12 23:29:12 1 of 7 START test accepted_values_stg_orders_status__placed__shipped__completed__return_pending__returned [RUN] 23:29:12 1 of 7 PASS accepted_values_stg_orders_status__placed__shipped__completed__return_pending__returned [PASS in 0.41s] 23:29:12 2 of 7 START test not_null_customers_customer_id ............................... [RUN] 23:29:13 2 of 7 PASS not_null_customers_customer_id ..................................... [PASS in 0.55s] 23:29:13 3 of 7 START test not_null_stg_customers_customer_id ........................... [RUN] 23:29:13 3 of 7 PASS not_null_stg_customers_customer_id ................................. [PASS in 0.23s] 23:29:13 4 of 7 START test not_null_stg_orders_order_id ................................. [RUN] 23:29:13 4 of 7 PASS not_null_stg_orders_order_id ....................................... [PASS in 0.21s] 23:29:13 5 of 7 START test unique_customers_customer_id ................................. [RUN] 23:29:14 5 of 7 PASS unique_customers_customer_id ....................................... [PASS in 0.56s] 23:29:14 6 of 7 START test unique_stg_customers_customer_id ............................. [RUN] 23:29:14 6 of 7 PASS unique_stg_customers_customer_id ................................... [PASS in 0.58s] 23:29:14 7 of 7 START test unique_stg_orders_order_id ................................... [RUN] 23:29:15 7 of 7 PASS unique_stg_orders_order_id ......................................... [PASS in 0.37s] 23:29:15 23:29:15 Finished running 7 tests in 0 hours 0 minutes and 3.58 seconds (3.58s). 23:29:15 23:29:15 Completed successfully 23:29:15 23:29:15 Done. PASS=7 WARN=0 ERROR=0 SKIP=0 TOTAL=7
各モデルのtests
に指定したunique
やnot_null
の指定にしたがってテスト用のクエリが実行されます。
データモデルについてのドキュメントを作成します。
models/schema.yml
version: 2 models: - name: customers description: One record per customer columns: - name: customer_id description: Primary key tests: - unique - not_null - name: first_order_date description: NULL when a customer has not yet placed an order. - name: stg_customers description: This model cleans up customer data columns: - name: customer_id description: Primary key tests: - unique - not_null - name: stg_orders description: This model cleans up order data columns: - name: order_id description: Primary key tests: - unique - not_null - name: status tests: - accepted_values: values: ['placed', 'shipped', 'completed', 'return_pending', 'returned']
データモデルのdescription
やカラムのdescription
に説明を記述できます。
$ dbt docs generate $ dbt docs serve
データモデルの説明などがわかるドキュメントサイトが表示されます。
まとめ
dbtの公式Getting startedをRedshift Serverlessで試しました。dbtの使い方を知ることができる良いGetting startedだと思いました。
dbtにおいては、SQLを再利用できる他にテストやドキュメント生成をする機能が使えそうという印象を持ちました。機会があれば本格的に利用したいと思います。
レガシーなSQLコードをどのようにdbt向けにリファクタリングするかという内容やベストプラクティスの内容など本格的に使う場合にも参考になる情報が記載されていて良いと感じました。