はじめに
Infrastructure from Code(IfC)ツールの一種である、Wingでシンプルな 「APIGateway → Lambda → DynamoDB」のCRUDアプリを作ってみました。公式チュートリアル実施後にもう少し理解を深めたかったので、実施してみた記事になります。
なお、Wingのインストールなど初期設定については割愛しています。
Wing自体の説明も少し入れていますが、日々アップデートが進んでいるので気になった方は公式ドキュメントをぜひ読んでみてください。
ターゲット読者:
- IaCに魅力を感じている方
- Infrastructure from Codeに興味がある方
- Wingを試してみたいと思っている方
Wingとは
Wing とはプログラミング言語であり、アプリケーションコードとインフラコードを同時に記述できる言語です。
公式では cloud-oriented language と説明されており、AWS, GCP, Azureといったクラウドサービス上でアプリ構築することを前提とした言語になっています。
2023年7月には $20Million (約30億円) の資金調達 に成功しています。
簡単なコード例は以下のようになります。
main.w
bring cloud;
let api = new cloud.Api();
api.get("/", inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
return cloud.ApiResponse {
status: 200,
body: "Hello Wing"
};
});
こちらのコードをコンパイルすると、APIGateway → Lambda構成のterraformテンプレートとLambda関数のjsファイルが生成されます。
執筆時点(2023.10.2)では、バージョン 0.34.9 で 機能は多くありませんが、思想が面白く正式リリースされた際に使ってみたいと感じたので、触ってみました。
Wingで実装する際の流れ
Wingを使用して実装する際の体験は非常に興味深いものだったので、その概要をご紹介します。
- 実装する
- ローカルのデモ機能でテスト
- コンパイルしてterraformのテンプレートを生成
- terraformのapplyコマンドでAWSへデプロイ
2.ローカルのデモ機能でテスト について軽く説明します。
Wingの特徴として、ローカルでクラウドアーキテクチャを含めたデモが簡単に行えることが挙げられます。
わずかなコマンド wing it main.w
で、先程述べたシンプルなコードを実行するだけで、ブラウザ上でデモが立ち上がります。
このように、クラウドサービスにデプロイされているかのように想像しながらデモを行うことが可能です。
デプロイせずともローカルで簡単にデモを動かせるのはWingの大きな強みです。使用感としてもかなり使いやすく、デプロイして動作確認するようなストレスから解放されます。
3.コンパイルしてterraformのテンプレートを生成 4.terraformのapplyコマンドでAWSへデプロイ について軽く説明します。
現状は Terraform & AWS の環境しか選択できませんが、今後は CDK & AWS, Terraform & Azure, Terraform & GCP にもデプロイ可能にする展望があるようです。1つのコードでAWS, GCP, Azureにデプロイできたら面白いですね。
CRUDのAPIを実装してみた
今回のコード全体はこちらのリポジトリに挙げています。
公式チュートリアルでは「Lambda → SQS → S3」や「Lambda → SNS → DynamoDB」などの構成を体験することができます。今回の記事ではチュートリアルの次のステップとして、サーバーレスな「APIGateway → Lambda → DynamoDB」の構成を試してみました。
簡単な仕様は以下です。
- DynamoDBに
users
というテーブルを作成 - usersテーブルには
id
とname
を保存する - usersテーブルのHashKeyは
id
- 以下APIのパスを用意する。それぞれCRUDに対応。
- POST
- path: /users
- body: {name: "UserName"}
- GET
- path: /users/{id}
- PUT
- path: /users/{id}
- body: {name: "UserName"}
- DELETE
- path: /users/{id}
- POST
Wingの実装
コードは以下のようになりました。
main.w
bring cloud;
bring ex;
bring util;
let api = new cloud.Api();
let userTable = new ex.DynamodbTable({
name: "users",
attributeDefinitions: {
"id": "S"
},
hashKey: "id"
});
api.get("/users/{id}", inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let user = userTable.getItem({"id": request.vars.get("id")});
return cloud.ApiResponse {
status: 200,
body: Json.stringify(user)
};
});
api.post("/users", inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
// リクエストボディからユーザー名を取得
if let body = request.body {
let id = util.uuidv4();
let parsedBody = Json.parse(body);
let userName = parsedBody.get("name").asStr();
if userName == "" {
return cloud.ApiResponse {
status: 400,
body: "name required."
};
}
try {
userTable.putItem({
"id": id,
"name": userName
}, {
// 上書きをしない条件を記載
// see: https://www.winglang.io/docs/standard-library/ex/api-reference#conditionexpressionoptional-
// see: https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html
conditionExpression: "attribute_not_exists(id)"
});
} catch e {
log(e);
return cloud.ApiResponse {
status: 400,
body: "id duplicated."
};
}
return cloud.ApiResponse {
status: 200,
body: "SUCCESS"
};
}
// bodyがない場合
// NOTE: 本来bodyがない場合をバリデーションで取り除きたかったが、型推論が効かないため書きづらく最後に持ってきた
return cloud.ApiResponse {
status: 400,
body: "request body required."
};
});
api.put("/users/{id}", inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = request.vars.get("id");
if let body = request.body {
let parsedBody = Json.parse(body);
let userName = parsedBody.get("name").asStr();
if userName == "" {
return cloud.ApiResponse {
status: 400,
body: "name required."
};
}
try {
userTable.putItem({
"id": id,
"name": userName
}, {
// 上書きのみの条件
conditionExpression: "attribute_exists(id)"
});
} catch e {
log(e);
return cloud.ApiResponse {
status: 400,
body: "id not exists."
};
}
return cloud.ApiResponse {
status: 200,
body: "SUCCESS"
};
}
// bodyがない場合
// NOTE: 本来bodyがない場合をバリデーションで取り除きたかったが、型推論が効かないため書きづらくこのようにした
return cloud.ApiResponse {
status: 400,
body: "request body required."
};
});
api.delete("/users/{id}", inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = request.vars.get("id");
userTable.deleteItem({"id": id});
return cloud.ApiResponse {
status: 200,
body: "SUCCESS"
};
});
少し解説。
let id = request.vars.get("id");
request.vars
で パスパラメータを取得することができます。
その他取得できるものはこちらに記載があります。
bring util;
// ...
util.uuidv4();
util
からはWingの言語機能で用意されている 便利関数を利用できます。
その他の関数はこちらに記載があります。
ちなみに、こちらはまだ試せていませんが、自作したJavaScriptのソースを読み込ませてWingプログラム内で利用することも可能です。
デモで動作確認
デモの様子も軽く紹介します。
wing it
でデモを起動します。
Api, Function, DynamoDBTableの3つのリソースを確認することができます。
試しにPOSTのAPIを叩いてデータ登録してみます。
続いてDynamoDBTableのリソースを見てみると、データが登録されていることが確認できます。
Terraformテンプレートにコンパイルしてデプロイ
wing compile --target tf-aws
で、WingコードからTerraformのコードにコンパイルします。
その後、生成されたファイルを元にterraformのコマンドでAWSにデプロイします。
cd target/main.tfaws
terraform init
terraform apply
※ちなみに、私の環境ではスイッチロール&MFA認証が必要なアカウントにデプロイする必要があったため、こちらの記事を参考に aws-vault
を利用して terraform コマンドを実行しました。
aws-vault exec <profile名> -- terraform init
aws-vault exec <profile名> -- terraform apply
AWS環境で動作確認
こちらも軽く動作確認しておきます。
DynamoDB テーブル
APIGateway
Lambda
Postmanで登録のAPIを実行してみます。
DynamoDBにデータが登録されていることが確認できます。
所感
- 言語仕様や機能が固まってないため、現状は複雑なアプリケーションを作るのは難しい
- デモが非常に使いやすくて快適に開発できる
- クラウドサービス側の最新機能の追従はどのように行なっていくつもりなのか気になる
- コアな部分に修正が入っている段階なので、またしばらく経った後に触ってみてアップデートを追っていきたい
この記事が参考になっていれば幸いです!