先日、以下のブログ記事を拝読しました。
昔、ちょっと試した時には遅くて開発環境しか使えないなという印象を持っていたのですが、Rails 8になってだいぶ変わったようです。
そこで、ECS FargateにてRails 8アプリを動かし、データベースとしてEFS上に置いたSQLiteという構成を取ってみようと思い立ち、実装してみました。
Rails 8アプリの実装
Rails 8アプリの実装に特別注意することはありません。ECSで動かす場合はいくつかの環境変数を設定すればOKです。以下の記事はRails 7.2の場合ですが、やることは特に変わりません。
今回、Production環境にてデータベースをSQLiteにするので、config/database.yml
の設定を以下のようにします。
(省略) production: <<: *default database: /mnt/efs/production.sqlite3
AWSの設定(CDK)
AWSの環境はCDKにて実装します。Railsを動かす際の基本的な実装は以下のブログ記事に記しています。
また、EFSをマウントする際の基本的な実装は以下のブログ記事に記しています。
基本はこれらを組み合わせればできるのですが、細かい注意点があるのでEFS周りを中心に取り上げて解説します。
今回はEFSを使うので、作成しておきます。
const fileSystem = new FileSystem(this, 'MyEfsFileSystem', { vpc: vpc, encrypted: true, removalPolicy: RemovalPolicy.DESTROY, lifecyclePolicy: LifecyclePolicy.AFTER_14_DAYS, performanceMode: PerformanceMode.GENERAL_PURPOSE, throughputMode: ThroughputMode.BURSTING });
ポイントはアクセスポイントの作成です。
const accessPoint = new AccessPoint(this, 'EFSAccessPoint', { fileSystem: fileSystem, path: '/mnt/efs', posixUser: { uid: '1000', gid: '1000' }, createAcl: { ownerGid: '1000', ownerUid: '1000', permissions: '755' }, });
このアクセスポイントの指定はRails 8が生成してくれるDockerfile
の以下の指定に起因するものです。
# Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ chown -R rails:rails db log storage tmp USER 1000:1000
アクセスポイントの指定がないと、Rails 8アプリがUID/GIDがそれぞれ1000のユーザーでファイルを書き込みにいく一方で/mnt/efs
がrootユーザーしか書き込めないことになります。結果として、ECSサービスの起動時にSQLite3が作成できずに起動に失敗します。
path
はSQLiteファイルを置く場所としてconfig/database.yaml
で指定した場所と合わせてください。
作成したEFSとアクセスポイントはタスク定義にて指定しておきます。メモリやCPUは適宜調整してください。以下の例ではコンテナイメージをM2 mac上で作ったのでcpuArchitectureが
ARM64`になっていますが、こちらも適宜修正してください。
const taskDef = new FargateTaskDefinition(this, "MyTaskDef", { memoryLimitMiB: 512, cpu: 256, runtimePlatform: { operatingSystemFamily: OperatingSystemFamily.LINUX, cpuArchitecture: CpuArchitecture.ARM64 }, volumes: [ { name: "dbfile", efsVolumeConfiguration: { fileSystemId: fileSystem.fileSystemId, authorizationConfig: { accessPointId: accessPoint.accessPointId, iam: "ENABLED" }, transitEncryption: "ENABLED" } } ] });
あとはコンテナ定義とマウントポイントを作成してあげます。
const containerDef = new ContainerDefinition(this, 'MyContainerDefinition', { image: ContainerImage.fromRegistry('使用するコンテナイメージ名'), logging: LogDrivers.awsLogs({ streamPrefix: 'myrailsecs', logRetention: RetentionDays.ONE_DAY }), taskDefinition: taskDef, environment: { RAILS_ENV: "production", RAILS_LOG_TO_STDOUT: "1", RAILS_SERVE_STATIC_FILES: "1", RAILS_MASTER_KEY: this.node.tryGetContext("RAILS_MASTER_KEY") } }); containerDef.addMountPoints( { containerPath: '/mnt/efs', sourceVolume: 'dbfile', readOnly: false } );
最後にセキュリティグループの設定です。ECSサービスからEFSに対して接続を許可してあげます。
fileSystem.connections.allowDefaultPortFrom(fargateService.connections);
これでCDKの実装はおしまいです。
動作
これでRails 8+SQLiteを使ったアプリケーションを動かすことができました。
ですが、少し動作確認をしたところ、以下のエラーメッセージを時折出力することに気がつきました。タイミングはデータベースへの書き込みタイミング。
database disk image is malformed
どうもSQLiteのデータベースファイルが壊れたという意味のようで、実際、Railsアプリでは500エラーが返ってきます。ただ、その後読み込み処理を行ったところ正常にデータは読み込めたのでちょっと不思議です。
よくよく調べてみるとSQliteのFAQに以下のものがありました。SQLiteはロック機構をOSの機能に依存しているため、NFSを使った場合はうまくいかないことが多いとのこと。結果、今回の構成は非推奨のような気がします。
考察
以上を踏まえると、SQLiteをEFS上に置いてデータベースの代わりにするのはちょっと危険な気がします。この構成が取れたら、RDSを使わずにコスト削減ということも考えられたのですが、そう簡単にはいかないように思えます。