🍏

[第一部]これならわかる!Flask+Nginx+uWSGIをAWSに丁寧にデプロイ

2021/04/05に公開1

はじめに

タイトル通り、Flaskで作った簡単なWebアプリをAmazon Linux2に上げてNginx+uWSGIを使ってとりあえず外部から見れるようにします。

AWSに関してはVPCの作成から載せています。(補足でCloudWatchを設定して請求金額のアラート通知も出来るようにするのでお金の面も安心!)

かなり丁寧に解説を挟みますので、下に挙げる前提知識を持っている方であれば問題なく理解し実践出来る内容になっています。

また、今後「第二部」「第三部」では「DNSの設定」や「DB(RDS)やストレージ(S3)との連携」、「HTTPS化」等についても記事にまとめていくので一覧のシリーズを読んでいけばとりあえず個人開発でFlaskアプリを作ってデプロイ出来る知識を素早く身に付けられるように作っています。

最低限必要な前提

  • AWSは登録済みかつIAM作業用ユーザー作成済み(もしくは独力で登録出来る)
  • Pythonの基礎文法は把握している且つpyenvくらいなら調べながら使える
  • Flaskを軽くは知っている(Webフレームワークであることは理解しているレベルでおk)
  • Webサーバー、アプリケーションサーバーの違いはわかる
  • Linuxの基本コマンドやパーミッション等についての基礎知識はある

[AWS]ネットワーク構築

さっそく作成済みのIAM作業用ユーザーでマネジメントコンソール(以後、管理画面と呼称)にログインしてネットワークを構成していきましょう。

[AWS]VPC設定

VPC」とは仮想ネットワークのこと。ここを「サブネット」という小さなネットワークで区切っていきます。

管理画面上部の検索バーから「VPC」検索します。

アクセス後、ダッシュボード(左側のバーのこと)からVPCを選択。

選択後、VPCを作成をクリック。

VPCの名前は好きなものを設定してください。IPアドレスはプライベートipの範囲で好きなものを選択してください。(よくわからないという方は今回は私と同じ「10.0.0.0/16」と設定してください。その他はデフォルトのままで大丈夫です。)
「VPCを作成」ボタンをクリックしましょう。


ちゃんと新しいVPCが出来ています。
ちなみにVPCのNameは後から編集することも出来ます。(私もこちらのクラスメソッド さんの記事を見て名称を見直してみました💦)

[AWS] パブリックサブネット作成

続いて左側のダッシュボードから「サブネット」を選択します。
今回作るサブネットはインターネットに接続できるようにしてWebサーバーを置く予定なので「パブリックサブネット」にします。
※DB用途などには「プライベートサブネット」を選択します。

※最初から4つあるのは、デフォルトでAWSが用意してくれているサブネットです。無視して進めましょう。

先ほど作成したVPCを選択。

設定では(1)サブネットの名前(2)アベイラビリティゾーン(3)CIDRブロックを設定します。

(2)に関してはどこでも問題ありません。(今回私はリージョンを安い米国西部にしているのでオレゴンのアベイラビリティゾーンが選択出来ます。)

(3)に関してはVPCの中に設定するものなので先ほどVPCに設定したCIDRより大きい値を設定しましょう。

無事、設定出来ました。この画面で間違いがないか一応確認してみてみましょう。

※「利用可能なIPv4が254じゃないのなぜなの〜」という方はこちらのドキュメントを読めば251になっている理由がわかります。

[AWS] ルーティング設定 その1 IGWのアタッチ

インターネットゲートウェイ(IGW)」をVPCにアタッチしていきます。

※インターネットゲートウェイとは雑に言えばデフォルトルートが登録されたデフォルトゲートウェイ(ブロードバンドルーター)のようなもの。より簡単に言えばインターネットとVPCを繋ぐ入り口です。

まず先ほどのVPCのページのダッシュボードから「インターネットゲートウェイ」を選択しインターネットゲートウェイの作成に移ります。

作成したインターネットゲートウェイを確認すると「Detached」になっている。これをアクションから自分のVPCに「アタッチ」するように設定していく。

[AWS] ルーティング設定 その2 ルートテーブルの作成

またダッシュボードから「ルートテーブル」を選択する。

無事作成。次はこのルートテーブルをVPCへの紐付きからパブリックサブネットへの紐付きに変更します。


サブネットを選択し、保存。

最後にルーティングテーブルで「0.0.0.0/0」のipアドレスが先ほど設定したインターネットゲートウェイに振り分けされるように設定。

※0.0.0.0/0とは要は全てのIPアドレスのこと。振り分けを設定した他のIP(10.0.0.0/16)以外は全てインターネットゲートウェイに向かいます。こうすることでインターネットに繋がります。

[AWS] EC2の設定

[AWS] EC2インスタンス作成

EC2」というAWS上の仮想サーバーを利用します。また管理画面の検索バーからEC2のページにアクセスしましょう。

EC2設定前にさくっと用語を整理しておくと、「インスタンス」 というのはEC2から建てられたサーバーのこと。「AMI」はOSのテンプレートイメージ、「インスタンスタイプ」はサーバースペック、「ストレージ」はそのまま文字通りデータの保存場所(EBSを利用することが多いとのこと)。インスタンスを立ち上げるさいにAMI、インスタンスタイプ、ストレージを指定して設定します。

また後ほど、ここで作ったインスタンスにPythonやFlask、Nginxといったソフトウェア、ミドルウェアをインストールすることでWebサーバーとしての役割を果たせるようにしていきます。

まずダッシュボードで「インスタンス」を選択。


右上の「インスタンスを起動」を選択。

AMI」に関しては今回は「Amazon Linux 2(x86)」を選択します。

インスタンスタイプ」は「t2.micro」を選択します。
t2.microはAWSのアカウント開設から12ヶ月間は無料で使用できるインスタンスです。

※参考:改めてAWSの「無料利用枠」を知ろう / クラスメソッド

インスタンスの詳細設定では、

(1)「ネットワーク」、「サブネット」を先ほど自分で作ったものに設定
(2)「自動割り当てパブリックIP」は有効に(インターネットに繋ぐ時に必要なグローバルIPをAWSが自動で割り当ててくれる。)
(3)「キャパシティーの予約」は今回勉強用なのでなしに。

※参考:AWS EC2 オンデマンドキャパシティー予約を詳しく知る / Serverworks

今回、ストレージの割り当てはデフォルトの8GiBで問題ありません。


タグはNameでインスタンスの名前を付けてやります。

セキュリティグループの名称を付けてやります。

後ほどセキュリティの設定を行ってインターネットからの接続を許可しますが一旦、インスタンスを起動させます。

インスタンスの起動前にこのインスタンスに接続するためのkeypairを作成してダウンロードしておく。なくさないように保存しておきましょう。

※注意:このkeyは絶対にオンラインに公開などしないように!!!

最後にセキュリティウォールの設定をしていきます。

「EC2」のダッシュボードから「セキュリティグループ」を選択します。

先ほど作成したセキュリティグループにチェックボックス を入れて下の「インバウンドルール」で「インバウンドルールを編集」をクリックします。

編集画面でHTTPを上記と同じように追加しましょう。

これでHTTP通信でインターネットのどこのIPからでもこのインスタンスにアクセスが出来るようになりました。

[AWS] MacからAWSインスタンスにSSH接続する

ターミナル を起動して下記のようにコマンドを打っていきます。

まず、keypairのパーミッションを600番に変更します。

$ chmod 600 ~/キーペア保存までのパス/作成したkeypair

次にAWSの管理画面のEC2のページから接続したいインスタンスにチェックボックスを入れて、下の方にある詳細画面から「パブリックIPv4アドレス」をコピーしてきます。(インスタンスを再起動させたりすると変わってしまうので注意!)

下記のコマンドを打って、インスタンスに接続しよう。なお初めてインスタンスに接続する場合は「fingerprint」がないが大丈夫か聞いてくるので「yes」と打ってEnterしてやりましょう。

$ ssh -i ~/キーペア保存までのパス/作成したkeypair ec2-user@パブリックIP

[AWS] 補足:請求アラートの設定

右上のIAMユーザー名のドロップダウンリストから「マイ請求ダッシュボード」を選択。左のダッシュボードの「設定」から「Billingの設定」をクリックします。

無料利用枠(アカウント解説から12ヶ月無料の分)を過ぎた際に指定したメールにアラートを流すように設定します。また一定以上の金額が発生した場合にもアラートが出るように請求アラートにチェックをいれます。

続いてCloudWatchを設定してアラートの詳細設定を行います。
サービス画面から検索してCloudWatchを選択して、

「アラームの作成」を選択します。

※バージニアリージョンでしかCloudWatchを使えないという情報をWeb上でよく見かけましたが、私のアカウントではオレゴン(やおそらく東京リージョン)で問題なくアラームを設定できました。

今回は10ドルを閾値として設定します。

上記画面で連絡先のメールアドレスや任意のアラート名を設定して、最後に「アラームの作成」を選択します。

下記のようなメールが設定したメールアドレス宛に届いているはずなので、「Confirm subscription」をクリックして登録を承認します。

※ちなみに追加で別のメールアドレスにも届くように設定するにはAWSの「SNS」というサービスを使用すれば良いのですが今回は説明を省略します。

Pythonセットアップ+各種ソフトインストール

下準備とpyenvのインストール

まずyumのアップデート及び必要なソフトウェアのインストールを行います。

yumでのインストール
$ sudo yum -y update
$ sudo yum -y install \
  bzip2 \
  bzip2-devel \
  gcc \
  git \
  libffi-devel \
  make \
  openssl \
  openssl-devel \
  readline \
  readline-devel \
  sqlite \
  sqlite-devel \
  zlib-devel \
  tree

次にgithubからpyenvというPythonのバージョン管理ツールをダウンロードして設定(パス通し)を行ってやります。

$ git clone git://github.com/yyuu/pyenv.git ~/.pyenv
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

ちなみに下記のようなシェルスクリプトを追加することで.bash_profileがちゃんと読み込まれた時にメッセージを出してくれるので便利です。

$ echo 'echo "ec2-user bsah_profile stands up"' >> ~/.bash_profile

準備が出来たら、.bash_profileを読み込んでやります。

-- 下記どちらでも良い
$ . ~/.bash_profile
$ source ~/.bash_profile

Python、Flask、uWSGIの取得

pyenvで現在取得出来るPythonのバージョンを確認出来ます。

$ pyenv install --list
-- 上だと長いので下記のようにすると3.9と前に着いているバージョンのみ確認出来る
$ pyenv install --list | grep "3\.9\.*" | grep -v "[A-Za-z]"

pyenvを用いて指定のバージョンのPythonをインストールします。(結構インストールには時間がかかります...)

$ pyenv install 3.9.2
-- 現在使用できるバージョンの確認(systemはこのインスタンスに最初から入っていたもの)
$ pyenv versions
* system (set by /home/ec2-user/.pyenv/version)
  3.9.2
-- バージョン切り替え
$ pyenv global 3.9.2

後々使用するamazon-linux-extrasコマンドのため下記のようなシンボリックリンクを貼ります。

$ ln -s /lib/python2.7/site-packages/amazon_linux_extras ~/.pyenv/versions/3.9.2/lib/python3.9/site-packages/

pipでFlaskとuWSGIをインストールします。

$ /home/ec2-user/.pyenv/versions/3.9.2/bin/python3.9 -m pip install --upgrade pip
$ pip install flask
$ pip install uwsgi

amazon-linux-extrasコマンドでNginxをインストールします。

-- nginxがインストール出来ることを確認
$ amazon-linux-extras list | grep nginx
38  nginx1                   available    [ =stable ]

-- インストール(インストールするか聞かれたらyと入力)
$ sudo amazon-linux-extras install nginx1

以上でPythonのセットアップ及び各種ソフトウェアのインストールは終了です。

次のセクションではいよいよFlaskのアプリケーションを作ってFlask付属の簡易サーバーで動かしていきます。

Flaskアプリケーション作成

本来は、

(1)ローカルのPCでFlaskアプリを作成
(2)それをGitHubのようなgitのホスティングサービスに(git pushで)アップロード
(3)gitホスティングサービスからEC2インスタンス上で(git cloneで)取得してソースコードやコミットログを取得

としていくのがスタンダードなやり方です。
ただ今回は、(gitの解説を省略するために)インスタンス上で直接Flaskアプリケーションを作成していきます。

ディレクトリ構成は最終的に下記のようになります。

最終的なディレクトリ構成
$ cd /var/www
$ tree myapp
myapp/
├── __pycache__
│   ├── myproject.cpython-39.pyc
│   └── run.cpython-39.pyc
├── myproject.ini
├── myproject.py
├── new_comer.trigger
├── run
│   └── mywsgi.sock
├── run.py
├── static
│   └── logo_uwsgi.png
└── templates
    ├── advance.html
    └── index.html

ディレクトリと静的ファイルの下準備

まずFlaskアプリケーションを/var/www/myappディレクトリに作成していきます。

ちなみに「なぜNginxのデフォルトのドキュメントルートの/usr/share/nginx/htmlではなく/var/wwwに作るのか」というというと、Nginxの公式ドキュメントにもある通り、

Nginx公式英文ドキュメント引用

You should not use the default document root for any site-critical files. There is no expectation that the default document root will be left untouched by the system and there is an extremely high possibility that your site-critical data may be lost upon updates and upgrades to the NGINX packages for your operating system.

「デフォルトのドキュメントルートのディレクトリはNginxのパッケージアップデートの際にも使用されるものであり、このアップデートの際に重要なデータが消える可能性があるので避けた方が良いよ」、と言うことのようです。

※参考:Not Using Standard Document Root Locations Nginx公式ドキュメント

さっそく作っていきます。

-- ec2インスタンス上
$ sudo mkdir -p /var/www/myapp
$ cd /var/www/myapp

さて、Flaskのルールとして下記のようなファイルの置き場所に関わるルールがあります。

  • templates」ディレクトリ
    HTMLファイルの置き場所でここに置くことでPython用のテンプレートエンジンJinja2が適用される。
  • static」ディレクトリ
    「(HTMLファイル以外の、)CSSファイル・JSファイル・画像ファイル」などの静的ファイルの置き場所です。今回は下記の画像を保存しておきます。

※ちなみに最初、私はtemplatestemplateとしていたので延々と「jinja2テンプレートが見つかりません」jinja2.exceptions.TemplateNotFoundとエラーが出て憂鬱な気持ちになっていました。

ではそれぞれのディレクトリを作っていきます。

-- ec2インスタンス上
$ sudo mkdir templates static run

runディレクトリに関しては後ほどuWSGIサーバーの関連ファイルを保存するために使うので先に作っておいてください。

次にstaticディレクトリに入れる画像をローカルのPCに入れてそれをsshでサーバーに送りましょう。scpコマンドを使用すれば出来ます。

-- ローカル上
$ scp -i ~/keypairまでのパス/作成したkeypair ~/送りたいファイルまでのパス/送りたいファイル ec2-u
ser@インスタンスのパブリックIP:~/送り先のディレクトリのパス

送り先のディレクトリのパスはec2-userのホームディレクトリ「~/」を指定するのがオススメです。

※デフォルトの状態でいきなりルート直下の/var/www/myapp/staticなどを指定するとPermission deniedとなってしまいます。

今回のようにファイルを送る場合はscpコマンドにはオプションは入りませんがディレクトリを送る場合は-rとつけましょう。

ちなみに今回は下記の画像を使用します。

今度はssh先で画像を/var/www/update/staticに移しましょう。

-- ec2インスタンス上
$ sudo mv ~/logo_uwsgi.png /var/www/myapp/static/

次にtemplatesディレクトリに下記の二つのHTMLファイルを入れます。

※普通のHTMLファイルと違う書き方ですがこれはtemplatesディレクトリに置いた.htmlファイルはFlaskのテンプレートエンジンによって解釈されるためです。

index.html
<!DOCTYPE HTML>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ name }}さんのページだぞ</title>
</head>
<body>
<h1>きみは{{ name }}さんだね!</h1>
<img src="/static/logo_uwsgi.png" style="width:100%">
<p>これはindex.htmlのページです</p>
<form action="/" method="GET">
    <p><label>名前を変更してみよう: <input name="name" type="text" placeholder="なまえ入力してね"></label>
    <p><input type="submit" value="名前変更確定!"></p>
</form>
</body>
</html>
advance.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ name }}</title>
</head>
<body>
{% if name == "Kumamoto" %}
    <h1>きみはオーナーだね</h1>
{% elif name %}
    <h1>きみはゲストの{{ name }}さんだね</h1>
{% else %}
    <h1>ななしさんだね</h1>
{% endif %}
<form action="/adv" method="POST">
    <p><label>名前を更新する:<input name="name" type="text"></label></p>
    <p><input type="submit" value="更新!">
</form>
<p>advance.htmlのページです</p>
</body>
</html>

続いて本家本元のMVTのView部分(MVCで言うコントローラー部分)を作成していきましょう。

myproject.py
from flask import Flask, render_template, request
application = Flask(__name__)

@application.route("/")
@application.route("/index")
def index():
    # Note:str()を使う事でNoneの時でも、TypeErrorを起こさず"様"をつけることが出来る
    name  = str(request.args.get("name")) + "様"
    return render_template("index.html", name=name)

@application.route("/adv", methods=["GET", "POST"])
def advance():
    # Note:dictのgetメソッドを使うことでNoneでもエラーにならない!
    name  = request.form.get("name")
    # Note:./を書いても書かなくてもいいんだなぁ。
    return render_template("./advance.html", name=name)


if __name__ == "__main__":
    application.run()

さらにこれを動かすpythonスクリプトを作りましょう。

-- run.py
from myproject import application

if __name__ == "__main__":
    application.run(host="0.0.0.0", debug=True)

さて、run.pyのほうでflaskインスタンスのrun関数のキーワード引数hostを"0.0.0.0"と指定しています。こうすることでどこのipアドレスからのアクセスも受け付けるようになっています。

※キーワード引数hostを指定しないデフォルトでは自分自身を表すプライベートIP127.0.0.1以外からは受け付けないです。

ただし!!今現在はインスタンスのセキュリティグループで「HTTPの80番ポート」からの接続はどこのIPアドレスからでも受け付けますが、80番以外のポート番号からのHTTP接続はどこのIPアドレスからであっても許可されていません。

実際にアクセス出来ないことをまずは実験してみましょう。

インスタンス上でFlaskアプリを立ち上げる
$ python /var/www/myapp/run.py
* Serving Flask app "myproject" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 301-577-841

flaskに付属する簡易サーバーが立ち上がります。
※warningにもある通りこの簡易サーバーを本番サーバーとして運用することは辞めましょう。

ログに「HTTP」の「どのIPアドレスからでも」「5000番ポートを使って」アクセス出来るとあります。

それではブラウザを立ち上げAWSで作成したEC2インスタンスに割り当てされているパブリックIPを打ち込んで、アクセスを試みて見ましょう。

アクセス出来ません(あたりまえ)

それではAWSの「EC2」サービスページのダッシュボード「セキュリティグループ」から自分の作ったセキュリティグループのチェックボックスにチェックを入れ、下の画面の「インバウンドルールを編集」からルールを編集しましょう。

下記のように「ルールの追加」で「タイプ:カスタムTCP」、「ポート:5000番」、「ソース:マイIP」を選択します。完了したら「ルールの保存」を選択しましょう。

※マイIPというのは自分に割り当てられているグローバルIPのことです。

再度、http://インスタンスのパブリックIP:5000にアクセスしてみましょう。
※flaskの簡易サーバーを止めた場合は再度、python run.pyすることを忘れずに!

無事アクセス出来ました!入力フォームに文字を入れてみたり、http://インスタンスのパブリックIP:5000/advにアクセスしたりわざとPythonのスクリプトにバグを起こして、debug画面を覗いたりしてみましょう。

uWSGIセットアップ

さらっと用語を学ぶ

uWSGIに関わる用語を整理します。

WSGI uWSGI uwsgi
アプリケーションとAP/Webサーバー間の標準的なインターフェイス APサーバー uWSGIサーバーのバイナリプロトコル

※参考:How To Set Up uWSGI and Nginx to Serve Python Apps on Ubuntu 14.04 / Digital Ocean

まずはコマンドで立ち上げ

uwsgiコマンドを用いて先ほどのFlaskアプリケーションをデプロイしましょう。

$ cd /var/www/myapp
$ uwsgi --http=0.0.0.0:5000 --wsgi-file=run.py --callable=application

コマンドオプションの意味を補足します。

httpがサーバーのipアドレス、ポート番号を指定します。既に説明しましたが、「0.0.0.0」はIPv4アドレス空間内の全てのアドレスと一致します。またポート番号を5000番としたのは、先ほどEC2のセキュリティグループで5000番ポートからのアクセスを許可するように設定したからです。(80番ポートを指定してもアクセスは出来ます。)

wsgi-fileでは先ほど作ったflaskのアプリケーションファイル(つまり今回だとrun.py)を指定します。

callableではFlask(__name__)のインスタンス名を指定します。(今回はapplication)
またhttp://インスタンスのパブリックIP:5000にアクセスしてサイトが映るか確かめてみましょう。

uWSGIのための設定ファイルを書こう

毎回コマンドのオプションを指定するのは面倒なのでドットiniファイルにuWSGIの設定を書いていきます。

$ cd /var/www/myapp
$ sudo vim myproject.ini

中身は下記。

.iniファイル
[uwsgi]
# Nginxを使わずにアクセス出来るように一時的にhttpプロトコルを設定
http=0.0.0.0:5000
module=run
callable=application
master=true
processes=5

base_dir=/var/www
pj_name=myapp

# uwsgi-socketはsocketと指定してもよい
uwsgi-socket=%(base_dir)/%(pj_name)/run/mywsgi.sock
logto=/var/log/uwsgi/uwsgi.log
# 後々Nginxがアクセス出来るように666にしている
chmod-socket=666
vacuum=true
die-on-term=true
# wsgi-file=/var/www/myapp/run.py
wsgi-file=%(base_dir)/%(pj_name)/run.py
touch-reload=%(base_dir)/%(pj_name)/new_comer.trigger

moduleはflaskを動かすファイル(run.py)、callableFlask(__name__)のインスタンスです。

master=true公式のGlossary(用語集)にもある通り推奨されている設定です。

processesは実行するプログラム数のことです。

vacuumオプションをtrueにすることで、プロセスの停止時にソケットをクリーンアップしてくれます。

die-on-termオプションの設定です。これは、initシステムとuWSGIが、それぞれのプロセス信号が何を意味するかについて、同じ仮定を持っていることを保証することができます。これを設定することで、2つのシステムコンポーネントが整合し、期待される動作が実装されます。

またtouch-reloadについては後ほど説明します。

続いて、logtoで指定したlogファイル用のディレクトリを用意してやります。

$ sudo mkdir -p /var/log/uwsgi

さて、これでようやっと立ち上げ、、、

.iniファイルによるuWSGIの立ち上げ
$ cd /var/www/myapp
$ uwsgi myproject.ini
*** Starting uWSGI 2.0.19.1 (64bit) on [Fri Apr  2 04:56:20 2021] ***
compiled with version: 7.3.1 20180712 (Red Hat 7.3.1-12) on 29 March 2021 17:49:25
os: Linux-4.14.225-168.357.amzn2.x86_64 #1 SMP Mon Mar 15 18:00:02 UTC 2021
nodename: ip-10-0-0-59.us-west-2.compute.internal
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 1
current working directory: /var/www/myapp
detected binary path: /home/ec2-user/.pyenv/versions/3.9.2/bin/uwsgi
your memory page size is 4096 bytes
detected max file descriptor number: 65535
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
bind(): Permission denied [core/socket.c line 230]

出来ませんでした。 エラーログを見る限りPermission関係で弾かれたと推測できます。

次の項からはuWSGIがまともに動けるように(1)「ec2-user」をサーバー管理者に設定する(2)ディレクトリの所有者をサーバー管理者に変更する、ということをやっていきましょう。

サーバー管理者となるユーザー の設定変更

今回は「ec2-user」をサーバーの管理者用のユーザーとしたいと思います。サブグループとして既に「admin」に所属となっていますが「nginx」グループにも所属させてやります。

念のため/etc/groupファイルでgroup一覧を確認してやりましょう。

$ view /etc/group
-- 省略して表示
adm:x:4:ec2-user
nginx:x:993:
ec2-user:x:1000:

※ファイルを確認する時はviewコマンドの他lessmorecatコマンドなどでもおkです。

groupをみてやると「nginx」グループにはサブグループとして所属しているユーザーはいないことがわかります。

※ちなみにUbuntuなどのDebian系だと「nginx」などのサーバー名の付いたグループではなく、「www-data」グループを使用するのが一般的なようです。ディストリビューションによってもポピュラーなやり方は異なると思うので適宜調べてみてください。

つづいてまた念のため「/etc/passwd」でユーザー一覧を確認してみましょう。

$ view /etc/pwasswd
ec2-user:x:1000:1000:EC2 Default User:/home/ec2-user:/bin/bash

以上から現状「ec2-user」グループはプライマリーグループとして「ec2-user」、サブグループとして「admin」に所属していることがわかります。

今回は学びのために敢えて回りくどいやり方をしましたがユーザーに関してはidコマンドを、グループに関してはgetentコマンドを使えばさらに簡単に確認が出来ます。(これ以降はこれらのコマンドを使っていきます)

-- ec2-userのユーザーidや所属するグループ(プライマリーグループもサブグループも)を確認出来る
$ id ec2-user
uid=1000(ec2-user) gid=1000(ec2-user) groups=1000(ec2-user),4(adm),10(wheel),190(systemd-journal)
-- 今、nginxグループをサブグループとしているユーザーはいない
$ getent group nginx
nginx:x:993:

では現状確認が終わったので「ec2-user」のサブグループに「nginx」を追加します。

$ sudo usermod -aG nginx ec2-user

先ほどと同様にidコマンドとgetentコマンドを使って上手くec2-usernginxに所属してくれたか確認しましょう。

$ id ec2-user
uid=1000(ec2-user) gid=1000(ec2-user) 
groups=1000(ec2-user),4(adm),10(wheel),190(systemd-journal),993(nginx)

$ getent group nginx
nginx:x:993:ec2-user

問題なさそうです。

では続いては各種ディレクトリの所有者を変更していきましょう。

各種ディレクトリの所有者変更

まず本家本元の/var/www以下の所有者を変更していきましょう。

-- ec2-userになっていない場合「sudo su - ec2-user」でユーザーを切り替えてください
$ echo $USER
ec2-user

-- /var/www以下の全ての所有者を変更
$ sudo chown $USER:nginx -R /var/www

上手くいったか確認してみましょう。

$ ls -ld /var/www
drwxr-xr-x 3 ec2-user nginx 33  4月  1 09:06 /var/www
$ ls -l /var/www
drwxr-xr-x 4 ec2-user nginx  92  4月  2 04:46 myapp

ではいったん、uwsgiの設定ファイルmyproject.iniのうちlogファイルの指定部分をコメントアウトしてやって、、、

[uwsgi]
#logto=/var/log/uwsgi/uwsgi.log

サーバーの接続を再度試みてみましょう。

uwsgiコマンドによるサーバー立ち上げ
$ cd /var/www/myapp
$ uwsgi --ini myproject.ini
*** Starting uWSGI 2.0.19.1 (64bit) on [Fri Apr  2 06:17:56 2021] ***
compiled with version: 7.3.1 20180712 (Red Hat 7.3.1-12) on 29 March 2021 17:49:25
os: Linux-4.14.225-168.357.amzn2.x86_64 #1 SMP Mon Mar 15 18:00:02 UTC 2021
nodename: ip-10-0-0-59.us-west-2.compute.internal
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 1
current working directory: /var/www/myapp
detected binary path: /home/ec2-user/.pyenv/versions/3.9.2/bin/uwsgi
your memory page size is 4096 bytes
detected max file descriptor number: 65535
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uWSGI http bound on 0.0.0.0:5000 fd 3
uwsgi socket 0 bound to UNIX address mywsgi.sock fd 6
Python version: 3.9.2 (default, Mar 29 2021, 17:42:02)  [GCC 7.3.1 20180712 (Red Hat 7.3.1-12)]
*** Python threads support is disabled. You can enable it with --enable-threads ***
Python main interpreter initialized at 0x29b7370
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 145840 bytes (142 KB) for 1 cores
*** Operational MODE: single process ***
WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x29b7370 pid: 22333 (default app)
mountpoint  already configured. skip.
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 22333)
spawned uWSGI worker 1 (pid: 22366, cores: 1)
spawned uWSGI http 1 (pid: 22367)
unable to stat() /var/www/myapp/new_comer.trigger, events will be triggered as soon as the file is created
[pid: 22366|app: 0|req: 1/1] 157.107.117.161 () {38 vars in 753 bytes} [Fri Apr  2 06:18:00 2021] GET / => generated 656 bytes in 7 msecs (HTTP/1.1 200) 2 headers in 80 bytes (1 switches on core 0)

ブラウザからhttp://インスタンスのパブリックIP:5000にアクセスしてみたところ問題なくつながりました。

では今度はuWSGIのログがちゃんと指定したログファイルに入っていくようにまたディレクトリの所有者を変更していきましょう。

$ sudo chown $USER:nginx -R /var/log/uwsgi
$ ls -ld /var/low/uwsgi
drwxr-xr-x 2 ec2-user nginx 6  4月  2 04:42 /var/log/uwsgi

忘れずにmyproject.iniのlogtoのコメントアウトを外しておきましょう。

※当然、所有者を変えているのでec2-userユーザーならもうsudoせずともvimで編集できますよ!

-- ログが大幅に減っているが、/var/log/uwsgi/uwsgi.logに保存されている(後々確認してみよう!)
$ uwsgi --ini myproject.ini
[uWSGI] getting INI configuration from myproject.ini

Nginxセットアップ

Nginxの設定ファイルを作成

ようやくNginxサーバーのセットアップに入ります。

まずNginxの設定ファイルを確認してみましょう。

$ cd /etc/nginx
$ view nginx.conf

nginx.confファイルの中身についての詳細な解説は今回省略しますが、

http {
--省略--
     include /etc/nginx/conf.d/*.conf;

となっているのが確認出来たでしょうか。これは/etc/nginx/conf.dディレクトリ下の.confという名前のファイルは全て読み込むように設定されているということです。

それでは今回作成Flaskアプリ用のNginx設定ファイルを作成していきましょう。

$ cd /etc/nginx/conf.d
$ sudo vim myapp.conf

myapp.confの中身は下記のようにします。

server {
        listen 80;
        listen [::]:80;
        root /var/www;
        location / {
                    include uwsgi_params;
                    uwsgi_pass unix:///var/www/myapp/run/mywsgi.sock;
                    }
        }

rootとあるのはルートドキュメントを/var/www以下にしますよ、という意味です。

.sockファイルの指定は、既に作成したmyproject.iniで設定したのと同じパス、ファイル名を指定してください。

念のため、今設定したファイルにNginx上の文法ミスがないか確認してみましょう。

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

問題なさそうですね。

続いて、今後uWSGIとNginx間の通信プロトコルとしてHTTPではなくuWSGIデフォルトのuwsgiプロトコルを使って欲しいのでmyproject.iniファイルからhttp=0.0.0.0:5000という行を削除します。

Nginxを立ち上げてuwsgiと接続してみましょう。

$ sudo systemctl start nginx
$ sudo systemctl enable nginx
$ cd /var/www/myapp
$ uwsgi --ini myproject.ini

先ほどと同じようにインスタンスのパブリックIPを指定してブラウザからアクセスしてみてください。

自動でNginxとuWSGIがアクセス出来るようにする

さて今まではNginxが起動している状態で手動でuWSGIを立ち上げしていましたが、こんなことを毎回したくはないです。

そこでnginxと同じようにuWSGIもsystemctlコマンドで立ち上げっぱなしに出来るようにsystemd管理下のサービスにしてあげましょう。

まずsystemdが管理するサービスの設定ファイルのあるディレクトリに移動しましょう。

$ cd /etc/systemd/system
$ sudo vim uwsgi.service

下記のように設定ファイルを記述していきます。

uwsgi serviceの設定ファイル
[Unit]
Description=uWSGI instance to serve myapp
After=network.target

[Service]
User=ec2-user
Group=nginx
WorkingDirectory=/var/www/myapp
ExecStart=/home/ec2-user/.pyenv/shims/uwsgi --ini myproject.ini
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

uwsgiサービスを立ち上げしていきます。

$ sudo systemctl start uwsgi.service
$ sudo systemctl enable uwsgi.service

これでインスタンスを停止しない限り、常時Flaskで作ったWebサイトにアクセス出来るようになりました!

Flaskのソースコードを変更した際にアップデートされるようにする

Flaskのアプリのソースコード(今回ならmyproject.pyなど)を変更してもWebサイト上の表記が変わらないことがあります。

これは同じディレクトリ内の__pycache__ディレクトリにあるキャッシュファイルが原因です。
※逆にこのファイルのおかげでソースコード変更がない際は毎度処理が行われることなく済んでいます。

当然NginxとuWSGIを停止して再起動させればアップデートされたコードが反映されますが、毎回そんなことをするのは面倒くさい。。。

そこでソースコードの反映をさせるための.triggerファイルを作成しましょう。

先ほど設定したuWSGIのmyproject.iniファイルの中身を再度確認してください。

[uwsgi]
--省略--
# .triggerファイルのファイル名は任意のものを使用可能
touch-reload=%(base_dir)/%(pj_name)/new_comer.trigger

上記で指定したリロードファイルを作成してみよう。

ソースコードの変更を反映するには.triggerファイルをtouchコマンドで更新するだけで反映される。

-- リロードファイルの中身は空で良い
$ cd /var/www/myapp
$ touch new_comer.trigger

以上で第一部は終わりです。お疲れ様でした!

おまけ tips&エラー集

筆者がサーバーを立ち上げる中でぶつかったエラー及びその原因についておまけで載せていきます。
実際にみなさんがサーバー構築をやっていくとここに出ていないエラーが出てくるかもしれませんし、同じエラー文でも違う原因で発生しているかもしれません。
ただここに出てくる解決方法を試してみれば何か解決の糸口が掴めるかもしれません。

毎回ログに残る時間が日本時間でない

原因:設定漏れ
これはエラーではなく単なる設定漏れですが、

$ sudo su -
$ timedatectl set-timezone Asia/Tokyo

とタイムゾーンを変更してやるだけで問題ないです。

502 Bad Gatewayと出てしまう

原因:uWSGIとの接続が上手くいっていない
NginxとuWSGIの接続が上手くいかない場合に発生します。
具体的に言うとrun.pyを設定ファイルの中でTypoしていました。

こういったぽかミスであってもtailコマンドでエラーログを追っていくと「No such file」等のエラー原因の特定に繋がるメッセージが出ていることが多々あります。

$ sudo tail -f /var/log/nginx/error.log

Internal Server Errorと出てしまう

原因:Pythonの文法エラー
「Internal Server Error」とははっきり言ってエラー原因の手がかりが読み取れないエラーです。
ただPythonの文法エラーである場合も多いので、一旦NginxとuWSGIを停止させFlaskの簡易サーバーをデバッグを行うことで原因が見つかることもあります。

GitHubで編集を提案

Discussion

mohreymohrey

非常にわかりやすい解説ありがとうございます。
基本的なところの質問なのですが、/var/www/myapp は、Amazon Linuxのルートのvarに変更を加えていらっしゃいますか?それとも/home/ec2-user 下に/var以下を作っていますか?
よろしくお願いします。