開発の動機
個人的に欲しかったサービスを年末年始の時間を利用して開発してみようと考えていたのがきっかけで、なぜこのアーキテクチャにしたかというと、AWSのサービスを使いながら運用コストを抑えた開発をしたいという思いが、まず一番にありました。それと前から気になっていたサーバレスのLambdaを採用したかったのと、Frontの技術を個人開発しながら身につけたいという思いで、比較的コスパの良いアーキテクチャと思ったS3, Lambda, EFSという構成でスモールスタートしてみようと考えました。
どれぐらいお財布に優しくなったのか
直近の実績ベースで1日の運用コストは約0.5USDでした。1ヶ月30.5日で考えると15.25USD/Mで、仮に1USD=150円で計算すると、1ヶ月2,287.5円程度になります。ちなみに無料利用枠は使っていないので、アカウント今から作る人ならもっとお安くなるのではと思っています。個人的な比較対象として同じAWSのサービスでECSとRDBの構成だとこの倍の請求だったので、個人的には継続課金しても良いと思えるレベルまでコスト抑えられたと思います。
アーキテクチャ
早速本題に入っていきます。※全てAWSのサービスを使用していますので、AWSやAmazonは省略して表記しています。
- DNS:Route 53
- CDN:CloudFront ※1
- Front:S3(React)
- Database:EFS(SQLite)
- Backend:Lambda(Python)
アーキテクチャ概略図
CDNとは
ユーザーにより近いリージョン(例えばユーザーが日本国内なら東京リージョン)にコンテンツをキャッシュして保存し、エンドユーザーのブラウザやアプリケーションにコンテンツをより速く配信するための分散型のサーバーネットワークのことです。
こちらを参考にしていただくと、イラスト付きで理解しやすいと思います。
そんな CDN(Amazon CloudFront) は、セキュリティ面でも心強く、AWSのセキュリティ機能と統合して、データの暗号化、DDoS攻撃の軽減、アクセス制御など、高度なセキュリティ対策を提供してくれているので安心して使えそうなので、採用しました。
実装の流れ
- S3で簡単な静的コンテンツの表示
- 独自ドメイン取得してHTTPSでS3へアクセスする
- VPC作成
- EFSの作成
- Lambdaの設定
- LambdaとEFSの通信設定
- Lambda経由でDB作成
- バックエンド処理
- API Gatewayの設定
- API Gatewayへのリクエストを追加
最初はこんな感じで全体像を掴むため、上記のMermaidで書いたアーキテクチャ概略図
を手書きで書いてみて、どこまで実装できたのか確認しながら進めました。
S3で簡単な静的コンテンツの表示
まずはS3へHTMLで実装した簡単なコードをアップロードして、ブラウザで確認できる状態から開始しました。目に見えるものがあると、安心感があるので先に設定しておきました。ちなみに確認の際は、一次的にS3の公開設定をパブリックにして、アップロードしたコードが画面で表示できるか確認しておきました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width" />
<title>テストページ</title>
</head>
<body>
<h1>テストページ</h1>
<ul><li>リストアイテム1</li></ul>
</body>
</html>
独自ドメイン取得してHTTPSでS3へアクセスする
HTTPSでS3の静的なページにアクセスするようにAWSのCDNサービスであるCloudFrontを組み合わせることで、より効率的でセキュアな通信を出来るようにします。余談ですが、独自ドメイン取るとなんか愛着湧きますよね。設定作業は、ざっとこんな流れです。
- Amazon Route 53
- 独自ドメインの取得
- DNS設定
- S3バケットの設定
- S3バケットで静的ウェブサイトホスティングを有効にする
- AWS Certificate Manager (ACM)でSSL/TLS証明書を取得(地味に時間かかります)
- Amazon CloudFrontの設定
- Route 53 で CloudFrontディストリビューションへのルーティングを設定
設定作業は下記の記事を参考にしたので、ご紹介します。
VPC作成
LambdaやEFSが動作する環境(VPC:Virtual Private Cloud)を東京リージョン ap-northeast-1
で作成。もし、VPCを設定したことがなければ下記の記事が初学者には分かりやすい内容になっていると思いますので、参考までに
EFSの作成
Lambdaの設定
Lambdaの作成
まず、Lambdaの 関数の作成
から作成
詳細設定
から、EFSと同じリージョンを選択(後からでも設定できます)
SGの作成
Lambda関数がEFSへアクセスできるようにSG(セキュリティグループ)を作成
- インバウンドルール(
Inbound rules
):外からLambda(中)へ向かう流れ- LambdaからFESへアクセスできるように設定
- Lambdaが存在するVPCのIPアドレスからのアクセスを許可するによう設定
- アウトバウンドルール(
Outbound rules
):Lambda(中)から外へ向かう流れ- 外部のどこにでも出ていけるように設定しておくので、0.0.0.0/0にする
LambdaとEFSの通信設定
アクセスポイントの作成
Lambda関数がEFSにアクセスするためのエントリーポイントの設定で、先ほど作成したEFS内のダッシュボード、アクセスポイントの アクセスポイントを作成
から作成することができます。
マウントターゲットの設定
LambdaがEFSのどのディレクトリをマウントするか設定します。これにより、Lambda関数がEFS上のSQLiteデータベースにアクセスできます。
EFSのディレクトリの権限変更
ルートディレクトリ作成のアクセス許可 - オプション
の アクセスポイントのアクセス許可
から設定できるようです。私は、EFSにアクセスするEC2インスタンスを作成して、EFSのディレクトリの権限をchmodコマンドで変更しました。
LambdaとEFSが通信できているか確認
Lambdaのコードソースということころに通信テスト用のコードを記載して、Test
からコードを実行することで、実際にLambdaとEFSが通信出来ているか確認しました。
ここでは疎通を確認出来ればいいので、好きなように書いてテストするといいと思います。画像はRubyで書いています。
import json
import os
def lambda_handler(event, context):
efs_path = '/mnt/efs/testfile.txt' # EFSにマウントされたパス
try:
# ファイルが存在しない場合は作成
if not os.path.exists(efs_path):
with open(efs_path, 'w') as file:
file.write('Hello from Lambda to EFS')
# ファイルの内容を読み込む
with open(efs_path, 'r') as file:
content = file.read()
return {
'statusCode': 200,
'body': json.dumps(f'File Content: {content}')
}
except Exception as e:
print(f"Error: {e}")
return {
'statusCode': 500,
'body': json.dumps(f"Error reading or writing to file: {str(e)}")
}
Lambda経由でDB作成
簡単なテーブルを作成
LambdaとEFSの疎通は確認できたので、次に1つ簡単なテーブルを作成してみます。下記のコードをLambdaで実行して、テーブルを作成します。
# EFSと疎通確認する関数
import sqlite3
def handler(event, context):
# EFS上のデータベースファイルパス
db_path = '/mnt/efs/mydatabase.db'
# データベースに接続
conn = sqlite3.connect(db_path)
# テーブルを作成
conn.execute('''
CREATE TABLE IF NOT EXISTS Logs (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
''')
# データベースの変更をコミットし、接続を閉じる
conn.commit()
conn.close()
return {
'statusCode': 200,
'body': 'Database and tables created successfully.'
}
レコード作成
同じようにLambdaから下記のコードを実行して試しにレコードを作成してみます。
# 手動でレコードを作成する関数
import sqlite3
from datetime import date
def handler(event, context):
db_path = '/mnt/efs/mydatabase.db'
conn = sqlite3.connect(db_path)
conn.execute('''
INSERT INTO Logs (title, description, created_at) VALUES (?, ?, ?)
''', ('タイトル', '概要', date.today()))
# コミットとクローズ
conn.commit()
conn.close()
return {
'statusCode': 200,
'body': 'Record added to Logs successfully'
}
レコードの取得
作成したレコードをLambdaから取得できるか確認しておきます。
# 特定のデータベースの内容を取得する関数
import sqlite3
def handler(event, context):
db_path = '/mnt/efs/mydatabase.db'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# logsテーブルから最新の10件のレコードを取得
cursor.execute('SELECT * FROM Logs ORDER BY created_at DESC LIMIT 10')
# 結果を取得
rows = cursor.fetchall()
# 結果を表示用に整形
result = [{
"id": row[0],
"title": row[1],
"description": row[2],
"created_at": row[4]
} for row in rows]
conn.close()
return {
'statusCode': 200,
'body': result
}
ここまでで、Lambdaから、テーブル作成、レコード追加、レコード取得が出来ることを確認しました。更新や削除も同じ要領でLambdaから行うことが出来ます。
バックエンド処理
動的コンテンツのリクエストに対してレコードを取得して、レスポンスとして返してあげるバックエンドの処理をLambdaに書いていきます。完成系ですが、この後出てくるHTTPリクエストの取得など、デバッグしながら書いています。
import sqlite3
import json
import logging
from datetime import date
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event, context):
db_path = '/mnt/efs/mydatabase.db'
conn = sqlite3.connect(db_path)
logger.info('Connected to the database successfully')
http_method = event.get('method') # HTTPメソッドによる処理の分岐
if http_method == 'GET': # GETリクエストの処理
try:
cursor = conn.cursor()
cursor.execute("SELECT title, description, implementation_date FROM Logs")
logs = cursor.fetchall()
logger.info('Fetched records from the database successfully')
conn.close()
# HTMLコンテンツの生成
html_content = ""
for log in logs:
html_content += f"<li><strong>{log[0]}</strong> - {log[2]}<p>{log[1]}</p></li>"
except Exception as e:
logger.error('Error fetching records from the database: %s', e)
return {
'statusCode': 500,
'body': json.dumps({'error': str(e)})
}
return {
'statusCode': 200,
'headers': {'Content-Type': 'text/html'},
'body': html_content
}
API Gatewayの設定
API Gatewayは、クライアントから動的コンテンツを取得するためのリクエストを受け取って、クライアントが欲しい情報を返してあげるGatewayの設定が必要です。こちらの記事が参考になります。
注意点として、今回はAPIがクライアントからGETメソッドを受け取ることを想定しているので、GETの 結合リクエスト
で マッピングテンプレート
を設定する必要があります。ここで設定しておかないとLambdaが何のリクエストが来たのか分からなくて困ります。
API Gatewayへのリクエストを追加
最後にS3へアップロードしていたファイルに少し手を加えて動的コンテンツを取得するようにします。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width" />
<title>テストページ</title>
</head>
<body>
<h1>テストページ</h1>
<ul id="hoge-list"></ul>
<script>
fetch('https://XXXXXX.execute-api.ap-northeast-1.amazonaws.com/hoge')
.then(response => response.text())
.then(data => {
document.getElementById('hoge-list').innerHTML = data;
})
.catch(error => console.error('Error fetching data:', error));
</script>
</body>
</htm
この設定で、ユーザーがアクセスすると下記のような流れで欲しい情報を取得してブラウザに表示してくれます。
- S3から静的コンテンツを取得、表示させる
- JavaScriptのfetch関数で、API Gatewayに設定されたエンドポイントに対してHTTPリクエストを非同期で送信
- API Gatewayの設定に基づいてリクエストがLambda関数にルーティングされる
- Lambda経由でEFSからレコードを取得
- 取得したレコードを
<ul id="hoge-list"></ul>
のliタグ内に表示
まとめ
年末年始にまとまった時間を確保出来たので、新しいアーキテクチャで新しいサービスを開発できたのは学びも多く、開発者として幸せになってます。途中省略したところもあったので、気づいたら記事も更新していこうと思いますが、なにはともあれ運用コストも抑えることが出来たので、当初の目的は概ね達成出来たと思います。次は開発環境の整備とFrontの開発を進めてヌルヌルした画面を作って行こうと思います。
最後までお読みいただきありがとうございました。