【備忘録】home-serverに自動デプロイする

## やりたいこと
外部公開していないローカルのサーバーで自動デプロイしたい

## 今回の方法
Nextをビルドして、動かすだけの構成なのでshellスクリプトを動かす構成にする。
[tailscale](https://tailscale.com/)を利用しているので、そちらでGitHub Actionsを利用するような感じにしてもよいが、ほかでも応用が利きそうなshellスクリプト方式にした。

```mermaid
sequenceDiagram
    participant D as Developer
    participant M as Master Branch
    participant H as Home Server (https://home-server/deploy/sample-branch)
    participant N as Nginx
    participant F as Flask (localhost:9000)
    participant S as Shell Script

    D->>M: 1. masterブランチにデプロイ
    D->>H: 2. URLにアクセス
    H->>N: 3. リクエストを転送
    N->>F: 3. リバースプロキシ (localhost:9000)
    F->>S: 4. デプロイ用シェルスクリプトを実行
    S-->>F: 実行結果を返す
    F-->>N: 4. レスポンスを返す
    N-->>H: レスポンスを返す
    H-->>D: レスポンスを表示
```

## 実際にやってみる

### 前提
pm2やnginxは元から今回は入っています。

### GitHub tokenの事前準備

https://qiita.com/koinunopochi/items/23e89815fccc8a892d4e

今回はpullで非公開repoなので、tokenを利用して引っ張るための準備を上記を参考に行う

:::note warn
今回は外部公開しないのでこれですが、sshで設定したほうが良いと思います
:::

### 1回git pullスル

```bash
# tokenをキャッシュする。多少のセキュリティリスクあり
git config --global credential.helper store
git pull origin master
```

https://qiita.com/aqua_ix/items/0433f85330087c62bffa

平文でトークンが保存されるので、危険ではありますが特に外部公開するつもりはないので良しとします

### スクリプトの作成
適宜、必要な形式に変更するようにしてください。
```shell:/home/youruser/deploy.sh
#!/bin/bash

# nvm の手動ロード。パスが見つからない問題に対処
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# デプロイ対象のディレクトリ
APP_DIR="/home/youruser/sample"

# ログ出力
echo "Starting deployment..." >> /var/log/deploy.log 2>&1

# リポジトリの最新コードを取得
cd $APP_DIR || exit
git pull origin main >> /var/log/deploy.log 2>&1

# アプリケーションのビルド
cd next-app
npm install >> /var/log/deploy.log 2>&1
npm run build >> /var/log/deploy.log 2>&1

# PM2を再起動
pm2 restart sample >> /var/log/deploy.log 2>&1

echo "Deployment completed." >> /var/log/deploy.log 2>&1
```

実行権限を与えます
```bash
sudo chmod +x /home/youruser/deploy.sh
```

ログファイルを作成し、書き込めるように設定します。今回は今のユーザーとします。
```bash
# 空ファイルを作成
sudo /var/log/deploy.log
# 権限を今のユーザーに変更する
sudo chown $(whoami):$(whoami) /var/log/deploy.log
```

試しに実行してみます。
```bash
bash /home/youruser/deploy.sh
```
併せて、実行ログもみてみるとデバッグがはかどるでしょう。
```bash
tail -f /var/log/deploy.log
```

### nginxにルートを準備する
今回は、deploy配下にまとめるイメージで、作っています
```conf:/etc/nginx/sites-available/default など
    location /deploy/sample {
        # allow 127.0.0.1; # 自分だけ実行できるように
        # deny all;
        proxy_pass http://127.0.0.1:9000;
    }
```

設定の反映
```
sudo nginx -t
sudo systemctl reload nginx
```

### deployサーバーの準備
簡単にflaskで作っていきます。なんとなくflaskなだけで、expressだろうがhonoだろうが何でもよいです。
```bash
# venvのインストール
python -m venv .venv
# venvの起動
. .venv/bin/activate
pip install flask
```
:::note alert
何も考えずにgetにしていましたが、アクセスしただけでdeploy走るの普通に嫌なので追記した奴を使うようにしてください。
:::

flaskのコードを準備します
```py:/home/youruser/deploy_server.py
from flask import Flask
import subprocess

app = Flask(__name__)

@app.route("/deploy/sample", methods=["GET"])
def deploy():
    subprocess.Popen(["/home/youruser/deploy.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    return "Deployment started", 200

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=9000)

```
とりあえず起動してみて、動きを確認してみます。
自分の時は、flaskで呼び出すとnpmなどのパスを認識してくれなくて修正の必要がありました。
```bash
python /home/youruser/deploy_server.py
```

### flaskを永続化する

```conf:/etc/systemd/system/deploy_server.service
[Unit]
Description=Deployment Trigger Server
After=network.target

[Service]
User=youruser
WorkingDirectory=/home/youruser
ExecStart=/usr/bin/python3 /home/youruser/deploy_server.py
Restart=always

[Install]
WantedBy=multi-user.target
```

実行し直します
```bash
sudo systemctl daemon-reload
sudo systemctl enable deploy_server
sudo systemctl start deploy_server
```

## 追記:実行結果を返すようにする
```py
from flask import Flask, request
import subprocess

app = Flask(__name__)

@app.route("/deploy/sample", methods=["GET", "POST"])
def deploy():
    if request.method == "POST":
        # POSTの場合、デプロイを実行
        subprocess.Popen(["/home/youruser/deploy.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        return "Deployment started. <a href='/deploy/log'>View deploy log</a>", 200
    else:
        # GETの場合、デプロイボタンを含むフォームを返す
        return """
        <form method="POST" action="/deploy/sample">
            <button type="submit">Deploy Now</button>
        </form>
        <a href="/deploy/log">View deploy log</a>
        """, 200

@app.route("/deploy/log", methods=["GET"])
def view_log():
    try:
        # /var/log/deploy.log の最後の50行を取得
        with open("/var/log/deploy.log", "r") as log_file:
            lines = log_file.readlines()
            last_50_lines = lines[-50:]
            log_content = "".join(last_50_lines)
            return f"<pre>{log_content}</pre><br><a href='/deploy/sample'>Back to deploy</a>", 200
    except FileNotFoundError:
        return "Log file not found.", 404
    except Exception as e:
        return f"Error reading log: {str(e)}", 500

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=9000)
```

flaskを再起動する
```
sudo systemctl restart deploy_server
```

これを追記する
```conf:/etc/nginx/sites-available/default など
    location /deploy/log {
        # allow 127.0.0.1; # 自分だけ実行できるように
        # deny all;
        proxy_pass http://127.0.0.1:9000;
    }
```
再起動する
```
sudo systemctl restart nginx
```

## まとめ
後はログサイズなどを`logrotate`を利用したりして管理するとより本番っぽくなるが、そこまで大量にデプロイするつもりもないのでしばらくは大丈夫だと思いたい

面倒くさくなったらログの管理とかもしようと思う