Progress of a2 (original) (raw)

runnは、HTTPリクエストのシナリオテストに便利なツールです。CLI として使用することもできますが、Go のテストコード内で runn を使用することで、より柔軟で強力なテストが可能になります。

実際のサーバーに対してリクエストを行おうとすると、正常なリクエストだと判定させるために authentication token を用意したり、 middleware にリクエストが弾かれないような workaround が必要なことがあります。単にリソースサーバーの endpoint に対してシナリオテストを実行したい場合、runn を Go のテストヘルパーとして使うことで、 application 内の router 部分に対してのみテストを実行できます(図)。

runnの公式サイトには多くの情報が掲載されていますが、 機能が豊富な分、具体的な実装を完全な形で見つけるのが少々骨です。実際に動かすことで、テストヘルパーとしての runn の使い方を理解したので、書いておきます。

実装方法

runn を Go のテストヘルパーとして使用する際の重要なポイントは、clientを外部から渡し、runbook内で定義しないことです。

CLI tool として runn の参考文献を勉強していくと、以下のように runner を定義する形で yml ファイルを書く方法に慣れます。

runners: req: http://localhost:8080 steps:

しかし、httptestserver などを用いる場合、runner は runn をコード上で呼び出す際に option で渡すため、yml ファイル内の定義は消しておくべきです

ts := httptest.NewServer(NewRouter()) t.Cleanup(func() { ts.Close() })

opts := []runn.Option{ runn.T(t), runn.Runner("req", ts.URL), # ここで runner を渡す }

runners:

req: http://localhost:8080 <-- yml ファイル内では定義しない

steps:

実装例

以下が、動作するコードの全体です

main.go

package main

import ( "fmt" "log" "net/http" )

func main() { mux := NewRouter()

fmt.Println("Server is running on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))

}

func NewRouter() http.Handler { mux := http.NewServeMux()

// GETメソッドの処理
mux.HandleFunc("GET /resource", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "GET request received")
})

return mux

}

main_test.go

package main

import ( "context" "net/http/httptest" "testing"

"github.com/k1LoW/runn"

)

func TestSample(t *testing.T) {

ctx := context.Background()
ts := httptest.NewServer(NewRouter())
t.Cleanup(func() {
    ts.Close()
})

opts := []runn.Option{
    runn.T(t),
    runn.Runner("req", ts.URL),
}
o, err := runn.Load("*.yml", opts...)
if err != nil {
    t.Fatal(err)
}
if err := o.RunN(ctx); err != nil {
    t.Fatal(err)
}

}

get_resource.yml

desc: resource を GET する

runners:

req: http://localhost:8080

steps:

casbin を使って web application の認可を行おうとする場合、使い方の例が少なく、混乱する。

基本

How It Works | Casbin

このページの例を少し広げると、以下のような設定になる

import "github.com/casbin/casbin/v2"

e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")

model.conf

[request_definition] r = sub, obj, act

[policy_definition] p = sub, obj, act

[policy_effect] e = some(where (p.eft == allow))

[matchers] m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

#policy.csv p, admin, data1, read p, admin, data1, write p, admin, data2, read p, admin, data2, write p, stuff, data1, read p, stuff, data2, read p, customer, data1, read g, alice, admin g, bob, stuff g, charlie, customer p, david, data2, read

data1 が顧客情報で、data2 が製品情報だとすれば、

といった認可が実現できる。

困ること

上記の例だと、alice, bob は csv ファイル上に定義してあるので、ユーザー数が不定のサービス提供においては運用に耐えない。

新しい契約をとるたびにファイルを編集したり、csv のファイルの読み書きを行うわけにはいかない。

一つの解決策はそれぞれのユーザーがどの権限(admin, seller customer) なのかを独自に database に保持する方法である。

CREATE TABLE roles ( id uuid NOT NULL, user_id uuid references users(id), role string NOT NULL, );

role, err := db.GetUserRole(ctx, userID) if err != { // todo: error handle }

allowed, err := e.enforce(role, data1, "read") if err != nil { // todo: error handle }

if (!allowed) { w.WriteHeader(http.StatusForbidden) return }

これで endpoint ごとの policy を定義しておけば、認可判断を行うことができる

他の解決策

ユーザーがどの権限(admin, seller customer) なのかを casbin に管理させることもできる。

この方法を取ると、より柔軟な RBAC を実装できる。

詳細は document 参照のこと。

Policy Storage | Casbin

この方法も万能ではなく、カラム名を自由に命名できなかったり、パフォーマンス上のチューニングが必要なことが指摘されている。

Dify 使ってますか?ワークフローを作れて、可能性が広がる素敵なツールです。

そんな Dify を local で立ち上げようとしたら、初手からつまづきました。Mac 環境で同様の事象が起きる可能性があるため、ログを残しておきます。

事象

公式の Github によれば、

github.com

cd docker cp .env.example .env docker compose up -d

と、何も労せず立ち上がるとのことです。

実際、過去に Ubuntu 環境で動かした際は問題ありませんでした。しかし、今回 Mac で同様に実行したところ、

api-1 | ERROR [root] Database migration failed, error: (psycopg2.OperationalError) connection to server at "localhost" (127.0.0.1), port 5432 failed: Connection refused api-1 | Is the server running on that host and accepting TCP/IP connections? api-1 | connection to server at "localhost" (::1), port 5432 failed: Cannot assign requested address api-1 | Is the server running on that host and accepting TCP/IP connections?

と表示されました。

flask_migrate というライブラリを使って migration する時に、そもそも DB に繋がらなくてエラーになっているようです。

調査ログと解決法

DB が上がっているかの確認

docker/docker-compose.yml に変更を加え、

db: ... ports: - "${EXPOSE_DB_PORT:-5432}:5432"

と、host マシンからアクセスできるようにして、接続可能かを確認します。

docker compose で立ち上げたのち、

% psql -h localhost -p 5432 -U postgres Password for user postgres: psql: error: connection to server at "localhost" (::1), port 5432 failed: FATAL: password authentication failed for user "postgres"

ここで、password としては docker-compose に記載の difyai123456 を入力しましたが、認証失敗と出てしまいました。

そこで、今度はためしに postgres db のデフォルトのパスワード postgres で ログインしてみることにします。

% psql -h localhost -p 5432 -U postgres Password for user postgres: psql (15.5 (Homebrew), server 15.8) Type "help" for help.

postgres=#

成功しました。

password はファイルに記載の値ではないかもしれませんが、DB は立ち上がって動作していることが確認できました。

解決

Mac 環境では docker の動作や環境変数の受け渡しに違いが生まれる経験があったので、ここまでの調査から、なんとなくあたりはついていました。

DB は立ち上がっているが connection エラーになるということは、host の解決がうまくいっていない可能性が高いです

docker/docker-compose.yaml にて、

DB_HOST: db

と書き換えて再度 docker-compose up すると、今度はエラーが出ずに立ち上がりました。

その他

調査の過程でパスワードの書き換えも行っていたが、不要だったようです。

その他、一応下記の要件を満たしているかは確認していたほうがよいかと思います。

最近、作ってみたいWebアプリを思いつき、Vercelにデプロイしたいと考えました。せっかくなら使ったことのない技術を使おうと思い、RemixでSSRに挑戦することにしました。

SSRと言えばNext.jsのApp RouterとPage Routerは経験がありましたが、最近はバックエンドやインフラを触ることが多く、SSR自体が久しぶりで面白かったです。

公式サイトにQuick StartとTutorialの二つが用意されていたので、両方実施しました。

Quick Start (5m) | Remix

Tutorial (30m) | Remix

Quick Startの感想

Remixは開発時にViteを使用しています。Viteは通常CSR/SPAのイメージがあったので違和感がありましたが、Viteのビルドの速さやホットリロード、エコシステムを活用するためだと理解しました。

拡張性にも言及しながらスムーズに進められる良いガイドでした。

Tutorialの感想

規約に慣れるまでは違和感がある

Outletコンポーネントについての説明がありましたが、なぜOutletを配置すると子コンポーネントレンダリングされるのか、直感的に理解しづらかったです。

また、editページを追加した際に、ファイル名のミスで以下のエラーが発生しました:

Error: Invariant failed: Missing contactId param

app/routes/contacts.$contactId_.edit.tsxとすべきところを、ピリオドを抜かしてしまっていました。修正後解決しました。

他にも、細かい点は気になりました。慣れるまで時間がかかりそうです。

その他の感想

actionを定義すると、Formのsubmitから自動で発火され、dataも再評価される仕組みが面白いと感じました。Web開発に特化した結果、かなり便利な機能が多いのだろうと思います。

理解しながら進めるのに1時間ほどかかりましたが、最後まで完了できました。ただし、loading UIだけ反映されない問題があり、原因は特定できませんでした。

Remixそのものの感想

Next.js App Routerと比較すると、サーバー側での実行を意識しなくて良い点が大きな違いだと感じました。

規約やフレームワークが提供する便利な機能には慣れるまで時間がかかりそうです。Convention over configurationを重視するRuby on Railsのように、レールから外れたい場合は難しさを感じるかもしれません。ただし、バックエンドと違ってデータを持たないので、設計上の失敗による影響は比較的小さいと思われます。

チーム内にRemixに慣れている人が一人でもいれば、初速を求めるプロジェクトでの良い選択肢だと感じました。

前回に引き続き、手探りで API Gateway を設定している.

今回は認証を追加する。といっても実装中の Lambda が外部から好き勝手に叩かれなければそれでいいので、決め打ちの key の値があるかどうか、くらいの設定をする。

API GatewayAPI Key を利用しようと思ったが、 API Key は API Gateway v1 (REST API) で利用できる機能で、今使っている v2 では適用できなかった。

v2 の HTTP Request では Authorization の実現に 2 つの選択肢がある。 JWT と lambda だ。

JWT を選べば、必要な parameter を入れておくだけで、OIDC に準拠した IdP Provider と勝手に連携してくれると思われる(推測。実装はしていない)。

今回は IdP や Cognito を使うほどではないので、lambda で簡単に済ませたい。

terraform 実装

シンプルな lambda 関数を定義

exports.handler = async (event) => { const apiKey = event.headers['x-api-key'];

// APIキーの検証
if (apiKey !== API_KEY) {
    return {
        isAuthorized: true,
        context: {
            user: 'authorized'
        }
    };
} else {
    return {
        isAuthorized: false
    };
}

};

見ての通り、決め打ちの値が header に含まれているか、で判定を行っている。自分用の開発であれば、これで十分だろう。

lambda 関数のリソースを作成

resource "aws_lambda_function" "api_authorizer" { architectures = ["x86_64"] description = "API Authorizer" filename = data.archive_file.authorizer.output_path function_name = "ApiAuthorizer" role = aws_iam_role.lambda_role.arn handler = "authorizer.handler" runtime = "nodejs20.x" source_code_hash = data.archive_file.authorizer.output_base64sha256 }

data "archive_file" "authorizer" { type = "zip" source_dir = "functions/authorizer" output_path = "functions/authorizer.zip" }

authorizer resource を定義

resource "aws_apigatewayv2_authorizer" "temporal_authorizer" { api_id = aws_apigatewayv2_api.lambda.id authorizer_type = "REQUEST" authorizer_uri = aws_lambda_function.api_authorizer.invoke_arn identity_sources = ["$request.header.Authorization"] name = "temporal-authorizer" authorizer_payload_format_version = "2.0" enable_simple_responses = true # isAuthorized: true 形式のレスポンスを利用 }

enable_simple_responses がポイント。false の場合は、IAM 形式の response を返す必要がある

route を update して、lambda authorizer を使うように指定

resource "aws_apigatewayv2_route" "hello_world" { api_id = aws_apigatewayv2_api.lambda.id authorization_type = "CUSTOM" authorizer_id = aws_apigatewayv2_authorizer.temporal_authorizer.id ... }

動かしてみる

デプロイした後、該当の endpoint に curl request すると、Unauthorized が帰ってきてしまった。

% curl "$(terraform output -raw base_url)/hello?Name=Terraform" -H "x-api-key: API_KEY" {"message":"Unauthorized"}%

色々調査したり試行錯誤した結果、 authorization header をつけてあげる必要があった。

-H "authorization: abc"

aws_apigatewayv2_authorizer で定義した↓の部分が、request にそもそも含まれていないということで、 Unauthorized になってしまっていた。

identity_sources = ["$request.header.Authorization"]

cache が効いてるかも

curl request すると、意図した通りに本来の結果が返された。認可処理を無事通過できたことがわかる。

% curl "$(terraform output -raw base_url)/hello?Name=Terraform" -H "x-api-key: XXX" -H "authorization: abc" {"message":"Hello, Terraform!"}%

ここで、 API_KEY を誤った値に変えたらどうなるかを試してみた

% curl "$(terraform output -raw base_url)/hello?Name=Terraform" -H "x-api-key: WRONG" -H "authorization: abc" {"message":"Hello, Terraform!"}%

すると、認可された時と同じ response が帰ってきた。

疑問に思って調べると、以下の記述を見つけた。

For HTTP APIs, identity sources are also used as the cache key when caching is enabled.

https://docs.aws.amazon.com/apigatewayv2/latest/api-reference/apis-apiid-authorizers-authorizerid.html

identity_sources の値(ここでは、 authorization header の値)がキャッシュのkeyに使われているとのことだった。

ということで、キャッシュが効かない別の値でリクエストすると、以下のように意図した通り Forbidden が帰ってきた

% curl "$(terraform output -raw base_url)/hello?Name=Terraform" -H "x-api-key: WRONG" -H "authorization: different" {"message":"Forbidden"}%

終わりに

これで最低限の認可がある状態でデプロイされた環境で開発を進められるようになった

AWS Lambda の使い方を手探りで調べていたら、Hashicorp が出している tutorial にたどり着いた。

developer.hashicorp.com

AWSの勉強はリソースが残ったときにお金がかかるのが悩みだろう。 私は普段は terraform でリソースを記載して、一日の終わりに terraform destroy するなどしている。

この方法で時にしんどいのは、触ったことがない AWS サービスを勉強する場合だ。そもそもの仕組みがわかっていない中、terraform の書き方と両方を同時に調べながら試す必要がある。

AWS Lambda は AWS Game Day などでマネコンで触ることはあれど、運用で触ったことはなかったので、デプロイの方法等はわかっていなかった。 そのため、terraform で書きつつ動作も確認して、と中々理解が進まない状況だった。

そんな中見つけた冒頭の tutorial が、ちょうど自分の状況にあっていて助かった。

以下、発見や感想をつらつら残しておく

Lambda

Lambda は関数を記載したファイルや環境を zip 化しておく必要があり、手元で zip コマンドを実行してから terraform apply を叩いていた。

実は、zip の処理は terraform のコードで表現できる(terraform が機能として提供している)らしい。tutorial で初めて知った。

data "archive_file" "lambda_hello_world" { type = "zip"

source_dir = "${path.module}/hello-world" output_path = "${path.module}/hello-world.zip" }

resource "aws_s3_object" "lambda_hello_world" { bucket = aws_s3_bucket.lambda_bucket.id

key = "hello-world.zip" source = data.archive_file.lambda_hello_world.output_path

etag = filemd5(data.archive_file.lambda_hello_world.output_path) }

なお、手元では terraform init -upgrade が必要だった

terraform apply ╷ │ Error: Inconsistent dependency lock file │ │ The following dependency selections recorded in the lock file are inconsistent with the current configuration: │ - provider registry.terraform.io/hashicorp/archive: required by this configuration but no version is selected │ │ To update the locked dependency selections to match a changed configuration, run: │ terraform init -upgrade

js file に変更を加えた際に差分が反映されるか疑問だったが、hash 値の管理を忘れなければ問題ない。

source_code_hash = data.archive_file.lambda_hello_world.output_base64sha256

Lambda Function を作った後に AWS CLI で lambda の動作確認をするステップが入っているのも、tutorial の丁寧さを感じる。

API Gateway

この tutorial をやる前は、手探りで調べていたので、 api gateway には aws_api_gateway_rest_api という resource を指定していた。

tutorial では aws_apigatewayv2_api resource を指定しており、その存在から初めて知った。

aws_apigatewayv2_stage resource を作ることで API Gateway がデプロイされるらしい。これも tutorial がなければしばらく調査の海を彷徨っていたかもしれない。

終わりに

普段は serverless なシステムを作ったりしないので、知らない知識が多くて勉強になった。

tutorial のままの設計だと認証がないので、まだまだ調べる必要はある。

個人プロジェクトの一環で、Amazon DocumentDB を試してみることにしました。Amazon DocumentDB も MongoDB も初めて使うため、ChatGPT と Claude に手順を聞きながら作業を進めました。ある程度ハルシネーションがあることは普段から認識していましたが、やはりいくつかの問題に直面しました。

ゴール

API Gateway のユーザー作成 endpoint にリクエストが送信されると、Lambda が発火して、DocumentDB にデータを書き込むというシナリオを構築しました。

AWS のインフラ図

DynamoDB のインフラ

インフラは Terraform を使用して、以下のように生成しました。最初は subnet が一つだけであるなど、最適な設定ではありませんでしたが、後ほど修正を加えることで解決する範囲でした。

provider "aws" { region = "us-west-2" # or your preferred region }

resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" }

resource "aws_subnet" "main" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "us-west-2a" }

resource "aws_security_group" "main" { vpc_id = aws_vpc.main.id

ingress { from_port = 27017 to_port = 27017 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }

egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }

resource "aws_docdb_subnet_group" "main" { name = "main" subnet_ids = [aws_subnet.main.id] }

resource "aws_docdb_cluster" "main" { cluster_identifier = "chat-app-cluster" master_username = "masteruser" master_password = "securepassword" db_subnet_group_name = aws_docdb_subnet_group.main.name vpc_security_group_ids = [aws_security_group.main.id] }

resource "aws_docdb_cluster_instance" "main" { count = 1 identifier = "chat-app-cluster-instance-${count.index}" cluster_identifier = aws_docdb_cluster.main.id instance_class = "db.r5.large" }

この生成結果に以下の修正を加えました

Lambda Function の生成

Lambda 関数は JavaScript で生成されました。最初に生成されたコードでは、

と、不十分な生成結果になりました

const AWS = require('aws-sdk'); const { v4: uuidv4 } = require('uuid');

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

exports.handler = async (event) => { const { username, email } = JSON.parse(event.body);

const params = {
    TableName: process.env.TABLE_NAME,
    Item: {
        id: uuidv4(),
        username,
        email,
        createdAt: new Date().toISOString()
    }
};

await docClient.put(params).promise();

return {
    statusCode: 201,
    body: JSON.stringify({ message: "User created successfully" }),
};

};

改めて DocumentDB を使うことを明示的に指定しながら、今度は Claude に生成させた結果、完璧ではないものの、基盤として使えそうなコードが生成されました。

import { MongoClient } from 'mongodb'; import { v4 as uuidv4 } from 'uuid';

let cachedDb = null;

async function connectToDatabase() { if (cachedDb) { return cachedDb; }

const client = new MongoClient(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
});

await client.connect();
const db = client.db(process.env.MONGODB_DATABASE);
cachedDb = db;
return db;

}

export const handler = async (event) => { try { const db = await connectToDatabase(); const collection = db.collection('users');

    let body;
    try {
        body = JSON.parse(event.body);
    } catch (error) {
        return {
            statusCode: 400,
            body: JSON.stringify({ message: "Invalid request body" }),
        };
    }

    const { username, email } = body;

    if (!username || !email) {
        return {
            statusCode: 400,
            body: JSON.stringify({ message: "Missing required fields" }),
        };
    }

    const user = {
        _id: uuidv4(),
        username,
        email,
        createdAt: new Date().toISOString()
    };

    await collection.insertOne(user);

    return {
        statusCode: 201,
        body: JSON.stringify({ message: "User created successfully", userId: user._id }),
    };
} catch (error) {
    console.error('Error:', error);
    return {
        statusCode: 500,
        body: JSON.stringify({ message: "Error creating user" }),
    };
}

};

すでに JSON オブジェクトである event.body をさらに parse しようとしている問題は残っていたので、手動でコード修正しました。

また、MongoClient のオプション引数が deprecated だったので、これを削除する対応も入れました。

const client = new MongoClient(process.env.MONGODB_URI, {
    useNewUrlParser: true, 
    useUnifiedTopology: true, 
});

次に、必要なファイルを用意します

{ "dependencies": { "mongodb": "^6.7.0", "uuid": "^9.0.1" }, "type": "module" }

npm install zip -r create_user.zip package.json node_modules create_user.js

lambda 関数を用意します。生成されるコードは nodejs 14 を指定していることが多いですが、既にサポートされていないので、nodejs 20 を指定します。

また、 DocumentDB に接続するためには lambda function を DocumentDB と同じ Subnet に配置する必要があるため、そのように設定しています。terrraform apply 時に NetworkInterface 作成権限が不足していたため、権限も追加しています。

resource "aws_iam_policy" "lambda_vpc_access" { name = "lambda_vpc_access" path = "/" description = "IAM policy for Lambda VPC access"

policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DeleteNetworkInterface" ] Resource = "*" } ] }) }

resource "aws_lambda_function" "create_user" { filename = "lambda/create_user.zip" function_name = "create_user" role = aws_iam_role.lambda_role.arn handler = "create_user.handler" runtime = "nodejs20.x" source_code_hash = filebase64sha256("lambda/create_user.zip") timeout = 10

vpc_config { subnet_ids = [aws_subnet.subnet1.id, aws_subnet.subnet2.id, aws_subnet.subnet3.id] security_group_ids =[aws_security_group.main.id] }

environment { variables = { MONGODB_URI = "mongodb://${aws_docdb_cluster.main.master_username}:${aws_docdb_cluster.main.master_password}@${aws_docdb_cluster.main.endpoint}:${aws_docdb_cluster.main.port}/chat_app?tls=true&replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false" MONGODB_DATABASE = "chat_app" } } }

ここまで用意してデプロイした上で Lambda 関数をテスト実行すると、Lambda 関数がタイムアウトしました。

タイムアウトへの対応

このエラーでは他の具体的なエラーメッセージは表示されず、単にタイムアウトしていました。

LLM に助言を求めると、LLMは Security Group や ACL の設定を見直すように提案しました。自分の経験からも妥当な助言だったので調査しましたが、設定に問題は見当たりませんでした。VPC Flow Log を設定して確認したところ、全ての接続が ACCEPT されていることが分かりました。

VPC Flow Log の設定

CloudWatchロググループの作成

resource "aws_cloudwatch_log_group" "flow_logs" { name = "/aws/vpc/flow-logs" retention_in_days = 7 }

IAMロールの作成

resource "aws_iam_role" "flow_logs_role" { name = "flow_logs_role"

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

IAMポリシーアタッチメント

resource "aws_iam_role_policy_attachment" "flow_logs_role_policy_attachment" { role = aws_iam_role.flow_logs_role.name policy_arn = aws_iam_policy.flow_logs_policy.arn }

resource "aws_iam_policy" "flow_logs_policy" { name = "flow_logs_policy" description = "Policy to allow VPC Flow Logs to write to CloudWatch Logs" policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams" ] Effect = "Allow" Resource = "*" }] }) }

VPC Flow Logsの作成

resource "aws_flow_log" "main" { log_destination = aws_cloudwatch_log_group.flow_logs.arn traffic_type = "ALL" iam_role_arn = aws_iam_role.flow_logs_role.arn vpc_id = aws_vpc.main.id log_destination_type = "cloud-watch-logs" }

VPC Flow log を設定して確認してみたが、ログを見る限りは全ての接続で ACCEPT が出ている。下記はログの例(数字はランダマイズしています)

2 003500007300 eni-000cf1cbe08100000 10.0.1.00 10.0.3.000 32332 27017 6 7 802 1719110922 1719110923 ACCEPT OK

LLM に何度も助言を求めましたが解決できなかったため、DocumentDB の公式ドキュメントを確認しました。すると、TLS 設定の有無によって接続方法が異なることが記載されていました。 生成されたコードでは TLS の設定など全く考慮されていないので、TLS の接続設定が原因だろうと予想できました。調査方針が決まりました。

MongoDB のドキュメントを確認すると、TLS 設定に関して複雑な手順が記載されていました。

www.mongodb.com

... // Replace the filepaths with your certificate filepaths. const secureContext = tls.createSecureContext({ ca: fs.readFileSync(<path to CA certificate>), cert: fs.readFileSync(<path to public client certificate>), key: fs.readFileSync(<path to private client key>), }); ...

これを愚直に実装したくなかったので、Amazon DocumentDB の TLS を無効化できないか マネコンをぽちぽちしてみたが、導線が見つかりませんでした。

TLS を無効化するのは terraform で指定できるような気はしましたが、DocumentDB リソースの再作成が必要になるかもしれない、など予想して、TLS 有効な状態で動かす方針を先にもう少し調査することにしました。

Amazon DocumentDB の公式 document サンプルコードは nodejs の古い version で書かれており、動作するかあやしみつつ、他に見つけた Zenn の記事も照らし合わせつつ TLS 接続をするように書いたら、うまくいきました。

DocumentDBにローカルから接続するのは大変なのでLambda関数から接続してみた

最終的に出来上がったファイル

import { MongoClient } from 'mongodb'; import { v4 as uuidv4 } from 'uuid';

let cachedDb = null;

async function connectToDatabase() { if (cachedDb) { return cachedDb; }

const client = new MongoClient(process.env.MONGODB_URI, {
    tlsCAFile: './global-bundle.pem',
});


await client.connect();
const db = client.db(process.env.MONGODB_DATABASE);
cachedDb = db;
return db;

}

export const handler = async (event) => { try { const db = await connectToDatabase(); const collection = db.collection('users');

    const { username, email } = event.body;

    if (!username || !email) {
        return {
            statusCode: 400,
            body: JSON.stringify({ message: "Missing required fields" }),
        };
    }

    const user = {
        _id: uuidv4(),
        username,
        email,
        createdAt: new Date().toISOString()
    };

    await collection.insertOne(user);

    return {
        statusCode: 201,
        body: JSON.stringify({ message: "User created successfully", userId: user._id }),
    };
} catch (error) {
    console.error('Error:', error);
    return {
        statusCode: 500,
        body: JSON.stringify({ message: "Error creating user" }),
    };
}

};

Directory 構成

% tree -L 2
. ├── document-db.tf ├── lambda │ ├── create_user.js │ ├── create_user.zip │ ├── global-bundle.pem │ ├── node_modules │ ├── package-lock.json │ └── package.json ├── lambda.tf ├── main.tf ├── provider.tf ├── terraform.tfstate ├── terraform.tfstate.backup └── versions.tf