🐈

初めて VPS で Flask アプリをデプロイしてみた

2022/06/11に公開約11,000字

前回 GCP のコストの記事を書いといてなんなんだけども、結局デプロイしようとしていたものは VPS に載せました。この記事では VPS にデプロイするときに調べたこととかどうやってやったかを書いていきます。主に VPS 触ったことないとか初心者向けの内容です。ちょっとですが叩いたコマンドとかも書いてあります。

背景

そもそも今回まで「VPS」「レンタルサーバー」がどういうものかわかってなくて、「なんとなくムズカシイもの」として敬遠してきた結果、全てのアプリを GCP か Heroku でデプロイしてきました。が、今回はコスト計算がやっぱりめんどくさくなったのと、スクレイピングアプリを作っていた関係で、 Flask アプリの他にサーバーに chromedriver をインストールする必要がありました。これが調べたら Docker 使ってごにょごにょして...っていう感じらしく、Docker も触ったことないので大変そうだった。

そんなところで、「レンタルサーバー」を調べてみたら月200円くらいで借りれると分かったので、クラウドからレンタルサーバー・VPS に移行してみました。

レンタルサーバー・VPS の違い

大量に記事が出てるので詳細は割愛しますが、今回に関係するところで言うと「レンタルサーバー」は1つのサーバーをみんなで共有して使うサーバーで、 VPS (virtual private server) は自分専用のサーバーがもらえます。WordPress とか server・DB しか使わないアプリとか、インストールするものが決まっている場合はレンタルサーバーでいいんだけど、今回は chromedriver のインストールが必要だったので、自分は VPS を使いました。一般的に、 apt-get しないといけないなら VPS、サーバにプリインストールされてるライブラリで事足りるならレンタルサーバー になります。

値段で言うと、だいたい

  • レンタルサーバー: 200 yen/month 〜
  • VPS: 500 yen/month 〜

今回は Chrome がメモリを食う関係で、一番低いスペックだとだめだったので、自分は月900円ちょい払ってます。

どこの VPS を使うか
レンタルサーバー比較.comを見たところ、 VPS だと、 Serverman < 使えるねっと < さくら VPS の順で安かったです。

とりあえず一番安い Serverman を使ってみたんだけど、 yum update でエラーが出たり、それが解決しても別のエラーが出たりと環境構築が全然進みませんでした(Serverman のエラーの詳細はここの記事に書いてある)。心が折れたので、安定のさくらを試すことにしました。

さくら VPS は2週間のお試し期間があるし、Serverman みたいな謎のエラーもなくスムーズに環境構築ができました。こういう記事があるのも好き↓

https://knowledge.sakura.ad.jp/8218/

VPSを作る

(サーバーのスペックを選んでクレカを入れるということです)

さくらの VPS インスタンスを立てるときには

  • リージョン
  • スペック (メモリ・CPU・ストレージ)
  • OS

を選びます。


自分は、最初に↓の設定でいきました。

  • リージョン: 大阪第3
  • スペック: 512M/25GB → 1G/50GB
  • OS: Ubuntu

リージョンは、自分用なので割とどこでも良かったので一番安いところにしました。スペックは、よくわからなかったので一番安いやつにしたのですが、後述するようにメモリ不足でサーバーがクラッシュしたので、結局 1G にスペックアップしました。

OS は Linux のディストロの違いがよくわからなかったので、"Linux beginner friendly" でググって出てきた Ubuntu にしました。今のところ「わからん...」となってないので、多分次回も Ubuntu にすると思います。

環境構築

前述の通り Docker がわからないのと、そもそも Linux 自体2時間くらいしか触ったことがないので、SSH のやり方から調べて、1個ずつ apt-get でインストールしました。

とはいえ、 GCP とかと違って、ローカルをどのサービスに載せるかみたいなことを考えなくて良いのはめっちゃ楽。

 # ポート番号はデフォルトから変えてなければ22番
$ ssh ubuntu@SERVER_IP_ADDRESS -p 22
ubuntu@SERVER_IP_ADDRESS's password:
# パスワードを入力

ちなみに、最初に SSH ログインするユーザ名が root じゃなくて ubuntu なことに1時間以上気づかず、めっちゃ謎でした。

さくらの初期ユーザ名一覧↓
https://manual.sakura.ad.jp/vps/support/info/administrative-userl-login.html#id3)

最終的にインストールしたものはこれです↓

$ sudo apt-get install -y python3-pip
$ sudo apt-get install -y libpq-dev python-dev
$ sudo apt-get install -y chromium-browser
$ sudo apt-get install -y chromium-chromedriver
$ sudo apt-get install -y postgresql postgresql-contrib postgresql-client
$ sudo apt-get install -y redis-server
$ snap install ngrok

Python3 は元々入ってたけど、 pip3 とか PostgreSQL は別で入れる必要がありました。Redis もなんとなく使ってみました。

インストール作業をするときには DigitalOcean ってとこのサイトにめちゃくちゃお世話になりました。アイキャッチのくらげもかわいいし、記事もわかりやすいのでおすすめです。ここも VPS とかやってるぽい。

https://www.digitalocean.com/community/tutorials/how-to-install-python-3-and-set-up-a-programming-environment-on-ubuntu-22-04

ngrok をインストールしている最後の行で snap って書いてありますが、 Snap も APT のようなパッケージマネージャです。主な違いは APT と違って依存関係をパッケージごとに管理しているので、他のパッケージと依存でコンフリクトしないところです。

詳しく比較してくれてる記事↓

https://raspberrytips.com/snap-vs-apt

最初のデプロイ

ここまでで、 python main.py をすれば curl localhost:5000 で Hello World が帰ってくるようになったのですが、本番稼働にはまだ問題が2個あります。

  1. UI から API エンドポイントを叩けるようにする
  2. SSH 切った後も python main.py が動いていてほしい

1 は、既に ngrok をローカル開発から使っていたので本番でもそのまま ngrok を使いました。ちなみに ngrok はローカルのポートをインターネット全体に公開してくれるサービスです。

$ python main.py
# localhost:5000 でサーバーが立つ

$ ngrok http 5000 # 5000 番に ngrok を通す
# → http://some-random-domain.ngrok.io からアクセスできるようになる

無課金だと時間ごとにランダムなドメインが割り当てられますが、課金すると時間無制限で自分の好きなドメイン名を使えます。

2 については、この Stack Overflow にいろいろ候補があったのを見て、一番シンプルそうな nohup コマンドで行くことにしました。

nohup is a POSIX command which means "no hang up". Its purpose is to execute a command such that it ignores the HUP signal and therefore does not stop when the user logs out.
--- Wikipedia より

"does not stop when the user logs out" ということなので、これなら SSH を切ってもサーバーが動き続けられます。サーバーを止めたいときは ps -ef でポート番号を探して kill -9 PID をします。

# サーバー起動
$ nohup python main.py &

# サーバー停止
$ ps -ef | grep python # ポート番号を探す
$ kill -9 YOUR_PYTHON_PID

というわけで、最終的にデプロイするときに走らせるコマンドはこんな感じになりました。ログはわかりやすく ~/logs/ 以下に格納しました。

# サーバー起動してログの出力先を指定
$ nohup python main.py > ~/logs/server.log 2>&1 &

# ngrok 起動
$ nohup ngrok http --region=jp --hostname=wawa.jp.ngrok.io 5000 > ~/logs/ngrok.log 2>&1 &

メモリ不足によるサーバークラッシュ

※クラッシュっていうと大事に聞こえますが、自分しか使ってないので「なんか動かんな」くらいです

ログを見てみると、こんな感じの謎のエラーが出ました。

Stacktrace:
#0 0x55bad759c823 <unknown>
#1 0x55bad7305638 <unknown>
#2 0x55bad733ace7 <unknown>
#3 0x55bad733aeb1 <unknown>
#4 0x55bad736d564 <unknown>
#5 0x55bad735815d <unknown>
#6 0x55bad736b261 <unknown>
#7 0x55bad7358023 <unknown>
#8 0x55bad732e1cc <unknown>
#9 0x55bad732f2f5 <unknown>
#10 0x55bad75dd116 <unknown>

さすがに全くわからなかったので友達のエンジニアに見てもらったところ、メモリ不足が原因っぽいということでした。 top コマンドで見てみるとたしかに free のメモリが2ケタ MiB しかなくて、 512 MiB メモリーなので 80% 以上使ってました。

そこからサーバーを立てる前には仮想マシンで必要なスペックを見積もると良いことを教わったので、 ローカルで VirtualBox x Vagrant を立ててからスペックアップをすることにしました。

2022-06-12 追記

スペックアップしてもまだ謎の Stack trace は出ていました。Selenium からのエラーメッセージは次のいずれかでした↓

  • Can not connect to the Service /usr/lib/chromium-browser/chromedriver
  • chrome not reachable
  • unknown error: DevToolsActivePort file doesn't exist

何個か記事を読んでみましたが、だいたい「VM のメモリが小さすぎると起きる」とか「余分なプロセスを kill せよ」とかだったので、 ps aux | grep chrom から START フィールドが明らかに今使用中のプロセスじゃないものだけ kill -9 しました。こうすると、だいたい free -mused が < 400 MiB におさまったので、これでしばらくは大丈夫そうです(これをする前は 500 超えとかしてた)。

2022-06-14 追記

ローカルで --headless をオフにして調べたところ、 WebDriverWait が失敗したときにこの謎の Stacktrace が出るようでした。

wait = WebDriverWait(driver, 10)
try:
  container = wait.until(EC.presence_of_element_located((By.ID, "hogehoge")))
except Exception as e:
  print(f"error! {e}") # ここで Stacktrace が出る

公式にも wait が try ブロックで囲まれてるので多分そういうことだと思います。

https://selenium-python.readthedocs.io/waits.html#explicit-waits

仮想マシンでスペックの見積もり

ほんとは一番最初にあるべき手順ですが、ローカルの MacOS に VirtualBox と Vagrant を入れました。Vagrant は brew で簡単に入ったのですが、VirtualBox が権限周りのエラーで全く入りませんでした。最終的にはここの記事のやり方でいけました。

https://www.vagrantup.com/downloads

Vagrant と VirtualBox の関係ですが、 VirtualBox が仮想環境で、 Vagrant が環境を管理しやすくしてくれるものです。

# VirtualBox がインストールされてる前提

$ cd my-project

# 初期化
$ vagrant init ubuntu/focal64
# Vagrantfile っていう設定ファイルが作られる

# 設定
$ vim Vagrantfile
# apt-get とかを Vagrantfile に書き込んでいく

# 起動
$ vagrant up

# 停止
$ vagrant halt # halt はシャットダウン、suspend スリープ (i.e. マシンの状態を保存して停止)

vagrant init <box-name> で仮想環境を「Box」という概念で管理します。一から自分の Box を作ることもできますが、今回は Ubuntu のマシンならなんでも良かったので、公開されている Box を使いました。

ここのチュートリアルが分かりやすくて、基本的にこのやり方に沿ってます。

https://blog.ruanbekker.com/blog/2021/08/14/a-tour-with-vagrant-and-virtualbox-on-mac/

Vagrantfile はインストールしたいもののほか、ローカル <-> VM で同期するフォルダも設定できます。言い換えると、ローカルでコードを変更したら git pull しなくても VM 側で変更が反映されます。自分の書いた Vagrantfile はこんな感じになりました。

# -*- mode: ruby -*- # Ruby で書ける
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  # box を指定
  config.vm.box = "ubuntu/focal64"
  # ローカルの現在のフォルダ (= プロジェクトのフォルダ) を VM の my-project に同期
  config.vm.synced_folder ".", "/home/vagrant/my-project"
  # インストールしたいものを書く
  config.vm.provision "shell", inline: <<-SHELL
    sudo apt-get update
    sudo apt-get -y upgrade
    sudo apt-get install -y python3-pip
    sudo apt-get install -y libpq-dev python-dev
    sudo apt-get install -y chromium-browser
    sudo apt-get install -y chromium-chromedriver
    sudo apt-get install -y postgresql postgresql-contrib postgresql-client
    sudo apt-get install -y redis-server 
    snap install ngrok
	SHELL
end

設定を変更したら、 provision の変更なら vagrant provision そのほかは vagrant reload を叩きます。

後から気づいたんですが、先に Vagrant の手順を踏んでおけばサーバーで何を apt-get すればいいかわかって良さそう。

で、こっちの VM でいろいろインストールした後、サーバを動かしてエンドポイントを叩いたところ、やっぱり 400-500 MiB くらい使ってました。流石にギリギリすぎるのでさくら VPS をスペックアップすることにしました。

スペックアップ

さくら VPS だとめちゃくちゃ簡単にスペックアップできます。サーバーの管理画面からサーバーをシャットダウンして、「スケールアップ」ボタンを押すとスペックアップ先を選択して、あとはサーバーを再起動すれば終わりです。イチから環境構築し直しかと思ったので安心しました。

ちょっと改修

最初の環境構築とスペックアップを通してだいたい動きそうになったのですが、もうちょっとだけ改修しました。

エラーの通知

メモリ不足の件は解決したとはいえ、何かしらのエラーが起きる可能性はまだあります。アプリケーションが静かに死ぬのはやめてほしいので、何かしらの方法でスマホに通知させたいと考えました。

前も似たような理由で Sentry 的なライブラリを探したので、Honeybadger ならスマホアプリで通知を受け取れるのがわかっていました。が、今回はもういろいろライブラリを入れたりドキュメントを読む気力がなかったので、手軽に Slack App で通知させることにしました。

Slack には "Incoming Webhook" という機能があって、これを使うとあらかじめ与えられた URL に POST request を送るだけで、指定のチャンネルに Slack App 経由でメッセージが送れます。チャンネルごとに Webhook URL が設定できるので、たとえば

俺の個人開発用ワークスペース
|- # hoge-python-app-alerts
|- # foo-rails-app-alerts
|- ...

みたいな構成にして、自分用のワークスペースの中にプロジェクトごとのエラー通知先チャンネルを設定することもできます。 LINE とかと違って、いくら Webhook 経由でメッセージを送ってもタダなのも安心です。

Incoming Webhook を使う Slack アプリの作り方は、ドキュメントに詳しく書いてありますが、箇条書きすると、

  • https://api.slack.com/apps から "Create new app" ボタンを押す
  • "From scratch" でアプリを作る(追加の permission とかは不要だから)
  • Incoming Webhooks を有効化
  • Webhook を設定するチャンネルを指定 → URL をどっかにメモる
  • チャンネルに Slack App を招待する
  • curl なり python なりで Webhook URL にメッセージを送る

Slack API のエンドポイントを叩いてメッセージを送信する方法もありますが、こっちだと権限の設定をしなきゃいけないので Webhook の方が楽です。

で、 flask app の方では Flask 全体のエラーハンドラーに設定はせず、1つのエンドポイントの try...exceptexcept ブロックでだけ、 Slack メッセージを送信するようにしました。業務用のサーバーはないので、とりあえず一番エラーが起きそうなとこだけ監視する感じです。

Redis 導入

↑のエラーメッセージのところで、同じメッセージが連続して送られないように Redis を使ってフラグも立てました(正確には「エラー」メッセージというより INFO くらいのものです)。

Redis を使うには、

  • アプリケーションで pip install redis-py
  • OS で sudo apt-get install -y redis-server

をしました。OS の方は redis-cli で redis の中身とかを確認できるようにインストールしました。インストールはまた DigitalOcean の記事を読んでやりました。あとはここの記事を見ながら GET したり SET したりしてます。使わなくなったキーの DEL は今のところ気が向いたら手動でやっています。

手順まとめ

サーバーのスペックを最初に検証しておくバージョンだと、

  1. ローカル開発
  2. VM で必要なスペックを検証
  3. VPS のサービスを選ぶ(さくらとか GMO とか)
  4. VPS のスペック・OS を選ぶ
  5. デプロイ

という手順になります。最初から VM で開発をしてもいいと思いますが、自分はずっと VM 起動してると重くなりそうなので1と2は多分分けます。

感想

初めて VPS にデプロイしてみましたが、GCP を使うのと比べてなんのサービスを使うのかとか、どうすればコストが安くなるとかを難しく考えなくていいので良かったです。初めて Linux も触りましたが、 いろんな分かりやすい記事があったのでそこまで苦労しませんでした。あとは ps top nohup crontab あたりのコマンドを使うくらいなので完全 CLI も意外と大丈夫でした。

多分ちゃんとやればワンチャン GCP の方が安くなるケースもあると思いますが、趣味の個人開発でそこまで頭を使えないので、次何か作るときも VPS がいいなあと思っています。

Discussion

ログインするとコメントできます