Next.jsを環境変数を渡しつつ自動デプロイするようにCDを構築してみたので雑ですがまとめてみました。
構成
- どっかに配置
- GitLab
- GitLab Runner
- MySQL
- デプロイ用サーバ
- Next.js (Systemd)
デプロイ用ホストの設定
ユーザー
sudoをパスワードなしで実行できるユーザーを作成
$ useradd -m deploy -s /usr/bin/bash -G sudo
$ visudo
...
deploy ALL=(ALL) NOPASSWD: ALL
Next.js用のUnitファイル
$ vim /lib/systemd/system/nextjs@.service
[Unit]
Description=NodeJS server, NextJS frontend
After=network.target
[Service]
Type=simple
Restart=on-failure
RestartSec=10
EnvironmentFile=/etc/default/%i
WorkingDirectory=/home/deploy/%i/
ExecStartPre=/usr/bin/yarn --production
ExecStartPre=/usr/bin/yarn build
ExecStart=/usr/bin/yarn start -- -p ${PORT}
[Install]
WantedBy=multi-user.target
$ systemctl daemon-reload
Next.jsアプリケーション
事前にGitLabのリポジトリとGitLab Runner(shell executor)を登録しておく
※リポジトリはpublicにする必要がある。
ここからローカルで作業
$ npm install -g yarn npx
$ npx create-next-app@latest next-app --typescript
$ cd next-app
$ git remote add origin <gitlab_remote_url>
$ git push origin HEAD
.gitlab-ci.yml
Next.jsからMySQLへのアクセスとJWTトークンの操作をするとして環境変数を用意しています。
yml=.gitlab-ci.yml
variables:
SOURCE_URL: "$CI_PROJECT_URL/-/archive/$CI_COMMIT_REF_NAME/$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.tar.gz"
DEPLOY_HOST: "$DEPLOY_HOST"
DEPLOY_HOST_SSH_USER: "$DEPLOY_HOST_SSH_USER"
DEPLOY_HOST_SSH_PORT: "$DEPLOY_HOST_SSH_PORT"
SSH_PRIVATE_KEY: "$SSH_PRIVATE_KEY"
DEPLOY_PORT: "$DEPLOY_PORT"
MYSQL_HOST: "$MYSQL_HOST"
MYSQL_PORT: "$MYSQL_PORT"
MYSQL_USERNAME: "$MYSQL_USERNAME"
MYSQL_PASSWORD: "$MYSQL_PASSWORD"
JWT_SECRET: "$JWT_SECRET"
BETA_DEPLOY_HOST: "$BETA_DEPLOY_HOST"
BETA_DEPLOY_HOST_SSH_USER: "$BETA_DEPLOY_HOST_SSH_USER"
BETA_DEPLOY_HOST_SSH_PORT: "$BETA_DEPLOY_HOST_SSH_PORT"
BETA_SSH_PRIVATE_KEY: "$BETA_SSH_PRIVATE_KEY"
BETA_DEPLOY_PORT: "$BETA_DEPLOY_PORT"
BETA_MYSQL_HOST: "$BETA_MYSQL_HOST"
BETA_MYSQL_PORT: "$BETA_MYSQL_PORT"
BETA_MYSQL_USERNAME: "$BETA_MYSQL_USERNAME"
BETA_MYSQL_PASSWORD: "$BETA_MYSQL_PASSWORD"
BETA_JWT_SECRET: "$BETA_JWT_SECRET"
stages: # List of stages for jobs, and their order of execution
- build
- test
- deploy
build-job:
stage: build
before_script:
- sudo apt install -y nodejs
- npm install -g yarn
- yarn
script:
- echo 'MYSQL_PASSWORD=$MYSQL_PASSWORD\nJWT_SECRET=$JWT_SECRET' > ./.env.local
- yarn build
# unit-test-job: # This job runs in the test stage.
# stage: test # It only starts when the job in the build stage completes successfully.
# script:
# - echo "Running unit tests."
lint-test-job:
stage: test
before_script:
- sudo apt install -y nodejs
- npm install -g yarn
- yarn
script:
- yarn lint
beta-deploy-job:
stage: deploy
only:
- release
before_script:
- sudo apt install -y openssh-client
- eval "$(ssh-agent -s)"
- echo "$BETA_SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo -e "CI_PROJECT_NAME=$CI_PROJECT_NAME\nSOURCE_URL=$SOURCE_URL\nCI_COMMIT_REF_NAME=$CI_COMMIT_REF_NAME\nDEPLOY_PORT=$BETA_DEPLOY_PORT\nMYSQL_HOST=$BETA_MYSQL_HOST\nMYSQL_PORT=$BETA_MYSQL_PORT\nMYSQL_USERNAME=$BETA_MYSQL_USERNAME\nMYSQL_PASSWORD=$BETA_MYSQL_PASSWORD\nJWT_SECRET=$BETA_JWT_SECRET" > deploy_env
- scp -P "$BETA_DEPLOY_HOST_SSH_PORT" deploy_env "$BETA_DEPLOY_HOST_SSH_USER"@"$BETA_DEPLOY_HOST":~/.ssh/environment.beta
- >
ssh "$BETA_DEPLOY_HOST_SSH_USER"@"$BETA_DEPLOY_HOST" -p "$BETA_DEPLOY_HOST_SSH_PORT" 'export $(cat ~/.ssh/environment.beta | grep -v ^#) &&
sudo systemctl stop nextjs@"$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME".service &&
sudo rm -r "$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME"* 2> /dev/null || true'
script:
- >
ssh "$BETA_DEPLOY_HOST_SSH_USER"@"$BETA_DEPLOY_HOST" -p "$BETA_DEPLOY_HOST_SSH_PORT" 'export $(cat ~/.ssh/environment.beta | grep -v ^#) &&
curl -LO "$SOURCE_URL" &&
tar xf "$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME".tar.gz &&
echo 'MYSQL_HOST=$MYSQL_HOST\nMYSQL_PORT=$MYSQL_PORT\nMYSQL_USERNAME=$MYSQL_USERNAME\nMYSQL_PASSWORD=$MYSQL_PASSWORD\nJWT_SECRET=$JWT_SECRET' > ./"$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME"/.env.local &&
echo 'PORT=$DEPLOY_PORT' > /etc/default/"$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME" &&
sudo systemctl start nextjs@"$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME".service'
deploy-job: # This job runs in the deploy stage.
stage: deploy # It only runs when *both* jobs in the test stage complete successfully.
only:
- main
before_script:
- sudo apt install -y openssh-client
- eval "$(ssh-agent -s)"
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo -e "CI_PROJECT_NAME=$CI_PROJECT_NAME\nSOURCE_URL=$SOURCE_URL\nCI_COMMIT_REF_NAME=$CI_COMMIT_REF_NAME\nDEPLOY_PORT=$DEPLOY_PORT\nMYSQL_HOST=$MYSQL_HOST\nMYSQL_PORT=$MYSQL_PORT\nMYSQL_USERNAME=$MYSQL_USERNAME\nMYSQL_PASSWORD=$MYSQL_PASSWORD\nJWT_SECRET=$JWT_SECRET" > deploy_env
- scp -P "$DEPLOY_HOST_SSH_PORT" deploy_env "$DEPLOY_HOST_SSH_USER"@"$DEPLOY_HOST":~/.ssh/environment
- >
ssh "$DEPLOY_HOST_SSH_USER"@"$DEPLOY_HOST" -p "$DEPLOY_HOST_SSH_PORT" 'export $(cat ~/.ssh/environment | grep -v ^#) &&
sudo systemctl stop nextjs@"$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME".service &&
sudo rm -r "$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME"* 2> /dev/null || true'
script:
- >
ssh "$DEPLOY_HOST_SSH_USER"@"$DEPLOY_HOST" -p "$DEPLOY_HOST_SSH_PORT" 'export $(cat ~/.ssh/environment | grep -v ^#) &&
curl -LO "$SOURCE_URL" &&
tar xf "$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME".tar.gz &&
echo 'MYSQL_HOST=$MYSQL_HOST\nMYSQL_PORT=$MYSQL_PORT\nMYSQL_USERNAME=$MYSQL_USERNAME\nMYSQL_PASSWORD=$MYSQL_PASSWORD\nJWT_SECRET=$JWT_SECRET' > ./"$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME"/.env.local &&
echo 'PORT=$BETA_DEPLOY_PORT' > /etc/default/"$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME" &&
sudo systemctl start nextjs@"$CI_PROJECT_NAME"-"$CI_COMMIT_REF_NAME".service'
GitLabのリポジトリに設定する変数
-
main
用 (本番環境)- DEPLOY_HOST
- DEPLOY_HOST_SSH_USER
- DEPLOY_HOST_SSH_PORT
- SSH_PRIVATE_KEY
- DEPLOY_PORT
- MYSQL_HOST
- MYSQL_PORT
- MYSQL_USERNAME
- MYSQL_PASSWORD
- JWT_SECRET
-
release
用 (β版用)- BETA_DEPLOY_HOST
- BETA_DEPLOY_HOST_SSH_USER
- BETA_DEPLOY_HOST_SSH_PORT
- BETA_SSH_PRIVATE_KEY
- BETA_DEPLOY_PORT
- BETA_MYSQL_HOST
- BETA_MYSQL_PORT
- BETA_MYSQL_USERNAME
- BETA_MYSQL_PASSWORD
- BETA_JWT_SECRET
deployステージで行っていること
- デプロイ用のホストで利用する環境変数のファイル作成・scpでの送信
- デプロイ用のホストにSSH接続
- リポジトリからソースをダウンロード
- Next.js内で利用する環境変数(DBのホスト・接続ユーザー情報等)用のファイルを作成 (
.env.local
) - Next.jsをビルド
- Next.jsに割り当てるポートを環境変数として指定 (
/etc/default/<project_name>-<branch-name>
) - systemdでNext.jsをデーモンで起動
今回の構成ではgit-flowでの開発を想定してrelease
ブランチをβ版、main
ブランチを本番環境としてそれぞれブランチが更新されたときに自動デプロイするように作成しています。
あとがき
今考えている改善点
- curlでソースコードをDLしている部分をgitlab-runnerからscpで渡すようにすればリポジトリをprivateにしても動作しそう。
- Ansibleを介してデプロイさせたら綺麗にできそう。(gitlab-runnerからansibleを実行する。)
- デプロイ先のサーバーがリバースプロキシを介して公開している場合のnginxのコンフィグ切り替えもできたら良さそう。
- デプロイ用のサーバのAisibleのplaybookを用意すればちょっとは使えそう。(コンテナを使わない場合は特に)
- gitlab-runnerもAnsibleにまとめてしまえばもっと良くなる?
注意点
- 自動デプロイを実行するブランチをGitLabのリポジトリ設定でプロテクトしておかないと事故る可能性がある。
- GitLab上で登録する環境変数をProtectedとかMaskedにして環境変数を見れる範囲を制限しないと危険。
- sshキーとかシークレットが覗けてしまう。