助かりました。
libcのバージョン違いだそうで。
可能ならcgoを無効にするとよいらしい。
CGO_ENABLED=0 go build
デフォルトでcgoがリンクされる理由はよくわかってない。知りたいところ。
助かりました。
libcのバージョン違いだそうで。
可能ならcgoを無効にするとよいらしい。
CGO_ENABLED=0 go build
デフォルトでcgoがリンクされる理由はよくわかってない。知りたいところ。
あまりないことではあったけど、別プロセスのvim間でコピペしたくなった。
ちょうどよいプラグインがあったので。
GitHubはこっちにあった。
tmuxとかscreen使ってると割と便利かも。
実装としては仮ファイルに書き出して読みだすだけというシンプルなもの。
こういう単純で役立つやつっていいよね。
プラグインマネージャとかなくてもパッと入れたいときのために。
mkdir -p ~/.vim/plugin cd ~/.vim/plugin curl -OL https://raw.githubusercontent.com/vim-scripts/yanktmp.vim/master/plugin/yanktmp.vim
.vimrcにはドキュメント通りこんな感じで追記する。
ドキュメントはsを使っているけど、割と使うのでtが好みだった。
map <silent> ty :call YanktmpYank()<CR> map <silent> tp :call YanktmpPaste_p()<CR> map <silent> tP :call YanktmpPaste_P()<CR>
あんまり不便に感じてこなかったけど、最近感じるようになったので。
WSLのUbuntuを使っているんだけど、以下で入れたvimだとクリップボードに対応していない。
sudo apt install vim
確認は以下で。-になってたら未対応。
$ vim --version | grep clipboard -clipboard +keymap +printer +vertsplit +eval -mouse_jsbterm -sun_workshop -xterm_clipboard
仕方ないので対応版を入れる。ビルドは面倒だからしたくない。
参考にした以下曰く、対応版のパッケージが結構あるっぽい。
MY ROBOTICS - Vimでクリップボードからのペーストを可能にする
gtk版を入れることにした。gvim使わないけどあると便利になるのかも。
sudo apt-get install vim-gtk
確認しておく。
$ vim --version | grep clipboard +clipboard +keymap +printer +vertsplit +eval -mouse_jsbterm -sun_workshop +xterm_clipboard
vimrcに以下を加える。
set clipboard& set clipboard^=unnamedplus
設定説明は以下がとても親切だった。
事情があってSAMを学ぶ必要があったので。
10分だけログの残る、curlコマンドで使える掲示板を作ることにします。
使うものとしてはだいたい以下です(細かいのは省く)
EventBridgeは大体10分ごとにログを消すのに使います。
WAFは使い方を知っとく必要があったので強引に含めました。
リポジトリは以下
twinbird/aws-sam-chat-app - GitHub
言語はTypeScriptを使います。
AWSの公式ドキュメントの以下のページに従ってインストールします。
ルートユーザーで新しいユーザーを作成します。
以下のユーザー名でコンソール画面から作成。
limited-chat-dev-user
コンソールアクセスは許可しない。 「ポリシーを直接アタッチする」を選択して以下を追加。
AWSCloudFormationFullAccess IAMFullAccess AWSLambda_FullAccess AmazonAPIGatewayAdministrator AmazonS3FullAccess AmazonEC2ContainerRegistryFullAccess
今回はDynamoDBも使いたいので以下も加えます。
AmazonDynamoDBFullAccess
あとはEventBridgeと
AmazonEventBridgeFullAccess
WAFも使うので。
AWSWAFFullAccess
以下を参考にした。
AWS SAM 開発者ガイド - AWS CloudFormation メカニズムによる権限の管理
(IAM Identity Centerが推奨されている様子だが、諸事情でここではこちらを採用)
IAMから作成したユーザーを選択し、「セキュリティ認証情報」のタブを選択します。
「アクセスキー」から「アクセスキーを作成」を選択します。
コマンドラインインターフェース(CLI)を選択します。
認証情報はCSVダウンロードしておきます。
aws configure --profile limited-chat-dev
先ほど作成したアクセスキーを使います。 regionはap-northeast-1を利用しました。
初期化用の以下のコマンドを使ってプロジェクトを作成します。
sam init
対話式で設定できますが、結構長いです。
で作成しました。とりあえず全部載せます。
$ sam init You can preselect a particular runtime or package type when using the `sam init` experience. Call `sam init --help` to learn more. Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 Choose an AWS Quick Start application template 1 - Hello World Example 2 - Data processing 3 - Hello World Example with Powertools for AWS Lambda 4 - Multi-step workflow 5 - Scheduled task 6 - Standalone function 7 - Serverless API 8 - Infrastructure event management 9 - Lambda Response Streaming 10 - Serverless Connector Hello World Example 11 - Multi-step workflow with Connectors 12 - GraphQLApi Hello World Example 13 - Full Stack 14 - Lambda EFS example 15 - Hello World Example With Powertools for AWS Lambda 16 - DynamoDB Example 17 - Machine Learning Template: 1 Use the most popular runtime and package type? (Python and zip) [y/N]: N Which runtime would you like to use? 1 - aot.dotnet7 (provided.al2) 2 - dotnet8 3 - dotnet6 4 - go1.x 5 - go (provided.al2) 6 - go (provided.al2023) 7 - graalvm.java11 (provided.al2) 8 - graalvm.java17 (provided.al2) 9 - java21 10 - java17 11 - java11 12 - java8.al2 13 - nodejs20.x 14 - nodejs18.x 15 - nodejs16.x 16 - python3.9 17 - python3.8 18 - python3.12 19 - python3.11 20 - python3.10 21 - ruby3.2 22 - rust (provided.al2) 23 - rust (provided.al2023) Runtime: 13 What package type would you like to use? 1 - Zip 2 - Image Package type: 1 Based on your selections, the only dependency manager available is npm. We will proceed copying the template using npm. Select your starter template 1 - Hello World Example 2 - Hello World Example TypeScript Template: 2 Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: Would you like to enable monitoring using CloudWatch Application Insights? For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: Would you like to set Structured Logging in JSON format on your Lambda functions? [y/N]: Project name [sam-app]: limited-chat-app ----------------------- Generating application: ----------------------- Name: limited-chat-app Runtime: nodejs20.x Architectures: x86_64 Dependency Manager: npm Application Template: hello-world-typescript Output Directory: . Configuration file: limited-chat-app/samconfig.toml Next steps can be found in the README file at limited-chat-app/README.md Commands you can use next ========================= [*] Create pipeline: cd limited-chat-app && sam pipeline init --bootstrap [*] Validate SAM template: cd limited-chat-app && sam validate [*] Test Function in the Cloud: cd limited-chat-app && sam sync --stack-name {stack-name} --watch
ここまででビルドしてデプロイしてみます。
先ほど作成したプロジェクトディレクトリへ移動します。
cd limited-chat-app
まずは以下のコマンドでテンプレートファイルが正しいか確認できるようです。
$ sam validate 【ディレクトリ名】/limited-chat-app/template.yaml is a valid SAM Template
続いてビルドしてみます。
sam build ...【色々出る】 Build Succeeded ...【色々出る】
このままデプロイします。
sam deploy --guided --profile limited-chat-dev Configuring SAM deploy ====================== Looking for config file [samconfig.toml] : Found Reading default arguments : Success Setting default arguments for 'sam deploy' ========================================= Stack Name [limited-chat-app]: AWS Region [ap-northeast-1]: #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [Y/n]: Y #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: Y #Preserves the state of previously provisioned resources when an operation fails Disable rollback [y/N]: y HelloWorldFunction has no authentication. Is this okay? [y/N]: y Save arguments to configuration file [Y/n]: Y SAM configuration file [samconfig.toml]: SAM configuration environment [default]: 【以下略】
コンソールの最後に出るAPI GatewayのURLにアクセスして、JSONでHello WorldされればOKです。
とりあえずdeployしてみたので一度リソースを消します。
$ sam delete --profile limited-chat-dev Are you sure you want to delete the stack limited-chat-app in the region ap-northeast-1 ? [y/N]: y Are you sure you want to delete the folder limited-chat-app in S3 which contains the artifacts? [y/N]: y 【以下略】
template.yamlを見ると色々な箇所にHelloWorldが存在するので全てPostMessageへ置き換えていきます。
ディレクトリ名(hello-world)やnpmのpackage.jsonも変えてみます。
template.yamlを編集します。
Resources以下へ以下を追加します。
ChatTable: Type: AWS::DynamoDB::Table Properties: TableName: ChatTable AttributeDefinitions: - AttributeName: uuid AttributeType: S KeySchema: - AttributeName: uuid KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 10 WriteCapacityUnits: 10
DynamoDBへのアクセスにライブラリを使うので、post-messageディレクトリへ移動して以下でライブラリを導入します。
cd post-message npm i --save-dev @aws-sdk/client-dynamodb npm i --save-dev @aws-sdk/lib-dynamodb
使い方はこちらが参考になります。 SDK for JavaScript (v3) を使用した DynamoDB の例 DynamoDB での単一項目の読み取りと書き込み
このような感じでコードを書いて
/** * メッセージをデータベースへ保存する */ const storePost = async (message: string): string => { const client = new DynamoDBClient({}); const docClient = DynamoDBDocumentClient.from(client); const uuid = crypto.randomUUID(); const now = new Date(); const createdAt = now.toLocaleString('ja-JP', { timeZone: 'UTC' }); const command = new PutItemCommand({ TableName: 'ChatTable', Item: { uuid: { S: uuid }, createdAt: { S: createdAt }, message: { S: message }, }, }); const response = await client.send(command); return uuid; };
以下でビルドします。
sam build
こんな感じでESbuildのエラーメッセージが出ました。
Esbuild Failed: ✘ [ERROR] Could not resolve "@aws-sdk/client-dynamodb"
以下を参考に解決しています。
AWS SDK V3 + Lambda を SAM deploy した際の "Could not resolve" の対処法
template.yamlを以下のように変更しました。
Externalの箇所が必要なようです。Bundleするなと指示が必要らしい。
Metadata: # Manage esbuild properties BuildMethod: esbuild BuildProperties: Minify: true Target: "es2020" Sourcemap: true EntryPoints: - app.ts External: - "@aws-sdk/lib-dynamodb" - "@aws-sdk/client-dynamodb"
これでsam buildが通ります。
デプロイしてみます。
2度目なので以下だけでOKです。
sam deploy --profile limited-chat-dev
curlで試してみます。
curl -X POST 【APIゲートウェイのエンドポイントURL】 -H "Content-Type: application/json" -d '{ "message" : "Hello world" }'
開発中はエラーが起きると思うので、以下で確認できます。
sam logs --region=ap-northeast-1 --profile limited-chat-dev
あるいはブラウザでCloudWatchをコンソールから見る方がわかりやすいかも。
上記までで試すと以下のようなエラーが出ていました。
INFO AccessDeniedException: User: arn:【略】 is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:【略】 because no identity-based policy allows the dynamodb:PutItem action
権限をlambdaに与えてあげる必要があるようです。
template.yamlを書き換えます。
PostMessageFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-appli cation-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 17 Properties: CodeUri: post-message/ Handler: app.lambdaHandler Runtime: nodejs20.x Policies: AmazonDynamoDBFullAccess # 追加
再度ビルドとデプロイし、curlでアクセスしてみます。
ここまでで投稿用のAPIができました。
まず、lambda関数を加えます。
ディレクトリを作成します。 mkdir list-messages
簡単なサンプル関数をまずは書きます。
/** * API成功時のレスポンスを返す */ const successResponse = (): Response => { return { statusCode: 200, body: JSON.stringify({ message: 'success', }), }; }; /** * エラー発生時のレスポンスを返す */ const errorResponse = (): Response => { return { statusCode: 500, body: JSON.stringify({ message: 'some error happened', }), }; }; /** * メッセージ取得API * * Request Bodyに以下の形式のJSONが含まれることを期待します。 * { message: 'チャット投稿用メッセージ' } */ export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => { try { return successResponse(); } catch (err) { console.log(err); return errorResponse(); } };
設定ファイルはsam initで作成した最初の関数からコピーしてきました。
cp -a post-message/package.json list-messages/ cp -a post-message/tsconfig.json list-messages/ cp -a post-message/jest.config.ts list-messages/
一応package.jsonのnameとdescriptionくらいは書き換えておきます。
依存モジュールをインストールしておきます。
cd list-messages npm i
template.yamlを変更します。
これもコピペです。
ListMessagesFunction: Type: AWS::Serverless::Function Properties: CodeUri: list-messages/ Handler: app.lambdaHandler Runtime: nodejs20.x Policies: AmazonDynamoDBFullAccess Architectures: - x86_64 Events: PostMessage: Type: Api Properties: Path: /listMessages Method: get Metadata: BuildMethod: esbuild BuildProperties: Minify: true Target: "es2020" Sourcemap: true EntryPoints: - app.ts External: - "@aws-sdk/lib-dynamodb" - "@aws-sdk/client-dynamodb"
ビルドします。
sam build
ビルドが通ったのでデプロイしてみます。
sam deploy --profile limited-chat-dev
新しく追加したAPI GatewayのURLにアクセスするとレスポンスが返ってきます。
とりあえず関数の追加はできたようです。
コードを変更します。こんな感じで読み出し用の関数を用意して返す感じで。
/** * メッセージをデータベースから取り出す */ const fetchPosts = async (): [Post] => { const client = new DynamoDBClient({}); const docClient = DynamoDBDocumentClient.from(client); const command = new ScanCommand({ TableName: 'ChatTable', }); const data = await docClient.send(command); console.log(data); return data.Items.map((m) => { return { createdAt: m.createdAt, message: m.message, }; }); }
もはやクドイですが、ビルドしてデプロイします。
sam build sam deploy --profile limited-chat-dev
curlで試してみます。
curl 【取得関数のURL】
一覧が取れました。
jqを使えばちょっとそれっぽく見えるかも。
curl 【取得関数のURL】 | jq '.posts[] | .createdAt + " " + .message'
脱線する話題だけど、jqのパイプを初めて使った。便利だった。
掲示板なので昇順でも降順でも良いから並べたいです。
が、DynamoDBの設計がよくわかってなかったのでこのままだと上手くソートできない感じ。
実務ならできないだろうけど、まぁお遊びだからええやろということでLambdaでソートしてやることに。
先に書いておくと、DynamoDB自体にはTTL機能というものがあるので、本来こんな機能は実装の必要がありません。
あくまでも学習用のお試しです。
DynamoDBの各データを自動で削除する機能(TTL:Time to Live)を試してみた - Developers IO
AWS::DynamoDB::Table TimeToLiveSpecification - AWS
さて、最終的にはEventBridgeで定期実行したいですが、とりあえずLambdaでバッチ削除できる関数を作ります。
ディレクトリを作成します。 mkdir delete-old-messages
前回同様設定ファイルはsam initで作成した最初の関数からコピーしてきました。
cp -a post-message/package.json delete-old-messages/ cp -a post-message/tsconfig.json delete-old-messages/ cp -a post-message/jest.config.ts delete-old-messages/
package.jsonのnameとdescriptionは書き換えておきます。
次に関数を書いていきます。
vi delete-old-messages/app.ts
SDK使う部分主要なだけ抜き出すとこんな感じ。
/** * 引数の投稿を削除する */ const deletePost = async (post: Post) => { const command = new DeleteCommand({ TableName: 'ChatTable', Key: { uuid: post.uuid, }, }); await docClient.send(command); };
template.yamlも編集します。
API Gatewayと紐づける必要はないので、以下を参考にEvents以下は削除しました。
AWS SAM Lambda関数だけを単体でデプロイしたいときのテンプレート設定例
DeleteOldMessagesFunction: Type: AWS::Serverless::Function Properties: CodeUri: delete-old-messages/ Handler: app.lambdaHandler Runtime: nodejs20.x Policies: AmazonDynamoDBFullAccess Architectures: - x86_64 Metadata: BuildMethod: esbuild BuildProperties: Minify: true Target: "es2020" Sourcemap: true EntryPoints: - app.ts External: - "@aws-sdk/lib-dynamodb" - "@aws-sdk/client-dynamodb"
とりあえず実行は管理コンソールから。一応動作しているようです。
ここまで来て今さら気づいたのが、文字列をISO 8601形式にしておけば日付をQueryで絞り込めるのではということ。
ということで変えてみました。
- const createdAt = now.toLocaleString('ja-JP', { timeZone: 'UTC' }); + const createdAt = now.toISOString();
filterの方法はドキュメントを読んでそれっぽく書いてみました。
AWS SDK for JavaScript v3 - ScanCommand
Filterの書き方を間違えていて、何度やっても以下のエラーが出ていたけど
Invalid FilterExpression: Incorrect operand type for operator or function; operator or function: <, operand type: M
StackOverflowに救われて進めた。
DynamoDB: Query Incorrect operand type
DynamoDBのドキュメントをあれこれ読んでいるとこんなのを見つけた。
あー、なんかこんな制約あった気がする。
この辺りを真似する。
How to paginate dynamodb results using aws sdk sendCommand - Stack Overflow
/** * メッセージをデータベースから取り出す */ const fetchPosts = async (): [Post] => { const client = new DynamoDBClient({}); const docClient = DynamoDBDocumentClient.from(client); let data = []; const getChunk = async (key: string): string => { const command = new ScanCommand({ TableName: 'ChatTable', Limit: 10, ExclusiveStartKey: key, }); const result = await docClient.send(command); data.push(...result.Items); return result.LastEvaluatedKey; }; let key; do { key = await getChunk(key); } while (key); return data; }
しかしキャパシティユニット無駄に使うなこれ。Query使いたいが。
簡潔でありがたい。
【AWS SAM】EventBridgeでLambdaを定期実行するテンプレートの書き方
詳細はここを見ればよさそう。
AWS Serverless Application Model 開発者ガイド - ScheduleV2
なんか「年」のフィールドがある。cronにあったっけ?
あとこんなことも書かれているし、微妙にcronとは違う気がする。
cron 式の日フィールドと曜日フィールドを同時に指定することはできません。一方のフィールドに値または * (アスタリスク) を指定する場合、もう一方のフィールドで ? (疑問符) を使用する必要があります。
とりあえず以下のように書いてみた。
DeleteOldMessagesFunction: Type: AWS::Serverless::Function Properties: CodeUri: delete-old-messages/ Handler: app.lambdaHandler Runtime: nodejs20.x Policies: AmazonDynamoDBFullAccess Architectures: - x86_64 Events: ScheduledFunction: Type: ScheduleV2 Properties: ScheduleExpression: cron(0/10 * * * ? *) ScheduleExpressionTimezone: "Asia/Tokyo" State: ENABLED Name: limited-chat-app-delete-old-messages-function-schedule
あと、deployにあたって以下のポリシーがないとダメだった。そりゃそうだ。
AmazonEventBridgeFullAccess
これは使うケースが少なそうなんですが、諸事情で必要なので試してみます。
簡単な同一IPからのRate Limitをかけたい。
とりあえず使ったこともないのでコンソールから試してみる。
AWS WAF でアクセス数が一定回数を超えた IP アドレスを自動的にブラックリストに追加させる方法
一回作ったらコンソールからJSONがダウンロードできたので、ダウンロードして削除した。
{ "Name": "test-waf", "Id": "【IDが入ってた】", "ARN": "【ARNが入ってた】", "DefaultAction": { "Allow": {} }, "Description": "", "Rules": [ { "Name": "rate-limit-rule-100-request-per-5-minutes", "Priority": 0, "Statement": { "RateBasedStatement": { "Limit": 100, "EvaluationWindowSec": 300, "AggregateKeyType": "IP" } }, "Action": { "Block": {} }, "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": false, "MetricName": "rate-limit-rule-100-request-per-5-minutes" } } ], "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "test-waf" }, "Capacity": 2, "ManagedByFirewallManager": false, "LabelNamespace": "awswaf:381491890434:webacl:test-waf:" }
次にSAMで設定したい。
SAMでネイティブにサポートされているわけではないのかな。でもYAML書けば使えそうではある。
CloudFormationでググった方が情報出てくるかも。とググってると以下が出てきた。
参考にしつつ以下のtemplate.yamlへ書く。
API Gatewayの記述をこんな感じで分離しないとRefが取れないので面倒くさい。
Resources: PostMessageApi: Type: AWS::Serverless::Api Properties: StageName: Prod PostMessageFunction: Type: AWS::Serverless::Function Properties: CodeUri: post-message/ Handler: app.lambdaHandler Runtime: nodejs20.x Policies: AmazonDynamoDBFullAccess Architectures: - x86_64 Events: PostMessage: Type: Api Properties: Path: /postMessage Method: post RestApiId: Ref: PostMessageApi
WAFの記述はマジでわからん。特にAssociationsの部分はGitHubのIssueほぼ丸パクリ。
ドキュメントもどこ見ればよいのかサッパリや。。。
(そもそも元のGitHubの人もめちゃ苦労してそう)
LimitedChatAppApiWebAcl: Type: AWS::WAFv2::WebACL Properties: DefaultAction: Allow: {} Scope: REGIONAL Rules: - Action: Block: {} Name: rate-limit-rule-100-request-per-5-minutes Priority: 0 Statement: RateBasedStatement: Limit: 100 AggregateKeyType: IP VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: false MetricName: rate-limit-rule-100-request-per-5-minutes VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: limited-chat-app-waf-acl PostApiWebAclLimitedChatAppAssociations: Type: AWS::WAFv2::WebACLAssociation DependsOn: PostMessageApiProdStage Properties: WebACLArn: !GetAtt LimitedChatAppApiWebAcl.Arn ResourceArn: !Sub 'arn:aws:apigateway:${AWS::Region}::/restapis/${PostMessageApi}/stages/Prod'
DependsOnを入れてやらないとAPI Gatewayを作る前にWAFを設定しようとして死ぬ。
参考ページ曰く、命名規則があるらしく
らしい。ありがたや。
AWS SAM で Amazon API Gateway の Usage Plan を作るときに API Stage not found と出たら - kakakakakku blog
こっちは同じ問題をCDKで対応された方っぽい。エラーが一緒だった。
AWS WAF couldn?t perform the operation because your resource doesn?t exist.
[AWS CDK] APIGateway+WAFv2 Web ACL構成でデプロイしようとしてちょっとハマった点 - Zenn
実行時には権限が必要なのでとりあえず以下を追加した。
AWSWAFFullAccess
Lambda内では以下のように環境変数を取得できる。
let region = process.env.AWS_REGION
で、ローカル実行時にはパラメータを渡すと環境変数を上書きできるらしい。
deploy時にも変えたいんだよね。こちらを参考にしてみる。
AWS Lambdaで使いたい環境変数をAWS SAM CLIでどうするか
こんな感じでtemplate.yamlを書いて。
# 環境変数用に用意 Parameters: Secret: Type: String Resources: PostMessageApi: Type: AWS::Serverless::Api Properties: StageName: Prod PostMessageFunction: Type: AWS::Serverless::Function Properties: CodeUri: post-message/ Handler: app.lambdaHandler Runtime: nodejs20.x Policies: AmazonDynamoDBFullAccess Architectures: - x86_64 Environment: Variables: SECRET: !Ref Secret Events: PostMessage: Type: Api Properties: Path: /postMessage Method: post RestApiId: Ref: PostMessageApi
こんな感じでデプロイする。
sam deploy --profile limited-chat-dev --parameter-overrides Secret="THIS_IS_A_SECRET"
そうするとLambdaでこうできる。
let secret = process.env.SECRET
試しにレスポンスに含めてみたら取得できた。全然SECRETじゃないが。。。
ここまで来て?という感じだけどローカルでDynamoDBが動かせるらしいので。
コンピュータ上で DynamoDB をローカルでデプロイする
公式の指示通り以下をdocker-compose.yamlとして保存して
version: '3.8' services: dynamodb-local: command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" image: "amazon/dynamodb-local:latest" container_name: dynamodb-local ports: - "8000:8000" volumes: - "./docker/dynamodb:/home/dynamodblocal/data" working_dir: /home/dynamodblocal
データ保存先のディレクトリを用意してやる。
これをうっかりやらないとコンテナ側で書き込み権限がないので「cannot open DB[]: com.almworks.sqlite4java.SQLiteException: [] unable to open database file」とかエラーを吐く。
mkdir -p docker/dynamodb chmod 777 docker/dynamodb
ふつーに起動する。
docker compose up -d
とりあえずawsコマンドでつないでみる。
aws dynamodb list-tables --profile limited-chat-dev --endpoint-url http://localhost:8000
結果が返ってきたらとりあえずOKだろう。
テーブルは自分で用意してやらないといけないのでこんな感じでCLIで用意した。
aws dynamodb create-table \ --table-name ChatTable \ --attribute-definitions AttributeName=uuid,AttributeType=S \ --key-schema AttributeName=uuid,KeyType=HASH \ --provisioned-throughput ReadCapacityUnits=25,WriteCapacityUnits=25 \ --profile limited-chat-dev \ --endpoint-url http://localhost:8000
この辺りを参考にして環境変数で切り替えられるようにする。
AWS SAM+DynamoDBのDockerローカル開発環境を作る - Zenn まくろぐ - DynamoDB を Node.js で操作する(SDK ver.3 の場合)
まずはアプリ側でDynamoDBへ接続している個所を環境変数で切り替えられるようにする。
const config: DynamoDBClientConfig = { endpoint: process.env.DYNAMODB_ENDPOINT, }; const client = new DynamoDBClient(process.env.DYNAMODB_ENDPOINT ? config : {});
template.yamlも同じように変える。
# 環境変数用に用意 Parameters: Secret: Type: String DynamoDbEndpoint: Type: String 【中略】 PostMessageFunction: Type: AWS::Serverless::Function Properties: CodeUri: post-message/ Handler: app.lambdaHandler Runtime: nodejs20.x Policies: AmazonDynamoDBFullAccess Architectures: - x86_64 Environment: Variables: DYNAMODB_ENDPOINT: !Ref DynamoDbEndpoint
参考サイトだとsam localで動く時には環境変数のAWS_SAM_LOCAL
がTrueになるそうなので、その辺を使ってもよさそう。
で、以下のように実行する。
sam local invoke ListMessagesFunction --parameter-overrides DynamoDbEndpoint="http://dynamodb-local:8000" --docker-network="limited-chat-app_default"
DynamoDbEndpointは環境変数に渡すURLだけど、これはdocker-composeで用意したホスト名で「dynamodb-local」を渡した。
--docker-networkにもdocker-composeで用意したネットワークをそのまま渡している。(CLIで表示されたもの)
sam local invoke
もdockerで動くっぽいのでlocalhostとか指定しても動かない。
環境変数はこんな感じでファイルで定義もできるらしい。
あとはAPI Gatewayとかもエミュレートできるっぽいので試す。
sam local start-api --parameter-overrides DynamoDbEndpoint="http://dynamodb-local:8000" --docker-network="limited-chat-app_default"
これでサーバーが動くので、あとはcurlとかで試せる。
curl -X POST http://localhost:3000/postMessage/ -H "Content-Type: application/json" -d '{ "message" : "Hello, sam local" }' curl http://localhost:3000/listMessages/ | jq '.posts[] | .createdAt + " " + .message'
sam deploy
するときにはこんな感じにするのでOKだった。
sam deploy --profile limited-chat-dev --parameter-overrides='Secret="THIS_IS_A_SECRET" DynamoDbEndpoint=""'
他にも色々できそうなので、あとは必要に応じて。
ここまで書いてきて今さらなんだけどテストが欲しい。
一応公式がこういうページを出しているので結合テストにsam localを使うのは想定されているらしい。
E2Eのテストは上記までで作った環境で作れそうな気がする。こんな感じかな。
何かいい情報ないのかと思ったら公式があった。
リンク先にTypescriptのテストサンプルもあった。
aws-samples/serverless-test-samples | TypeScript Test Samples - GitHub
API Gateway + Lambda + DynamoDBというまさにそのままのパターンのコードがあったので見てみる。
Unitテストの方はaws-sdk-client-mockを使ってテストしている。
テストのパターンとしてもSAMのQuickStartで作成されたものと同じで、aws-lambdaライブラリのAPIGatewayProxyEventとAPIGatewayProxyResultを関数ハンドラに渡して結果を見るというものだ。
integrationテストの方はもっと直接的で、DynamoDBへつないでデータを投入して、Axiosで直接APIを叩いて試している。
どうやってDynamoDBの接続情報を取得しているのか疑問だったけど、ライブラリは現在の環境のAWSプロファイルを使うらしい。
環境変数AWS_PROFILE
を使って明示してやればうまいこと動作しそう。
というわけで簡単なテストを用意してみる。
postMessageをテストしてみる。
とりあえずmockライブラリを導入.
cd post-message npm install -D aws-sdk-client-mock
簡単なテストコードを用意する。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import { lambdaHandler } from '../../app'; import { expect, describe, it } from '@jest/globals'; import { mockClient } from "aws-sdk-client-mock"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { PutItemCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb"; // 元々あったeventのJSON const postMessageEvent: APIGatewayProxyEvent = { httpMethod: 'post', body: '{ "message": "test message" }', 【略】 }; const ddbMock = mockClient(DynamoDBClient); beforeEach(() => { ddbMock.reset(); }); describe('Post message API Tests', function () { it('valid parameter post', async () => { process.env.DYNAMODB_TABLE_NAME = 'unit_test_dynamodb_table'; ddbMock.onAnyCommand().resolves({}); // 全Command に対して定義 ddbMock.on(PutItemCommand).resolves({ '$metadata': { httpStatusCode: 200, requestId: 'e230fba2-732b-4f44-a0f2-ddca742f63fe', extendedRequestId: undefined, cfId: undefined, attempts: 1, totalRetryDelay: 0 }, Attributes: undefined, ConsumedCapacity: undefined, ItemCollectionMetrics: undefined }); const expectMessage = "TEST_MESSAGE"; postMessageEvent.body = JSON.stringify({ message: expectMessage }); const result: APIGatewayProxyResult = await lambdaHandler(postMessageEvent); expect(result.statusCode).toEqual(200); const resultObj = JSON.parse(result.body); expect(resultObj.message).toEqual('success'); expect(resultObj.postMessage).toEqual(expectMessage); expect(resultObj.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i); // UUID }); });
実行
npm run test
結構簡単に試せる。
ただ、mockを作るオブジェクトを間違えて1時間以上ハマった。
// サンプルコードの例 const ddbMock = mockClient(DynamoDBDocumentClient); // 実際に試した例 const ddbMock = mockClient(DynamoDBClient);
あと、eventsディレクトリが最上位階層にあったのに気付かなかった。
ここにeventを書いておいてテストからは読み込んで使う方法の方がよいのかもしれない。
基本的に直接APIを使ってクラウド側の動作確認をするだけなので、jsのfetchやawsのライブラリの知識さえあれば書ける。
ここのサンプルの通りunitテストとintegrationテストで実行を分けれるようにしておいた方が良いと思う。
雑だけどこんな感じで書いて
import { GetItemCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb"; describe('Post message API integration tests', () => { let baseApiUrl: string; beforeAll(() => { if (!process.env.API_URL) { throw new Error('API_URL environment variable is not set.'); } baseApiUrl = process.env.API_URL; }); describe('Post valid message', () => { it('post 1 char message', async () => { const data = { message: '1', }; const url = `${baseApiUrl}/postMessage`; // Postリクエストしてみてレスポンスを確認する const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const body = await res.json(); expect(body.message).toBe('success'); expect(body.postMessage).toBe('1'); const id = body.id; // DynamoDBに問い合わせて確認する const client = new DynamoDBClient({}); const command = new GetItemCommand({ TableName: 'ChatTable', Key: { uuid: { S: id }, }, }); const dynamoRes = await client.send(command); expect(dynamoRes?.Item?.uuid?.S).toBe(id); }); }); });
実行時にプロファイルやテスト環境のURLを指定して動かす感じにした。
AWS_PROFILE="limited-chat-dev" API_URL="xxxxxxx/Prod" npm run test
バグらしい。たしかにApiの個所を追記するとstageが消えた。
Globals: Function: Timeout: 3 Api: OpenApiVersion: 3.0.2
AWS SAMで「Stage」ステージが作られるバグを回避する
以下のコマンドでtemplate.yamlを検証できる。
sam validate
これで作って
いまさら聞けないWindows 10のTips「Hyper-V」の“クイック作成”でWindows環境を作成する際に押さえておきたい3つの注意点 - 窓の杜
これで日本語化した
いまさら聞けないWindows 10のTips「Hyper-V」の“クイック作成”で展開したWindows 10を日本語化しよう - 窓の杜
なんか日本語化してもちょっとおかしくなったりするけど、再起動するといい感じになる。
90日の開発ライセンスだけど、環境汚さず別環境が必要な時とか便利。
bashはコマンド履歴を検索できる。便利。
キー | 意味 |
---|---|
ctrl + r | 履歴を後方に検索 |
ctrl + g | 検索を中止 |
ctrl + s | 履歴を前方に検索 |
ただ、ctrl+sはデフォルトで端末の出力を止めてしまうので初期状態では使えない。
ということで以下の設定を行う。
stty stop undef
これを.profileとか.bash_profileとかに書いておく。
ctrl+sとctrl+qはマジで過去の遺物感があるな。
PHPで以下のようにするとHTTPのbodyをそのまま見ることができる。
$postBody = file_get_contents("php://input");
なんだけど、multipart/form-dataの場合にはこの手が使えないらしい。
過去には$HTTP_RAW_POST_DATAという変数があったらしいが削除されたっぽい。
困っていたのだが、enable_post_data_reading というフラグはまだ生きているらしい。
PHP - コア php.ini ディレクティブに関する説明
apacheを使っていれば以下のように.htaccessに書いてやれば試すことができる。
php_flag enable_post_data_reading Off