はじめに
NestJS製のAPIサーバーをEC2 1台で運用していたが、以下の課題が顕在化してきた。
- 単一障害点: インスタンスが死んだらサービス全停止
- デプロイが手作業: SSH → git pull → pm2 restart の職人芸
- スケールできない: 負荷が増えても1台で耐えるしかない
これを ALB + ASG + CodeDeploy の構成に移行した記録。
Before / After
Before
Route 53 → Elastic IP → EC2 (1台)
├── NestJS (pm2)
└── RDS (直接接続)
- デプロイ: SSH して手動
- 障害時: 気づいたら手動復旧
- スケール: 不可
After
Route 53 → ALB → ASG (min=1, max=2)
├── EC2 (Launch Template + AMI)
│ └── NestJS (pm2)
└── RDS (Private Subnet)
デプロイ: GitHub → CodeDeploy → ASG
- デプロイ: CodeDeploy が S3 からアーティファクトを取得して自動展開
- 障害時: ASG がヘルスチェック失敗を検知して自動置換
- スケール: ASG の max を上げるだけ
移行でやったこと
- VPC を整備: Public/Private サブネットを 2AZ (1a, 1c) に作成
- AMI を作成: 動いている EC2 からカスタム AMI をスナップショット
- Launch Template を作成: AMI + インスタンスタイプ + SG + UserData を定義
- ALB を作成: Public Subnet に配置、ターゲットグループに ASG を紐づけ
- ASG を作成: min=1, max=2 で Launch Template を指定
- CodeDeploy を導入: appspec.yml + デプロイスクリプトを用意
- 旧 EC2 をターミネート: Elastic IP も解放
ハマりどころ
1. ヘルスチェックが通らない
ALB のヘルスチェックパスをデフォルトの / のまま設定していた。NestJS 側に /health エンドポイントを用意して、ターゲットグループのヘルスチェックパスを /health に変更して解決。
教訓: ヘルスチェック用のエンドポイントは先に作っておく。
2. CodeDeploy の appspec.yml でパスを間違える
# NG: 先頭スラッシュなしだとエラー
files:
- source: .
destination: app/syncwell-backend
# OK
files:
- source: /
destination: /home/ec2-user/syncwell-backend
destination は絶対パスで書く。相対パスだと CodeDeploy が謎のディレクトリに展開する。
3. ASG の EC2 に SSH できない
ASG が起動したインスタンスにはパブリック IP が自動付与されない設定になっていた。EC2 Instance Connect を使うために、SG にエンドポイントの IP レンジ (3.112.23.0/29) を許可して対応。
教訓: ASG 管理下のインスタンスには Elastic IP が使えないので、接続方法を先に決めておく。
移行してよかったこと
- デプロイが怖くなくなった: CodeDeploy がロールバックもしてくれる
- 夜中に落ちても自動復旧: ASG のおかげで心穏やか
- インフラが再現可能に: Launch Template + AMI があるので、環境の再構築が容易
おわりに
EC2 1台構成は立ち上げ期には楽だが、運用フェーズに入ると負債になる。ASG + CodeDeploy への移行は思ったほど大変ではなく、一番時間がかかったのはヘルスチェック周りのデバッグだった。