1. はじめに
小規模案件やPoC案件では、インフラ費用をできるだけ抑えたい要望があります。しかし、EC2やECSはコストが高く、AWS以外のVPSも検討しました。
ただし、AWS以外のVPSサービスを利用すると、キャッチアップコストがかかることや拡張性に不安がありました。そこで、AWS LightsailとGitHub Actionsを組み合わせた構成を検討しました。
Lightsail構成はコスト・パフォーマンスともに十分です。基本構成で約48ドル、WAF追加で約55ドル、バックアップ付きでも約83ドルに収まります。
この記事では、構成案の概要と、LightsailでのDocker ComposeおよびGitHub Actionsによるデプロイフローを中心にまとめています。
2. 対象読者
- Lightsailは未経験だが、AWSやCI/CDには少し触れたことがある
- GitHub Actions で Docker をビルドしてデプロイしたい
3. 目次
4. 環境
- AWS Lightsail: 4GB RAM / 2vCPU / 80GB SSD
- OS: Ubuntu 22.04 LTS
- コンテナ: Docker Compose
- ルーティング: ALB + ACM
- セキュリティ: WAF v2
- DNS: Route 53
- CI/CD: GitHub Actions + GHCR
5. 本編
5-1. 構成案
| 構成名 | 月額費用 | 内容 | 説明 |
|---|---|---|---|
| 構成a | $24 | Lightsail+CloudWatch | 最小限インフラ、監視のみのシンプル構成 |
| 構成b | $48 | Lightsail+ALB+ACM+CloudWatch | HTTPS対応、監視機能を含む基本構成 |
| 構成c | $55 | 構成b+WAF | セキュリティ対策を追加した推奨構成 |
| 構成d | $83 | 構成c+Lightsailバックアップ | バックアップ機能を含むフル構成 |
本記事では構成cを採用しています。
理由は、セキュリティ、コストバランス、拡張性の3点が、PoCや開発環境に最適だと判断したためです。構成bではWAFがなくセキュリティが不十分です。一方、構成dはバックアップコスト($28/月)が開発環境には過剰と考えました。
5-2. 全体構成
本記事で構築するWebアプリのインフラ構成は以下の通りです。
デプロイフロー :
- GitHub Actionsがビルド時にSecrets Managerから環境変数を取得
- ビルドしたコンテナイメージをGHCRにpush
- LightsailがGHCRからコンテナイメージをpullしてデプロイ
ユーザーアクセスフロー :
- インターネットからのアクセスをRoute 53が受け、ALBに転送
- ALBにはWAFが紐づき、不正アクセス対策
- ACMで証明書を管理し、HTTPS通信を実現
監視・運用:
- Lightsail上のCloudWatch AgentがログをCloudWatch Logsに送信
5-3. ディレクトリ構成
今回はフロントエンドとバックエンドの2層構成を前提にしています。
project/
├── front/ # フロントエンド
│ ├── Dockerfile # ローカル開発用
│ ├── Dockerfile.dev # GitHub Container Registry用(開発環境)
│ └── src/
├── backend/ # バックエンド
│ ├── Dockerfile # ローカル開発用
│ ├── Dockerfile.dev # GitHub Container Registry用(開発環境)
│ └── src/
├── deploy/
│ └── development/ # Lightsail開発環境専用
│ ├── docker-compose.yml
│ ├── nginx/
│ │ └── nginx.conf
│ ├── cloudwatch-agent/ # CloudWatch Agent設定
│ │ └── config.json
│ ├── logrotate/ # ログローテーション設定
│ │ └── logrotate.conf
│ └── README.md
├── docker-compose.yml # ローカル開発環境用
├── .github/
│ └── workflows/
│ └── deploy-dev.yml
└── README.md
deploy/development/ 配下を Lightsail 用の設定置き場にしています。
Dockerfile、nginx.conf、config.json、logrotate.conf の詳細な説明は割愛します。
それでは、次にLightsailのセットアップを進めていきます。
5-4. Lightsailの準備
- Lightsailインスタンス作成(Ubuntu 22.04)
- 静的IPをアタッチ
- ファイアウォール開放
- 22 (SSH)
- 80 (HTTP)
- 443 (HTTPS)
- 8080 (ALBヘルスチェック)
- SSHキーを取得してローカルに保管
SSHキーは GitHub Actions からのデプロイにも使用します。
5-5. Lightsail初期セットアップ
Lightsailインスタンス作成後、SSH接続して初期セットアップを実行します。
ログファイルは各コンテナごとに区切られたディレクトリに保存します。
以下のスクリプトを setup.sh として保存し、Lightsail上で実行します。
#!/bin/bash
set -e
set -u
LOG_FILE="/tmp/docker-setup-$(date +%Y%m%d-%H%M%S).log"
exec 1> >(tee -a "$LOG_FILE")
exec 2> >(tee -a "$LOG_FILE" >&2)
sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io
sudo apt install -y docker-compose
mkdir -p /home/ubuntu/app
mkdir -p /home/ubuntu/app/logs/{front,backend,mysql,redis,nginx}
chown -R ubuntu:ubuntu /home/ubuntu/app/logs
sudo usermod -aG docker ubuntu
スクリプト実行後、再度SSHログインして、Dockerが使えることを確認します。
docker --version
docker compose version
5-6. Docker Compose設定
Lightsailインスタンス上で動作させる docker-compose.yml の設定です。
deploy/development/ ディレクトリに配置することを想定しています。
services:
nginx:
image: nginx:1.25-alpine
container_name: dev-nginx
ports:
- "80:80"
- "8080:8080"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- /home/ubuntu/app/logs/nginx:/var/log/nginx
- backend-files:/var/www/html:ro
depends_on:
- front
- backend
restart: always
networks:
- app-network
front:
image: ${FRONT_IMAGE:-ghcr.io/mycompany/app/dev-app-front:latest}
container_name: dev-front
user: "1000:1000"
env_file:
- .env.front.development
environment:
- APP_NAME=front
volumes:
- /home/ubuntu/app/logs/front:/var/log/app/front
restart: always
networks:
- app-network
backend:
image: ${BACKEND_IMAGE:-ghcr.io/mycompany/app/dev-app-backend:latest}
container_name: dev-backend
user: "33:33"
env_file:
- .env.backend.development
environment:
- APP_NAME=backend
volumes:
- backend-files:/var/www/html
- /home/ubuntu/app/logs/backend:/var/log/app/backend
depends_on:
- db
- redis
restart: always
networks:
- app-network
db:
image: mysql:8.4
container_name: dev-mysql
environment:
MYSQL_DATABASE: app_db
MYSQL_USER: app_user
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
- /home/ubuntu/app/logs/mysql:/var/log/app/mysql
command: >
--mysql-native-password=ON
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--log-error=/var/log/app/mysql/mysql-error.log
ports:
- "127.0.0.1:3306:3306"
restart: always
networks:
- app-network
depends_on:
- cloudwatch-agent
redis:
image: redis:7.2-alpine
container_name: dev-redis
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
- /home/ubuntu/app/logs/redis:/var/log/app/redis
ports:
- "127.0.0.1:6379:6379"
restart: always
networks:
- app-network
cloudwatch-agent:
image: amazon/cloudwatch-agent:latest
container_name: dev-cloudwatch-agent
env_file:
- .env.cloudwatch.development
volumes:
- ./cloudwatch-agent/config.json:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json:ro
- ./cloudwatch-agent/config.json:/opt/aws/amazon-cloudwatch-agent/bin/default_linux_config.json:ro
- ./cloudwatch-agent/common-config.toml:/opt/aws/amazon-cloudwatch-agent/etc/common-config.toml:ro
- ./cloudwatch-agent/config.json:/etc/cwagentconfig/amazon-cloudwatch-agent.json:ro
- /home/ubuntu/app/logs:/var/log/app:ro
restart: always
networks:
- app-network
command: ["/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl", "-a", "fetch-config", "-m", "onPremise", "-s", "-c", "file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json"]
volumes:
mysql-data:
redis-data:
backend-files:
networks:
app-network:
driver: bridge
5-7. ALB / ACM / WAFのセットアップ概要
この記事ではDocker ComposeとCI/CDに焦点を当てているため、ALB・ACM・WAFの詳細な手順は省略します。
以下の流れで設定を進めてください。
- ACMで証明書を発行(ワイルドカード + ルート)
- ALBを作成し、HTTPSリスナーをACMに紐付け
- ALBのターゲットにLightsailのプライベートIPを登録
- WAFはALBに紐付け
ドメインの具体的な設定は省略しますが、Route 53でALB向けのエイリアスを作る前提です。
5-8. GitHub Actionsワークフロー
develop ブランチに push されたら自動デプロイ という想定です。
以下のワークフローを .github/workflows/deploy-dev.yml に配置します。
name: Deploy to Development Environment
on:
push:
branches: [develop]
paths:
- 'front/**'
- 'backend/**'
- '.github/workflows/develop-deploy.yml'
permissions:
contents: read
packages: write
jobs:
build-and-push:
runs-on: ubuntu-latest
strategy:
matrix:
app: [front, backend]
fail-fast: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set lowercase repository name
run: |
echo "REPO_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Generate environment file from Secrets Manager
run: |
generate_env_from_secret() {
local secret_name=$1
local output_file=$2
secret_json=$(aws secretsmanager get-secret-value \
--secret-id "$secret_name" \
--query SecretString \
--output text \
--region ap-northeast-1)
echo "$secret_json" | jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' > "$output_file"
}
generate_env_from_secret "dev-app-${{ matrix.app }}" ".env.${{ matrix.app }}.build"
- name: Copy environment file to build context
run: |
APP_NAME="${{ matrix.app }}"
cp ".env.${APP_NAME}.build" "${APP_NAME}/.env.${APP_NAME}.build"
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push ${{ matrix.app }}
uses: docker/build-push-action@v5
with:
context: .
file: ./${{ matrix.app }}/Dockerfile.dev
push: true
build-args: |
${{ matrix.app != 'backend' && format('NPM_TOKEN={0}', secrets.GITHUB_TOKEN) || '' }}
tags: |
ghcr.io/${{ env.REPO_NAME }}/dev-app-${{ matrix.app }}:latest
ghcr.io/${{ env.REPO_NAME }}/dev-app-${{ matrix.app }}:${{ github.sha }}
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set lowercase repository name
run: |
echo "REPO_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Generate environment files from Secrets Manager
run: |
generate_env_from_secret() {
local secret_name=$1
local output_file=$2
secret_json=$(aws secretsmanager get-secret-value \
--secret-id "$secret_name" \
--query SecretString \
--output text \
--region ap-northeast-1)
echo "$secret_json" | jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' > "$output_file"
}
generate_env_from_secret "dev-app-backend" ".env.backend.build"
generate_env_from_secret "dev-app-front" ".env.front.build"
- name: Prepare deployment environment files
run: |
cp .env.front.build .env.front.development
cp .env.backend.build .env.backend.development
DB_PASSWORD=$(grep '^DB_PASSWORD=' .env.backend.build | cut -d'=' -f2-)
MYSQL_ROOT_PASSWORD=$(grep '^MYSQL_ROOT_PASSWORD=' .env.backend.build | cut -d'=' -f2-)
REDIS_PASSWORD=$(grep '^REDIS_PASSWORD=' .env.backend.build | cut -d'=' -f2-)
cat > .env.development << EOF
DB_PASSWORD=${DB_PASSWORD}
MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
REDIS_PASSWORD=${REDIS_PASSWORD}
EOF
cat > .env.cloudwatch.development << EOF
AWS_ACCESS_KEY_ID=${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY=${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION=ap-northeast-1
EOF
- name: Deploy to Lightsail Development
env:
HOST: ${{ secrets.DEV_LIGHTSAIL_HOST }}
USERNAME: ${{ secrets.DEV_LIGHTSAIL_USERNAME }}
KEY: ${{ secrets.DEV_LIGHTSAIL_SSH_KEY }}
run: |
SSH_KEY_FILE=$(mktemp)
trap 'rm -f "$SSH_KEY_FILE" .env.*.development .env.development' EXIT
echo "$KEY" > "$SSH_KEY_FILE"
chmod 600 "$SSH_KEY_FILE"
SCP_OPTS="-o StrictHostKeyChecking=no -i $SSH_KEY_FILE"
DEST="$USERNAME@$HOST:/home/ubuntu/app/development/"
scp -r $SCP_OPTS deploy/development/ $USERNAME@$HOST:/home/ubuntu/app/
scp $SCP_OPTS .env.front.development .env.backend.development .env.cloudwatch.development $DEST
scp $SCP_OPTS .env.development $USERNAME@$HOST:/home/ubuntu/app/development/.env
SSH_OPTS="-o StrictHostKeyChecking=no -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -i $SSH_KEY_FILE"
ssh $SSH_OPTS $USERNAME@$HOST "
cd /home/ubuntu/app/development
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker pull ghcr.io/${{ env.REPO_NAME }}/dev-app-front:${{ github.sha }}
docker pull ghcr.io/${{ env.REPO_NAME }}/dev-app-backend:${{ github.sha }}
export FRONT_IMAGE=ghcr.io/${{ env.REPO_NAME }}/dev-app-front:${{ github.sha }}
export BACKEND_IMAGE=ghcr.io/${{ env.REPO_NAME }}/dev-app-backend:${{ github.sha }}
docker compose down
docker volume rm development_backend-files || true
docker compose up -d
docker images ghcr.io/${{ env.REPO_NAME }}/dev-app-front --format \"table {{.Repository}}:{{.Tag}}\" | grep -v \"latest\" | tail -n +3 | xargs -r docker rmi || true
docker images ghcr.io/${{ env.REPO_NAME }}/dev-app-backend --format \"table {{.Repository}}:{{.Tag}}\" | grep -v \"latest\" | tail -n +3 | xargs -r docker rmi || true
docker system prune -f
docker image prune -f
"
GitHub Secrets の設定
GitHubリポジトリの Settings > Secrets and variables > Actions から以下を設定します。
| Secret名 | 説明 | 備考 |
|---|---|---|
| DEV_AWS_ACCESS_KEY_ID | AWSアクセスキーID | IAMユーザーを作成し、Secrets Manager・CloudWatch Logsの権限を付与 |
| DEV_AWS_SECRET_ACCESS_KEY | AWSシークレットアクセスキー | IAMユーザー作成時に取得 |
| DEV_LIGHTSAIL_HOST | Lightsailの静的IP | Lightsailコンソールから静的IPを作成・アタッチして取得 |
| DEV_LIGHTSAIL_USERNAME | SSH接続用ユーザー名 | |
| DEV_LIGHTSAIL_SSH_KEY | SSH秘密鍵 | Lightsailコンソールからキーペアをダウンロード、pemファイルの中身をそのまま登録 |
注: GITHUB_TOKEN はGitHub Actionsによって自動的に提供されるため、手動での設定は不要です。
6. さいごに
Lightsail + GitHub Actionsの構成で、月額55ドルからの低コストで自動デプロイ環境を構築できました。
ただし、Lightsailにはいくつかの制限があります。スケーリングの柔軟性がEC2/ECSより低く、大規模トラフィックには不向きです。また、VPC内の他のAWSサービスとの連携にも制約があります。
この構成はPoC・開発環境に最適です。本番環境への展開を検討する際は、トラフィック量やスケール要件を踏まえて、EC2/ECSへの移行も視野に入れることをおすすめします。