macOSにanyenv + pyenv + poetryでPythonの実行環境をつくる

これまでちょっとしたスクリプトが必要なときはRubyで書いていたけれど、なんとなくPythonでも書けるようになってみたいな〜と漠然と思い、本を買って読んでみた。画像を生成したり、OSのGUIもスクリプトで操作できるんだ〜。おもしろそう、ということで、まずはmacOSでPythonを実行する環境を整えてみることにしました。

Rubyはanyenvでrbenvをインストールしたうえでバージョンを管理していたので、Pythonも同様になんちゃらenvがあるのかなあと思って調べたら、venv、virtualenv、pipenv、pyenv… などがあり、ちょっと複雑。venvはPython 3.3から標準で提供されているPythonの仮想環境を作成するためのモジュール、virtualenvはvenvが標準で提供される前から存在していたツールでPython 3.3以前でも利用できる、pipenvは仮想環境管理とパッケージ管理を統合したツールでPipfileやPipfile.lockを用いて依存関係を管理できる、pyenvはPythonのバージョンを管理するためのツール、という理解をしました。

しばらく調べた結果、2023年7月現在はanyenvを使ってpyenvをインストールし、pyenvでインストールしたPythonを使ってvenvで仮想環境を作成するのが良い…?という結論になった。とはいえ、ちょっとPythonで遊んでみたいな、くらいの目的としては複数のバージョンをがっつり切り替える必要もなく、まあ動けばいいかということでvenvは必要になったら導入しようかな。

ここではanyenvでPythonをインストールした手順を残しておきます。なお、手元のmacOSのバージョンは13.4.1で、シェルはZshを使ってます。anyenvのインストールは割愛…

$ anyenv update
$ anyenv install --list
$ exec $SHELL -l
$ anyenv install pyenv
$ pyenv install --list
$ pyenv install 3.11.4 #記事公開時の最新バージョンです
$ pyenv global 3.11.4
$ pyenv versions

これでPythonの3.11.4をインストールできました🎉

Pythonにはpipというパッケージ管理ツールがあるものの、依存関係を管理しやすくなったpoetryというツールがあるようで、あわせてインストールしておきます。RubyでいうところのBundler、JavaScriptでいうところのnpmのようなもので、GemfileやGemfile.lock、package.jsonやpackage-lock.jsonにあたるのがpyproject.tomlとpoetry.lockとのこと(理解が間違っていたらすみません…)。

$ curl -sSL https://install.python-poetry.org | python3 -
# .zshrc に `export PATH=$HOME/.local/bin:$PATH` を追記
$ exec $SHELL -l
$ poetry --version
$ poetry self update
$ poetry config virtualenvs.in-project true 
$ poetry config --list

Pythonを実行するときは、RubyでBundlerを使うときのbundle execよろしく poetry run をつける。

$ poetry init -n
$ poetry run python main.py

そんな感じで〜す!

Ubuntu 22.04のVPSにStrapiをインストールする

Strapiは、オープンソースのヘッドレスCMS。ヘッドレスCMSとは、ヘッド(View)を持たないCMSのこと。CMSとはContent Management Systemの略で、Webサイトのコンテンツを管理できるシステムのこと。Next.jsでWebサイトをつくるにあたって、コンテンツの管理をヘッドレスCMSを使ってやってみたいなというのがモチベーションです。

ヘッドレスCMSにもContentfulとかmicroCMSとかいろいろあって、Strapiにも環境を用意する手間なく使えるStrapi Cloudというクラウド版もあるようだけど、じぶんでホストすれば無料で利用できるとのことなので、Ubuntu 22.04で動かしたConoha VPSにStrapiをインストールしたいと思います。

そもそもVPSをUbuntu 22.04でセットアップしたり、nginxを導入したここまでのあらすじは以下の記事を参照ください。

Node.jsをインストール

StrapiはJavaScriptで開発されているので、なにはともあれVPSにNode.jsをインストール。aptでインストールしてもいいのだけど、Node.jsのバージョンを管理したいので先にnodenvをインストールします。

GitHubのリポジトリからnodenvのソースコードをcloneしたあと、パスを指定してbashを再起動し、nodenvのプラグインのディレクトリへnode-buildのソースコードをcloneする。

$ git clone https://github.com/nodenv/nodenv.git ~/.nodenv
$ echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(nodenv init -)"' >> ~/.bashrc
$ source ~/.bashrc
$ git clone https://github.com/nodenv/node-build.git $(nodenv root)/plugins/node-build

うまくインストールできたかどうかはnodenv-doctorで調べられるらしい。実行したらOKっていわれたのでだいじょうぶそう 🎉

$ curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-doctor | bash

nodenvでインストールできるバージョンを調べつつ、Node.jsのLTSをインストール(記事公開時点の情報です)。

$ nodenv install -l
$ nodenv install 18.16.0
$ nodenv global 18.16.0

$ node -v で 18.16.0 って表示されたらOK 🎉

リバースプロキシを設定する

VPSで起動したStrapiへWebからアクセスできるように、nginxでリバースプロキシを設定します。
まずは、既存のnginxの設定ファイルをコピーして、StrapiへアクセスできるURLの設定ファイルを用意し、リバースプロキシの設定を追記。

$ cd /etc/nginx/sites-available/
$ sudo cp shikakun.dev example.shikakun.dev
$ sudo vim example.shikakun.dev
upstream strapi {
  server 127.0.0.1:1337;
}

server {
  listen 80;
  listen [::]:80;

  server_name example.shikakun.dev;

  location / {
    return 301 https://$server_name$request_uri;
  }
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  server_name example.shikakun.dev;

  ssl_certificate /etc/letsencrypt/live/shikakun.dev/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/shikakun.dev/privkey.pem;
  ssl_session_tickets on;
  ssl_protocols TLSv1.2;
  ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers on;

  location / {
    proxy_pass http://strapi;
    proxy_http_version 1.1;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_pass_request_headers on;
  }
}

作成した設定ファイルへsites-enabledからシンボリックリンクを貼り、nginxを再起動。

$ sudo ln -s /etc/nginx/sites-available/example.shikakun.dev /etc/nginx/sites-enabled/example.shikakun.dev
$ sudo systemctl restart nginx

Strapiをインストール

Strapiをインストールするフォルダを用意。

$ cd /var/www/
$ sudo mkdir example.shikakun.dev
$ sudo chown -R $USER:$USER /var/www/example.shikakun.dev

create-strapi-appを実行して、対話型インターフェースに回答しながら好みの仕様でStrapiをインストールします。データベースはとりあえずsqliteにしておきました。

$ npx create-strapi-app@latest example.shikakun.dev
? Choose your installation type Custom (manual settings)
? Choose your preferred language JavaScript
? Choose your default database client sqlite
? Filename: .tmp/data.db

Strapiのソースコードをビルドして、起動!

$ cd example.shikakun.dev
$ NODE_ENV=production npm run build
$ node /var/www/example.shikakun.dev/node_modules/.bin/strapi start

う、動いた〜! https://example.shikakun.dev/admin でアクセスできるようになりました 🎉

使うたびにStrapiを起動するのは大変なので、 pm2 を利用してデーモン化します。ということで、インストールしてシェルを再起動。

$ cd ~
$ npm install pm2@latest -g
$ source ~/.bashrc

設定ファイルを用意します。sqliteではなくデータベースのサーバーを用意する場合はenvのところに設定を指定するみたい。

$ vim ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'strapi',
      cwd: '/var/www/example.shikakun.dev',
      script: 'npm',
      args: 'start',
      env: {
        NODE_ENV: 'production',
      },
    },
  ],
};

保存したらpm2を実行。

$ pm2 start ecosystem.config.js
$ pm2 startup

[PM2] To setup the Startup Script, copy/paste the following command: のあとに、実行してねってコマンドが出力されるので、それをコピーペーストして実行!
$ pm2 list で登録されたかどうか確認できる。無事にデーモン化できました 🎉

Apacheをやめてnginxにする

Conoha VPSにUbuntu 22.04をインストールして、ApacheにPassengerというモジュールを組み込んでSinatraで書いたアプリケーションを動かしてました。PHPみたいにサーバーへファイルを置くだけで動くのは手軽で便利だったのだけど、勉強としていろいろ試してみようということでnginx + Pumaの構成へ変更してみます。

Apacheをアンインストール

$ sudo systemctl stop apache2
$ sudo apt-get remove apache2

$ apache2ctl -v で、コマンドがありませんと返されたらOK 🎉

設定ファイルはあとで見たくなるかも… と思って、ひとまずそのままにしておきます。
消したいときは $ sudo apt-get purge apache2 で消せるとのこと。

いったんポートも閉じておきます。

$ sudo ufw delete allow "Apache"
$ sudo ufw delete allow "Apache Secure"

$ sudo ufw status でルールが消えていることも確認。

nginxをインストール

$ sudo apt-get update
$ sudo apt install nginx

$ nginx -v で、バージョン情報が返されたらOK 🎉
OSを起動したらnginxも起動するように設定しておく。

$ sudo systemctl enable nginx

ポートを開きます。

$ sudo ufw allow "Nginx Full"

$ sudo ufw status でルールが増えていることを確認。

ここでブラウザで http://{サーバーのIPアドレス} へアクセスしてみると、nginxのデフォルトのHTMLファイルが表示されました 🎉

nginxの設定ファイルを作成

サーバーのIPアドレスではなくドメインで表示できるようにしたいし、バーチャルホストを設定したい。というわけで、nginxの設定ファイルを作成します。

設定ファイルには /etc/nginx/sites-available/etc/nginx/sites-enabled ってディレクトリがあって、 sites-available に書いた設定ファイルのうち有効にしたいやつを sites-enabled からシンボリックリンクを貼る… という運用だと理解しました(合ってますか?)。まずは sites-available にファイルを作成します。

$ sudo vim /etc/nginx/sites-available/shikakun.dev
server {
  listen 80;
  listen [::]:80;

  root /var/www/shikakun.dev;
  index index.html index.htm index.nginx-debian.html;

  server_name shikakun.dev;

  location / {
    try_files $uri $uri/ =404;
  }
}

作成したファイルへ sites-enabled から sites-available へシンボリックリンクを貼る。

$ sudo ln -s /etc/nginx/sites-available/shikakun.dev /etc/nginx/sites-enabled/shikakun.dev

サーバー名を追加することで「ハッシュバケットメモリの問題」なるものが発生する可能性があるとのことで、よく理解していないのですが /etc/nginx/nginx.conf に書かれている server_names_hash_bucket_size の設定をコメントアウトを外して保存。
参考: Ubuntu 20.04にNginxをインストールする方法 | DigitalOcean

$ sudo vim /etc/nginx/nginx.conf
- #server_names_hash_bucket_size 64;
+ server_names_hash_bucket_size 64;

ここまででnginxの設定は完了。念のため $ sudo nginx -t を実行すると、設定ファイルのシンタックスに問題がないか確認できて便利。
nginxを再起動!

$ sudo systemctl restart nginx

/var/www/shikakun.dev/index.html に適当な内容のHTMLファイルを置いておくと、ブラウザから http://shikakun.dev/ でアクセスできるようになりました 🎉
ちなみにChromeは .dev のトップレベルドメインのサイトは自動でhttpsプロトコルでアクセスする仕様になっているそうで、Firefoxで確認しました。

常時SSL化

Let's Encryptで取得したワイルドカードのSSL証明書をnginxで使います。SSL証明書を取得した手順は以下の記事に書いたので省略します。

さきほど作成したnginxの設定ファイルを開いて、ポート番号の80へアクセスしたら443でアクセスするようにリダイレクトを設定し、443でアクセスされたらSSL証明書を読み込むように設定。

$ sudo vim /etc/nginx/sites-available/shikakun.dev
server {
  listen 80;
  listen [::]:80;

  server_name shikakun.dev;

  location / {
    return 301 https://$server_name$request_uri;
  }
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  server_name shikakun.dev;

  root /var/www/shikakun.dev;
  index index.html index.htm index.nginx-debian.html;

  ssl_certificate /etc/letsencrypt/live/shikakun.dev/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/shikakun.dev/privkey.pem;
  ssl_session_tickets on;
  ssl_protocols TLSv1.2;
  ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers on;

  location / {
    try_files $uri $uri/ =404;
  }
}

$ sudo systemctl restart nginx でnginxを再起動すると、ブラウザから https://shikakun.dev/ でアクセスできるようになりました 🎉

SinatraアプリをPumaで動かす

Pumaで起動できるようにSinatraアプリのコードに手を加えます。
GemfileにPumaを追記。

gem "sinatra"
gem "puma"

app.rb にpumaで起動するように設定。

set :server, :puma

Sinatraアプリのルートディレクトリに、以下のような内容で puma.rb ってファイルを追加。
なにもわからないので Gistで @ctalkington さんが共有されていたコード をそのまま使用させていただきました。ありがとうございます。自分のアプリや環境に応じてチューニングしていきたいです。

root = "#{Dir.getwd}"

bind "unix://#{root}/tmp/puma/socket"
pidfile "#{root}/tmp/puma/pid"
state_path "#{root}/tmp/puma/state"
rackup "#{root}/config.ru"

threads 4, 8

activate_control_app

手を加えたSinatraアプリのコードをVPSへアップロードしたあと、アプリのルートディレクトリへ移動してGemをインストール。

$ cd /var/www/example-sinatra-app.shikakun.dev
$ bundle config set path "vendor/bundle"
$ bundle install

pumaを実行した際に作成されるファイルを置くディレクトリを用意しておく。

$ mkdir -p tmp/puma

ここからnginxの設定をします!
まずは、ここまでに作成した設定ファイルをコピーして、Sinatraアプリの設定ファイルを用意。

$ cd /etc/nginx/sites-available
$ sudo cp shikakun.dev example-sinatra-app.shikakun.dev
$ sudo vim example-sinatra-app.shikakun.dev
upstream app {
  server unix:///var/www/example-sinatra-app.shikakun.dev/tmp/puma/socket;
}

server {
  listen 80;
  listen [::]:80;

  server_name example-sinatra-app.shikakun.dev;

  location / {
    return 301 https://$server_name$request_uri;
  }
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  server_name example-sinatra-app.shikakun.dev;

  root /var/www/example-sinatra-app.shikakun.dev/public;

  access_log /var/www/example-sinatra-app.shikakun.dev/log/nginx.access.log;
  error_log /var/www/example-sinatra-app.shikakun.dev/log/nginx.error.log info;

  ssl_certificate /etc/letsencrypt/live/shikakun.dev/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/shikakun.dev/privkey.pem;
  ssl_session_tickets on;
  ssl_protocols TLSv1.2;
  ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers on;

  location / {
    try_files $uri @puma;
  }

  location @puma {
    include proxy_params;

    proxy_pass http://app;
  }
}

sites-enabledへ設定ファイルへのシンボリックリンクを貼って、nginxを再起動。

$ sudo ln -s /etc/nginx/sites-available/example-sinatra-app.shikakun.dev /etc/nginx/sites-enabled/example-sinatra-app.shikakun.dev
$ sudo systemctl restart nginx

Sinatraアプリのルートディレクトリへ移動してpumaを起動。

$ cd /var/www/example-sinatra-app.shikakun.dev
$ bundle exec puma --config puma.rb

https://example-sinatra-app.shikakun.dev でSinatraアプリが動きました 🎉

Pumaの起動をデーモン化

わ〜い!と無事に動いて喜んだものの、このままだとSSH接続を切るとSinatraアプリが終了してしまうので、デーモン化しておきます。以前は puma.rbdaemonize って書けばデーモン化できたようなのだけど、最近のバージョンではデーモン化はOSに任せるべきという方針から廃止されたらしい。なるほど。ということで、systemdでデーモン化することに。

PumaのGitHubのリポジトリにある設定例 を参考に、以下のように書いてみました。

$ sudo vim /etc/systemd/system/example-sinatra-app.shikakun.dev.service
[Unit]
Description=Puma HTTP Server for example-sinatra-app.shikakun.dev
After=network.target

[Service]
Type=simple
WorkingDirectory=/var/www/example-sinatra-app.shikakun.dev
ExecStart=/home/shikakun/.rbenv/shims/bundle exec puma --config /var/www/example-sinatra-app.shikakun.dev/puma.rb
Restart=always

[Install]
WantedBy=multi-user.target

保存したら、以下のコマンドを実行して有効化。

sudo systemctl daemon-reload
sudo systemctl enable example-sinatra-app.shikakun.dev.service
sudo systemctl start example-sinatra-app.shikakun.dev.service
sudo systemctl status example-sinatra-app.shikakun.dev.service

SSH接続を切っても、サーバーを再起動しても、 https://example-sinatra-app.shikakun.dev でSinatraアプリが動きつづけるようになりました 🎉