Express × Lightsail × Supabase で API サーバーを構築・公開してみた!

に公開

はじめに

この記事では、2人チームで開発している個人アプリのうち、
私が担当した バックエンド(Express)とAPIサーバー構築(Lightsail) についてまとめます。

  • 私:バックエンド(TypeScript/Express)とサーバー構築(Lightsail)を担当
  • 相方:フロントエンド(TypeScript/Next.js)とCloudflare Workersでの公開を担当

セキュリティ面についての注意

今回は 学習目的・コスト重視 という理由から AWS Lightsail を使用していますが、
本番環境での運用やセキュリティ要件が高いケースでは、EC2の方がより適しています

特に以下の点でLightsailには制約があります:

  • IAMロールの利用不可(APIキーの安全な管理には注意が必要)
  • セキュリティグループやVPC構成の柔軟性がない

本格運用の場合は、EC2やECSなどの選択肢を検討することをおすすめします。


なぜこの構成にしたのか?

今回のプロジェクトでは、以下の理由からこのような構成を選びました。

  • 2人で分業するために、バックエンドとフロントエンドを明確に分離
     → 相互に依存しすぎず、それぞれが責任を持って設計・開発を進めるため

  • 学習目的の技術選定
     → フレームワークを統一せず、私は Express、相方は Next.js を採用。
     → お互いのスキルを活かしつつ、得意分野を深掘りする狙いもありました

  • コストを抑えた運用を実現したかった
     → 月額固定の Lightsail(12 USD)と、無料枠の Supabase を活用することで、
      月1,000〜2,000円程度でサーバー構築・公開を実現


使用した技術スタックと構成

技術構成(アプリ全体)

項目 使用技術
バックエンド TypeScript(Express)
フロントエンド TypeScript(Next.js)
データベース PostgreSQL(Supabase)
APIサーバー AWS Lightsail(Linux)
フロント公開 Cloudflare Workers
ドメイン管理・CDN Cloudflare

使用した AWS サービス一覧

サービス名 用途
Lightsail APIサーバーのホスティング
SSM Parameter Store .env 用の環境変数(機密情報)の安全な管理
IAM(ユーザー) CLI認証用。最小権限を付与し、多要素認証(MFA)も設定

🔒 今回は 2人での共同開発かつ学習を兼ねた構築だったため、IAMユーザーには Lightsail および SSM に必要な最小限の権限を明示的に付与しました。
また、セキュリティ対策として MFA(多要素認証)も有効化 しています。


Lightsail を選んだ理由

AWS Lightsail の $12 プラン(スタンダード)は以下の構成です:
※2025年7月時点の情報です

  • 月額:12 USD(約1,900円)
  • メモリ:2 GB
  • vCPU:2 コア
  • ストレージ:60 GB SSD
  • データ転送量:3 TB/月

このプランを選んだ理由:

  • 固定料金でコスト管理がしやすい(従量課金のEC2より安心)
  • Ubuntu(他Linuxも選択可)を使って、自由にパッケージをインストール・構成できる
  • 初学者でもGUIベースでインスタンス構築が簡単

ローカルで動かしていた Express アプリを、Lightsail 上に配置し フロントエンドなどから HTTP(または HTTPS) 経由でアクセスできるよう公開するという目的に適しており、学習にも最適でした。


Supabase の使い方と選定理由

データベースには Supabase を使用しました。
Supabase は、PostgreSQL をベースにしたクラウド型のデータベースサービスで、
GUI が使いやすく、アカウント登録後すぐに使えるのが魅力です。

なぜ Supabase にしたか?

  • PostgreSQL が使える
  • 無料枠でも十分に試せる
  • GUIでテーブルやデータを簡単に操作できる
  • TypeScript との相性も良い

今回のような個人開発や学習目的のプロジェクトでは、コストをかけずに扱える点が非常にありがたかったです。

テーブル定義は Drizzle ORM で管理

Supabase 上のテーブル構造(スキーマ)は、Drizzle ORM を使って TypeScript でコードとして定義しています。
マイグレーション用の SQL を自動生成し、それを Supabase に適用する形でテーブルを作成しました。

このようにコードで管理することで:

  • テーブル構造を Git 上で管理・レビューできる
  • ローカルと本番で構造を揃えやすい
  • GUIでの操作ミスを防げる

といったメリットがあります。

データは Supabase の GUI から入力

データ自体は、Supabase の GUI を使って手作業で登録しています。
GUI からテーブルを開いて「行の追加」で入力していくイメージです。

まだデータ量がそれほど多くないため、GUIベースで確認・修正できる方が都合がよく、
シードスクリプトやCSVのインポートは今回は使っていません。

Supabase との接続とセキュリティ設計

環境変数の管理方法(SSM Parameter Store)

本番環境では、Supabase の接続情報を AWS Systems Manager Parameter Store(SecureString)に保存しています。
デプロイ時には、SSMから取得した値を .env ファイルに書き出して利用します。

具体的な .env 生成の方法は後述します(→「本番ではこのような形で .env を生成」セクション参照)。

Supabase 側の権限設計(読み取り専用ユーザー)

本番環境では、Supabase 側のデータベースロールを public_user として、読み取り専用のポリシーを設定しています。
SQL でのポリシー定義は以下のような内容です:

alter policy "Enable read access for all users"
on "public"."your_table"
to public_user
using (true);

このようにして、「アプリケーションから読み取りだけできる」安全な状態を作っています。


Express の構成とルーティング

API サーバーは TypeScript + Express で構築しており、ルーティングとロジックを役割ごとに分割する構成にしています。
プロジェクトはモノレポ構成で、API 側は apps/api/ 配下にあります。

apps/
└── api/
    ├── src/
    │   ├── controllers/     // ビジネスロジック
    │   │   └── problem.ts
    │   ├── db/              // Supabase接続やスキーマ
    │   │   ├── index.ts
    │   │   └── schema.ts
    │   ├── routes/          // APIルーティング
    │   │   ├── index.ts
    │   │   └── problem.ts
    │   └── index.ts         // アプリ起動エントリーポイント

たとえば、あるデータセットをランダムで取得する GET API は以下のように実装しています。

// controllers/problem.ts
import { sql } from 'drizzle-orm';
import { db } from '../db'; // DB接続設定
import { RequestHandler } from 'express';

export const getAllProblems: RequestHandler<{}, any, any, { limit?: string }> = async (req, res) => {
  const randomOrderBy = sql`RANDOM()`;

  const problems = await db.query.problems.findMany({
    limit: Number(req.query.limit) || 10,
    orderBy: randomOrderBy,
  });

  res.json(problems);
};
// routes/problem.ts
import { Router } from 'express';
import { getAllProblems } from '../controllers/problem';

const router: Router = Router();

router.get('/problems', getAllProblems);

export default router;

ファイルを責務ごとに分けることで、機能の追加や修正がしやすくなり、学習目的としても Express アプリの構成を理解しやすくなりました。

DB 接続まわりの構成と .env に関する補足

Supabase との接続処理は、apps/api/src/db/index.ts にて以下のように記述しています:

// db/index.ts
import * as schema from './schema';
import { config } from 'dotenv';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

config({ path: '.env' });

const connectionString = `postgresql://${process.env.SUPABASE_ROLE_NAME}.${process.env.SUPABASE_PROJECT_ID}:${process.env.SUPABASE_ROLE_PASSWORD}@${process.env.SUPABASE_HOST}:${process.env.SUPABASE_PORT}/postgres`;

const client = postgres(connectionString, { prepare: false });
export const db = drizzle({ client, schema });

このように dotenv.config() を使って .env を読み込む構成にしたことで、ローカル開発との互換性が保たれ、手軽に動作確認できるというメリットがありました。

一方で .env を使用するということは、機密情報が平文で扱われるリスクもあるため、本番環境では .env を Git に含めず、SSM Parameter Store から生成するシェルスクリプトを用いて対応しています。

本番ではこのような形で .env を生成

以下のように、事前に環境変数で SSM パラメータ名を設定しておきます

export SUPABASE_HOST_PARAM="/myapp/supabase/host"
export SUPABASE_PORT_PARAM="/myapp/supabase/port"
export SUPABASE_ROLE_NAME_PARAM="/myapp/supabase/role_name"
export SUPABASE_ROLE_PASSWORD_PARAM="/myapp/supabase/role_password"

SSM から値を取得し、.env を生成(上書き)

echo "SUPABASE_HOST=$(aws ssm get-parameter --name $SUPABASE_HOST_PARAM --with-decryption --query Parameter.Value --output text)" > .env
echo "SUPABASE_PORT=$(aws ssm get-parameter --name $SUPABASE_PORT_PARAM --with-decryption --query Parameter.Value --output text)" >> .env
echo "SUPABASE_ROLE_NAME=$(aws ssm get-parameter --name $SUPABASE_ROLE_NAME_PARAM --with-decryption --query Parameter.Value --output text)" >> .env
echo "SUPABASE_ROLE_PASSWORD=$(aws ssm get-parameter --name $SUPABASE_ROLE_PASSWORD_PARAM --with-decryption --query Parameter.Value --output text)" >> .env

🔎 少しだけ反省点
.env ファイル自体は生成されますが、中身はすべて
SUPABASE_HOST=$(aws ssm get-parameter ...) のように、SSM Parameter Store から取得した値を元に構成しています。

そのため、機密情報をコードや Git に直接書き込むことがなく、一定の安全性が担保された構成になっています。

生成された .env は dotenv によって読み込まれ、アプリケーション内で必要な情報として利用される仕組みです。
ローカルでも .env を配置すれば同様に動作するため、開発体験のしやすさも維持できます。

とはいえ、本番環境では .env を生成する工程そのものを省略し、あらかじめ環境に値を設定する運用も選択肢のひとつです。
今回はローカルとの互換性を優先しましたが、今後はさらにセキュアな構成も検討したいと感じました。


Lightsail を使った API サーバー構築手順

Lightsail インスタンスのセキュリティ設定について

Lightsail のインスタンスは作成直後、すべての IP アドレスからの接続が許可された状態です。
そのため、まず最初に以下のセキュリティ設定を行いました。

✅ IP アドレスによるアクセス制限(ファイアウォール設定)

Lightsail 管理画面の「ネットワーキング」タブから、
許可する IP アドレスを自分のみに制限します。

ポート番号 用途 アクション例
22 SSH 自分のIPだけ許可
80 / 443 HTTP/HTTPS 必要に応じて公開(または自分のみ)

❗ 初期状態では 0.0.0.0/0(全世界)からアクセス可能な設定になっているため、そのまま運用するとセキュリティリスクが非常に高くなります。


Lightsail での実行手順

今回は読み手がすぐに再現しやすいよう、Docker を使わないシンプルな構成で実行手順を紹介します。
ただし、実際の構築時には、自分の環境との差異を縮小する目的と学習の一環として Docker を利用しました。

また、Lightsail インスタンスに SSH 接続した直後は、ユーザーの初期設定やタイムゾーン変更、apt update などの操作が必要になります。
ただし今回は本題から外れるため、そうした初期セットアップの詳細手順は割愛します(必要に応じて各自対応お願いします)。

AWS CLI の認証設定(IAMユーザー)

Lightsail インスタンス上で AWS CLI を使って、SSM Parameter Store などから .env を自動生成する場合、
IAM ロールではなく、IAM ユーザーのアクセスキーとシークレットキーによる認証設定が必要になります。

以下のコマンドで、クレデンシャルを設定できます:

aws configure

対話形式で以下を入力:

AWS Access Key ID [None]: ********************
AWS Secret Access Key [None]: ********************
Default region name [None]: ap-northeast-1
Default output format [None]: json

これにより、Lightsail 上からでも以下のようなコマンドが実行できるようになります:

aws ssm get-parameter --name /myapp/supabase/host --with-decryption

⚠️ 補足:EC2 のように IAM ロールを割り当てて自動で認証する仕組みは Lightsail には存在しません。
そのため、アクセスキーは最小権限で作成し、使用後はローテーションや無効化を検討することをおすすめします。

アプリケーションのデプロイ手順

# 0. Gitでリポジトリをクローン(秘密鍵の生成&SSH設定は別途対応)
git clone git@github.com:your-username/your-repo.git

# 1. 必要なランタイムのインストール(例:Node.js)
# ※使用する言語やバージョンに応じて、各自インストールしてください。

# 2. リポジトリに移動
cd your-repo

# 3. .env を SSM などから自動生成(※例:generate-env.sh)
chmod +x generate-env.sh
./generate-env.sh

# 4. アプリを起動
# ※各プロジェクトの構成に応じて適切なコマンドを実行してください(例: `pnpm dev`, `node dist/index.js` など)

API の動作確認(GET リクエスト)

上述の lightsail での実行手順 セクションでは、読者が Docker を使用しなくても実行できる構成を紹介しましたが、
このセクションでは 筆者が実際に Docker を使って確認を行った方法 を紹介しています。
あくまで一例としてご参照ください。

自分の確認方法(実例)

以下の手順で、動作確認を行いました:

# Docker コンテナの起動状態と公開ポートを確認
docker ps

表示された 0.0.0.0:xxxx->ポート番号/tcp を元に、ブラウザで以下のようにアクセス:

http://<LightsailのIPアドレス>:<確認したポート番号>/problems

たとえば、8080 が割り当てられていた場合:

http://13.114.xxx.xxx:8080/problems

正常に JSON レスポンスが返ってくれば、API サーバーが無事に外部公開されていることを確認できます。

必要に応じて curl を使っても確認できます:

curl http://<LightsailのIPアドレス>:<ポート番号>/problems

フロントエンドとの連携(Next.js 側)

この API サーバーは、相方が構築した Next.js アプリ(Cloudflare Workers 上) からデータ取得のために利用されます。
具体的には、/problems API にアクセスして、画面にランダムな問題を表示するといった実装です。


おわりに

今回の構成は、個人開発の一環として API サーバーを Lightsail 上に構築し、フロントエンド(Next.js)と連携させるというシンプルな目的からスタートしました。

加えて、環境変数の安全な管理方法や、環境差異を縮小する Docker の扱いなど、インフラ周りの学習も兼ねた試みでした。

.env の取り扱いや SSM Parameter Store の活用、ローカル開発とのバランス、セキュリティ対策など、実践的な構成と試行錯誤を含んだ内容となっています。

今後は .env を介さない構成の検討や、API の拡張、本格的な CI/CD パイプラインの導入など、スケールに応じて発展させていく予定です。

Lightsail や Supabase を活用したシンプルな構成でも、十分に学びやすく実用的な環境が構築できます。この記事が、同じように個人開発に挑戦する方の一助になれば嬉しいです。

(次回はElastic Beanstalkについての記事書けたら良いなと思っています)

Discussion