AWS-CDKを用いたオリジナルなSymbolAPIを作ろう
👓

AWS-CDKを用いたオリジナルなSymbolAPIを作ろう

タグ
SymbolAWS
公開日
March 22, 2022

株式会社Opening Lineの松本一将です。

本日はAWS-CDKを使ったオリジナルなAPIを作ろうという記事をご紹介させていただければ幸いです。

本記事の対象読者

どちらかというとシステムエンジニア向けです。(ですが最近システムエンジニアという職業の肩書きが意味を成さず、プロとアマチュアの境界線が非常に曖昧な昨今となっております)

ですので、Symbolで何か開発したい方向けとさせていただきます。

みなさん楽しく読んでいただけますと幸いです。

AWS-CDKとは

AWS-CDKについてはいろんな方が説明しているかと思いますが、基本的なコンセプトは「AWSのリソースをコードで管理しよう」というのが基本的なコンセプトです。

このコンセプトを理解すると次のようなメリットが見えてきます

  1. コードによる管理で人に起因するミスを限りなく少なくする
  2. コードによる管理でマニュアル作成などの「付加価値を産まない重労働」を減らしていく

といった2つのコンセプトがあります。

よくある質問1: なぜAWSなのですか?

この手の記事で一番多い質問なのですが、対抗馬として挙げられるのがFirebaseのCloudFunctionsだと思います。(実際にそういったツイートをたくさん拝見しております。)ですが、この手においてAWS-CDKを使用する利点は一点です。

「オフィシャルのサポートが手厚いからです」(日本語での対応、料金プランに応じて、迅速な対応、営業担当、事例公開での営業戦略)

といった「エンジニアはプロダクトだけに全集中しよう」(エンジニアが付加価値を生みにくい重労働から解放しようということに執念を見出している感じもしますが。)という後ろ盾があるだけでエンジニアはものを作りやすくなります。

エラーコードに対しては状況を説明すると親身になって回答いただけます。GCPではそもそも英語でないと難しいですし(日本語対応はプレミアム級の値段です)今の自分の能力を鑑みて「サービスを提供すると考えた場合、どのクラウドサービスを選ぶべきなのか?戦っていけるのか?」を考えるとAWSになります。

準備

最低限準備しないといけないのは2つあります。

  1. AWSのアカウント
  2. AWSのIAMユーザーのプロファイル情報(CLIで接続できるようにするもの)

AWSのアカウントは作ってくださいね!

IAMユーザーのプロファイルの設定についてはこちらのドキュメントを参考に分からなければTwitterのDMで質問を投げてください

このドキュメントがローカル環境にAWSのアカウント情報を設定するドキュメントになります

注意点として

AWSのアカウント管理は非常にナーバスになって取り扱った方が絶対にいいです。なので、今回試しにやってみて、途中でやめる場合は必ずIAMキーは「削除」してからログアウトしてください。

また予算アラートを必ず設定しておきましょう、どこでどのように情報が流出するかわかりません。ですので、アラートは設定しておくに越したことはありません。

それではアカウントができてIAMユーザーの設定が終わったものと仮定して次に進めていきますね。

AWS-CDKの環境構築

aws-cdkの準備をしていきます。(ここからはまずコマンドがたくさん出てきます。そのコマンドの整理をします

PCのグローバル環境(PCのどこからでも使えるように)にaws-cdkをインストールするコマンド

npm i -g aws-cdk

cdkを色々作っていくフォルダの作成と移動

mkdir cdk-workshop && cd cdk-workshop

cdkの初期化と言語選択(typescriptを選択します)

cdk init app --language typescript

cdkのコードからCloudFormationのテンプレートの展開

cdk synth

cdkのデプロイのための初期設定として必要(別のプロジェクトを作成するときは不要)

cdk bootstrap

リソースのデプロイ

cdk deploy

といったコマンドの意味があるというぐらいの感覚でいてもらえるといいのかなぁと、(この辺りは自分でも調べたり、サポートに質問しながら自分だけの理解を深めていく方がいいです)

さてまずはグローバル環境にaws-cdkをインストールします

npm i -g aws-cdk

次にcdkを管理するプロジェクトのフォルダを作成してそこに移動します。

mkdir cdk-workshop && cd cdk-workshop

基本的にフォルダを作ってそこに初期化宣言をするのが一連の流れになっていますね。

次に初期化宣言と言語選択をします

cdk init app --language typescript

ここでcdk init sample-appと宣言すると本当にサンプルの状態でのアプリケーションができてしまします。このappというのは初期状態をどの状態まで作成しますか?という意味合いのappになりますのでここに適当にプロジェクト名を入れようとするのはやめてくださいね。

次はcdkからcloudformationのテンプレートの展開を実施します

cdk synth

次にcdkを始めてデプロイする方向けのcloudformation templateのデプロイが必要になります(これは一度実施すれば同じアカウントであれば他にする必要ありません。

cdk bootstrap

最後にデプロイ作業を実施します

cdk deploy

ここまでの作業は必ず実際に内容を作っていく前までに終わらせておいてください。

イメージとしてはデプロイ環境が整っている状態で開発をして、デプロイした内容を修正していくといった感じの開発が絶対にいいです。

最後にデプロイ環境でつまづいた場合、「コードが原因なのか?」「設定が原因なのか?」判断がつかなくなります。

コードを書いていく

それでは実際にコードを書いていきましょう。

lib/cdk-workshop-stack.ts

このファイルには色々な設定を統合した状態が入ってきます。(ここの詳細な解説はこちらから)

今のところ覚えておいたらいいのは、このスタックがCloudFormationのテンプレートの一つのスタック(単位)になるということぐらいです。

import { Duration, Stack, StackProps } from 'aws-cdk-lib'import {IResource,LambdaIntegration,MockIntegration,PassthroughBehavior,RestApi,} from 'aws-cdk-lib/aws-apigateway'import { Runtime } from 'aws-cdk-lib/aws-lambda'import {NodejsFunction,NodejsFunctionProps,} from 'aws-cdk-lib/aws-lambda-nodejs'import { Construct } from 'constructs'import { join } from 'path'// import * as sqs from 'aws-cdk-lib/aws-sqs';export class CdkWorkshopStack extends Stack {constructor(scope: Construct, id: string, props?: StackProps) {super(scope, id, props)const nodeJsFunctionProps: NodejsFunctionProps = {bundling: {externalModules: ['aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime],},depsLockFilePath: join(__dirname, '../lambdas/', 'package-lock.json'),runtime: Runtime.NODEJS_14_X,timeout: Duration.seconds(10),}const createOneLambda = new NodejsFunction(this, 'createItemFunction', {entry: join(__dirname, '../lambdas', 'create.ts'),...nodeJsFunctionProps,})const createOneIntegration = new LambdaIntegration(createOneLambda)const api = new RestApi(this, 'itemsApi', {restApiName: 'Items Service',})const items = api.root.addResource('items')items.addMethod('POST', createOneIntegration)addCorsOptions(items)}}export function addCorsOptions(apiResource: IResource) {apiResource.addMethod('OPTIONS',new MockIntegration({integrationResponses: [{statusCode: '200',responseParameters: {'method.response.header.Access-Control-Allow-Headers':"'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",'method.response.header.Access-Control-Allow-Origin': "'*'",'method.response.header.Access-Control-Allow-Credentials':"'false'",'method.response.header.Access-Control-Allow-Methods':"'OPTIONS,GET,PUT,POST,DELETE'",},},],passthroughBehavior: PassthroughBehavior.NEVER,requestTemplates: {'application/json': '{"statusCode": 200}',},}),{methodResponses: [{statusCode: '200',responseParameters: {'method.response.header.Access-Control-Allow-Headers': true,'method.response.header.Access-Control-Allow-Methods': true,'method.response.header.Access-Control-Allow-Credentials': true,'method.response.header.Access-Control-Allow-Origin': true,},},],})}

コピーしていい具合にコードフォーマッターを使ってくださいね。

次にbin/cdk-workshop.ts

こちらが実際に展開するスタック一覧になります。

#!/usr/bin/env nodeimport 'source-map-support/register';import * as cdk from 'aws-cdk-lib';import { CdkWorkshopStack } from '../lib/cdk-workshop-stack';const app = new cdk.App();new CdkWorkshopStack(app, 'CdkWorkshopStack', {/* If you don't specify 'env', this stack will be environment-agnostic.* Account/Region-dependent features and context lookups will not work,* but a single synthesized template can be deployed anywhere. *//* Uncomment the next line to specialize this stack for the AWS Account* and Region that are implied by the current CLI configuration. */// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },/* Uncomment the next line if you know exactly what Account and Region you* want to deploy the stack to. */// env: { account: '123456789012', region: 'us-east-1' },/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */});

少ないでしょ。(この2つは1つに統合して作ることもできます。まぁコードが大きくなってきたから分割したという見立てが正しいかな?)

これで基本的なスタックの設定は完了しています。

次はLambdaの出番です。

まずLambdasというディレクトリを作成します。

mkdir lambdas && cd lambdas

そしてpackage.jsonを実施するため

npm init -y

を実施します。

package.jsonの中身を次のように変換

{"name": "lambdas","version": "1.0.0","description": "Lambdas to do CRUD operations on DynamoDB","private": true,"license": "MIT","devDependencies": {"@types/node": "*","@types/uuid": "*"},"dependencies": {"aws-sdk": "*","rxjs": "^7.5.5","symbol-sdk": "^2.0.0","uuid": "*"}}

次にtsconfig.jsonでtypescriptのルールを設定

touch tsconfig.json

中身

{"extends": "../tsconfig.json","include": ["*.ts"]}

この状態で

npm i

を実施します。

するとpackage.jsonで設定したファイルがインストールされていくといった流れです。

これでsymbol-sdkとrxjsが入った状態です。

lambdasの階層の下にcreate.tsファイルを作成して以下のコードをコピペ

touch create.ts

コード

import { lastValueFrom } from 'rxjs';import {Account,Address,Deadline,PlainMessage,RepositoryFactoryHttp,TransferTransaction,UInt64,} from 'symbol-sdk'export const handler = async (event: any = {}): Promise<any> => {console.log(event, 'event')// Network informationconst nodeUrl = 'https://sym-test.opening-line.jp:3001'const repositoryFactory = new RepositoryFactoryHttp(nodeUrl!)const epochAdjustment = repositoryFactory.getEpochAdjustment()const epochAdjustmentLastValue = await lastValueFrom(epochAdjustment)const networkType = repositoryFactory.getNetworkType()const networkTypeLastValue = await lastValueFrom(networkType)const networkGenerationHash = repositoryFactory.getGenerationHash()const networkGenerationHashLastValue = await lastValueFrom(networkGenerationHash)// Returns the network main currency, symbol.xymconst currency = repositoryFactory.getCurrencies()const currencyLastValue = await lastValueFrom(currency)const lastValueCurrency = currencyLastValue.currency/* start block 01 */// replace with recipient addressconst rawAddress = 'TATO6GDWZRAT6IY4HWN4QUHIB73KZSY2T3NXX5A'const recipientAddress = Address.createFromRawAddress(rawAddress)const transferTransaction = TransferTransaction.create(Deadline.create(epochAdjustmentLastValue!),recipientAddress,[lastValueCurrency.createRelative(10)],PlainMessage.create('This is a test message'),networkTypeLastValue!,UInt64.fromUint(2000000))/* end block 01 *//* start block 02 */// replace with sender private keyconst privateKey ='AB1B431B4E18653F67E8D4890CEF36382F63B4567E65A6CA4AA8A3D0A046A142'const account = Account.createFromPrivateKey(privateKey, networkTypeLastValue!)const signedTransaction = account.sign(transferTransaction,networkGenerationHashLastValue!)console.log('Payload:', signedTransaction.payload)console.log('Transaction Hash:', signedTransaction.hash)/* end block 02 *//* start block 03 */const transactionRepository = repositoryFactory.createTransactionRepository()const response = transactionRepository.announce(signedTransaction)const responseLastValue = await lastValueFrom(response)console.log(responseLastValue)}

symbolのプライベートキーやアドレスについては各々設定よろしくお願いします。

さて、この状態でデプロイをしましょう。

cdk deploy

これでlambda関数ができていればOKとなります。

image

あとはテストを実行して、トランザクションが送金されればOKです。

と、今日はここまでです。

ここまでできると勘のいい人たちなら何ができるかすぐにわかると思うのですが、「カスタマイズ可能なAPIが作成することができます」といった感じになります。

個人的にはこの有用性はかなりいいかなぁと思います。

お疲れ様でした。

また詳しい話が聞きたい場合はTwitterやMENTAでもお受けしておりますので、お気軽にご相談ください。