AWSで爆安サーバレスRDBを構築する (個人開発向け)
個人開発楽しいですよね!
なるべく安く済ませたいので、基本はサーバレスサービスを使っていきますが、厄介なのがDB。
こいつだけはインスタンス代がそこそこかかります。
「性能は度外視で構わん。。動けばええんや。。。」
そんな要件で格安RDBをいい感じに作れないかチャレンジしてみました。
結論
EFS + SQLite3 に lambdaでマウント
全体構成
SQLite
mysqlやpostgresなどと異なり、ファイルベースで動かせるRDBMS。〇〇.sqlite3というファイルさえ管理してしまえばOK。
以前は「本番運用でSQLiteはありえないでしょ..」という感じでしたが、
最近はlitestreamやcloudflareのD1などで結構話題となっており、商用利用でも実例が増えてきました。個人開発なら全然問題ないと思います。
Elastic File System (EFS)
AWSの提供するスケーラブルなマネージドファイルシステム。NFSでマウント可能なので、EC2のみならずFargateやlambdaからも使えます。複数のNFSクライアントから同時接続できるのがポイント!
lambda, API Gateway
いわずもがな。お世話になっている人も多いと思うので割愛。
ポイント
lambdaからのEFSマウント
lambdaをVPC内に作成することで、lambdaからEFSへのマウントが可能になります。
色々設定が大変そう?なので、CDKでサクッとやってみます。
import { Construct } from 'constructs'
import { Duration } from 'aws-cdk-lib'
import * as efs from 'aws-cdk-lib/aws-efs'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as path from 'path'
import { DockerImageCode } from 'aws-cdk-lib/aws-lambda'
// VPCの定義
vpc = new ec2.Vpc(scope, "Vpc", {
cidr: "192.168.0.0/24",
// privatesubnetのみ作成。isolatedなので、NAT Gatewayも作られない。
subnetConfiguration: [
{
cidrMask: 26,
name: "efs",
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
maxAzs: 2,
vpcName: "phoquash-vpc",
});
// EFSの定義
fileSystem = new efs.FileSystem(this, "phoquashFileSystem", {
vpc: vpc,
// files are not transitioned to infrequent access (IA) storage by default
lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS,
performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
// files are not transitioned back from (infrequent access) IA to primary storage by default
outOfInfrequentAccessPolicy:
efs.OutOfInfrequentAccessPolicy.AFTER_1_ACCESS,
});
// EFSアクセスポイントの追加
accessPoint = fileSystem.addAccessPoint("AccesssPoint", {
// set /export/lambda as the root of the access point
path: "/export/lambda",
// as /export/lambda does not exist in a new efs filesystem, the efs will create the directory with the following createAcl
createAcl: {
ownerUid: "1001",
ownerGid: "1001",
permissions: "750",
},
// enforce the POSIX identity so lambda function will access with this identity
posixUser: {
uid: "1001",
gid: "1001",
},
});
// sqlite3を扱うライブラリをlayerに保存
nodeLayer = new lambda.LayerVersion(this, "nodeLayer", {
// layerディレクトリ内の構成は下記を参照
code: lambda.Code.fromAsset(path.join(__dirname, "../lambda/layer")),
compatibleRuntimes: [lambda.Runtime.NODEJS_16_X],
description: "node layer",
layerVersionName: "node-layer",
});
// DBを作成するlambda
createDb = new lambda.Function(
this,
"createDb",
{
// マウントするfilesystemを設定
filesystem: lambda.FileSystem.fromEfsAccessPoint(
accessPoint,
"/mnt/db"
),
runtime: lambda.Runtime.NODEJS_16_X,
handler: "index.handler",
code: lambda.Code.fromAsset(
path.join(__dirname, "../lambda/function/createDb")
),
vpc: vpc,
}
)
ポイントとなるのは、lambdaで用いるsqlite3ライブラリをlayerで用意することです。
適当なlinux環境で
$ npm init
$ npm install sqlite3
を行い、node_modulesを生成してあげます。
最終的にlayerとして指定するディレクトリ構成は /layer/nodejs/node_modules です。
最初mac環境で上記を行ったところ、mac向けのsqlite3が構成されてしまい、lambdaではエラーがでてしまいました。vagrantでのubuntu(focal)環境で生成したnode_modulesでは動作実績があります。
後はlambdaでアクセスするだけです。
exports.handler = async (event, context) => {
const sqlite3 = require("sqlite3");
const db = new sqlite3.Database("/mnt/db/test.sqlite3")
}
追記(未検証)
layerを作成せずとも、nodejsFunctionでlambdaを定義し、bundling.forceDockerBundlingをtrueにしておけばいい感じにsqlite3モジュールを組み込んでくれると思います。
import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs'
createDb = new nodeLambda.nodejsFunction(
this,
"createDb",
{
// マウントするfilesystemを設定
filesystem: lambda.FileSystem.fromEfsAccessPoint(
accessPoint,
"/mnt/db"
),
runtime: lambda.Runtime.NODEJS_16_X,
entry:
path.join(__dirname, "../lambda/function/createDb/index.ts"),
vpc: vpc,
bundling: {
forceDockerBundling: true // lambda互換dockerコンテナ環境でTypeScriptのビルドが実行される
}
}
)
料金
DB部分で必要となるのはEFSのストレージ代のみなので、$0.02/GB・月 程度で利用可能です。
RDSの最安インスタンスでも$50/月程度、EC2のクソ雑魚インスタンスでも$10程度かかるので、圧倒的に安いことがおわかり頂けるかと思います。
lambdaではなくFargateからEFSマウントもできますが、1コンテナあたり少なくとも$10はかかってしまうので、大してアクセスされない個人開発ではlambdaが最安だと思います。
まとめ
lambdaからEFSへマウント&SQLiteを使うことで、爆安サーバレスRDBを構築することができました。
ただし、SQLite自体は異なるlamdbaからの同時書き込みなど受け入れられない気がするので、
厳密にやるならSQSでキューイングしてあげるなどの処理が必要になるかと思います。
使ってみてわかる問題点など分かりましたら、また追記していきたいと思います。
(そこそこ前に行ったので一部ミスがあるかもです。遠慮なくコメントください。)
Discussion