🏸

AWSで爆安サーバレスRDBを構築する (個人開発向け)

2022/07/10に公開

個人開発楽しいですよね!
なるべく安く済ませたいので、基本はサーバレスサービスを使っていきますが、厄介なのがDB。
こいつだけはインスタンス代がそこそこかかります。

「性能は度外視で構わん。。動けばええんや。。。」
そんな要件で格安RDBをいい感じに作れないかチャレンジしてみました。

結論

EFS + SQLite3 に lambdaでマウント

全体構成

SQLite

mysqlやpostgresなどと異なり、ファイルベースで動かせるRDBMS。〇〇.sqlite3というファイルさえ管理してしまえばOK。

以前は「本番運用でSQLiteはありえないでしょ..」という感じでしたが、
最近はlitestreamcloudflareの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