Container-Optimized OS に Nx で作成したWEBアプリを自動デプロイ
概要
ユーザーが数人でデータ量も少ない NX アプリケーションを簡単に安くデプロイ・運用したい。Container-Optimized OS で簡単にデプロイできそうだったので試した。
- DB と nginx は VM 上に Docker で用意
- https 化は Let's Encrypt を利用
- Github に push して自動デプロイ
前提
- Container-Optimized OS バージョン : cos-stable-97-16919-103-10
- VM の SSH 鍵を用意しておく
アプリケーション構成
アプリケーションは 2 つ。
- api : NestJS
- web : Next.js
メールアドレスによるユーザー登録、アイテムの登録・更新・削除・一覧表示・アイテム名検索だけのシンプルなアプリケーション。mui を使っているので多少重い。
api, web の project.json の build 設定 generatePackageJson を true にしておく。
apps/
api/
web/
docker/
api/Dockerfile
web/Dockerfile
db/Dockefile, my.cnf
docker-compose.prod.yml
Dockefile
FROM node:16-slim
WORKDIR /app
COPY dist/apps/api .
ENV PORT=3333
EXPOSE ${PORT}
RUN yarn install
RUN yarn add mysql2
CMD node ./main.js
FROM node:16-slim
WORKDIR /app
COPY dist/apps/web .
ENV PORT=4200
EXPOSE ${PORT}
RUN yarn install
CMD yarn start
FROM mysql:8
COPY my.cnf /etc/mysql/conf.d/my.cnf
RUN chmod 644 /etc/mysql/conf.d/my.cnf
nginx の設定と https 化
nginx で api と web を振り分ける(ドメインは同じ。/ を web に、/api を api に振り分け)。Root53 を使用していたので certbot/dns-route53 (docs) によりSSL証明書作成。
server {
listen 80;
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/certs/example.com/fullchain1.pem;
ssl_certificate_key /etc/nginx/certs/example.com/privkey1.pem;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /api {
proxy_pass http://api:3333;
}
location / {
proxy_pass http://web:4200;
}
}
VM のディレクトリ構成
後述の github action で app/ に dist や Dockerfile を転送。
/var/www/
app/
data/
db/db-prod/*
nginx/
certs/example.com/fullchain1.pem, privkey1.pem
conf.d/default.conf
docker-compose.prod.yml
Github Action
- api, web をそれぞれビルド
- web は
apps/web/.env
で NEXT_PUBLIC_* を定義しているため env を指定NEXT_PUBLIC_WEB_URL=$WEB_URL NEXT_PUBLIC_API_URL=$API_URL
- web は
- 必要なファイルを VM に転送
- VM に SSH で入って docker-compose up する
-
.env.prod
は事前に VM 上に設置しておく
-
name: deploy-prod
on:
push:
branches: [ deploy ]
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- uses: actions/cache@v3
id: node_modules_cache_id
env:
cache-name: cache-node-modules
with:
path: '**/node_modules'
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- if: ${{ steps.node_modules_cache_id.outputs.cache-hit != 'true' }}
run: yarn install
- run: yarn nx run api:build:production
- run: yarn nx run web:build:production
env:
WEB_URL: https://example.com
API_URL: https://example.com
- name: copy file via ssh key
uses: appleboy/scp-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
port: ${{ secrets.PORT }}
source: "dist/*,docker/*,docker-compose.prod.yml"
target: "/var/www/app"
rm: true
- uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
port: ${{ secrets.PORT }}
script_stop: true
script: |
cd /var/www
mv app/docker-compose.prod.yml ./
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd):$(pwd)" \
-w "$(pwd)" \
docker/compose:1.29.2 --env-file .env.prod -f docker-compose.prod.yml up -d --build
docker-compose.prod.yml
version: "3"
services:
api:
build:
context: app
dockerfile: docker/api/Dockerfile
container_name: api-prod
env_file:
- .env.prod
web:
build:
context: app
dockerfile: docker/web/Dockerfile
container_name: web-prod
env_file:
- .env.prod
db:
build: app/docker/db
container_name: db-prod
ports:
- ${DB_PORT}:3306
volumes:
- ./data/db-prod:/var/lib/mysql
command: "--general_log=0 --slow_query_log=0"
cap_add:
- SYS_NICE
environment:
MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
MYSQL_DATABASE: $MYSQL_DATABASE
MYSQL_PASSWORD: $MYSQL_PASSWORD
MYSQL_USER: $MYSQL_USER
TZ: 'Asia/Tokyo'
nginx:
image: nginx:1.23.0-alpine
container_name: nginx-prod
ports:
- "443:443"
volumes:
- ./data/nginx/certs:/etc/nginx/certs
- ./data/nginx/conf.d:/etc/nginx/conf.d
depends_on:
- api
- web
まとめ
e2-micro, e2-small だと Dockerfile のビルド時に CPU 使用率が 100% を軽く超えてしまった。
ブートディスクの容量が 10GB でも実際に使える容量は 5.8GB 程度しかなく、シンプルなアプリケーションなのに no space left on device になってしまった。
e2-medium, 20~30GB 程度にすれば問題なさそうだが、Cloud Build + Artifact Registry でやった方がビルドも速く、価格も安くなるかもしれない(ビルドした後、docker save & 転送 & docker load すれば Artifact Registry 不要でより安く済むかも)。
e2-medium, 20GB の月間予測:
余談
Console 上で容量増やしたあと、sudo /usr/share/cloud/resize-stateful
を叩くだけでパーティションを拡大できたが、ドキュメントにこの方法の記載がなさそう。
追記 2022-07-24
Cloud Build を構成し、ローカル環境でビルド実行した後、VM 上で pull && docker-compose up することに。
(そもそも個人開発で滅多にリリースもしないので、とりあえず自動でなくていい)
api と web のイメージは合わせて 2GB 弱あるので、Artifact Registry のイメージはリリース後に手動で削除(0.5GBまで無料、$0.10/GB/月)。
古いイメージが残るのでほかっておくと容量がどんどん増えてしまう。
Artifact Registry の料金
Cloud Build のビルドは15~19分程度(デフォルトのマシンタイプで、$0.003/ビルド分。1ヶ月に20分*10回=200分使っても、$0.6≒81円)。
マシンタイプを上げれば速度改善するぽいけど1ついいのにすると約5倍 の $0.016 という価格に。
Cloud Build の料金
Increase vCPU for builds
ドキュメントの Best practices for speeding up builds を試したけれど、イメージのキャッシュ、node_modules のキャッシュともに速度改善しなかった。
ビルドを Cloud Build に移したので VM は e2-small から e2-micro に変更。ストレージは 20 GB のまま。空き 13 GB もあるからもっと少なくてよかったかも。
以下手順。1~3 の自動化が出来ればいいのだけれど難しそう。ビルドはトリガーで出来ても、VM 上でコマンド叩くやり方がよくわからなかった。
- ローカル環境でビルド実行:
gcloud builds submit --config cloudbuild.yaml
- VM 上で叩くコマンド:
docker pull asia-northeast1-docker.pkg.dev/PROJECT_ID/REPO_NAME/api && \
docker pull asia-northeast1-docker.pkg.dev/PROJECT_ID/REPO_NAME/web && \
docker image prune -f && \
cd /var/www && \
docker-compose --env-file .env.prod -f docker-compose.prod.yml up -d --build
- Artifact Registry のイメージを削除(latest 含め全部削除):
gcloud artifacts docker images delete asia-northeast1-docker.pkg.dev/PROJECT_ID/REPO_NAME/api --quiet
gcloud artifacts docker images delete asia-northeast1-docker.pkg.dev/PROJECT_ID/REPO_NAME/web --quiet
Cloud Build 構成ファイル:
steps:
- name: node
entrypoint: yarn
args: [ 'install' ]
- name: node
entrypoint: yarn
args: [ 'nx', 'run', 'api:build:production' ]
- name: node
entrypoint: yarn
args: [ 'nx', 'run', 'web:build:production' ]
env:
- 'WEB_URL=https://example.com'
- 'API_URL=https://example.com'
- name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-f', './docker/api/Dockerfile', '.', '-t', 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/REPO_NAME/api' ]
- name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-f', './docker/web/Dockerfile', '.', '-t', 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/REPO_NAME/web' ]
images:
- 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/REPO_NAME/api'
- 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/REPO_NAME/web'
timeout: 1200s
Discussion