🌐

Webアプリをデプロイしよう

に公開

進め方の全体像

さて、いよいよアプリのデプロイまで進めていこうと思います。
ゴールとしては

  • Linux
  • Docker
  • SQLite以外のDB
  • Django ※PythonのWebフレームワーク

を使ったアプリケーションを作っていきます。
アプリケーションそのもの(Djangoそのもの)はあまり作りこまず、こうやればWebアプリが公開できるというところまで理解することを目的とします。

全体がイメージできれば、後は各技術を勉強していけばよいので「何を学べばいいかわからない」「結局プログラミングって何ができるの?」を脱却できると思います。

それでは1つずつ進めていきましょう

注意

なるべく細かく記載していくつもりですが、おそらく作っていて気になるところをすべては記述しきれません。
適宜ChatGPTとかも駆使しながら1ステップずつ進んでいただければと思います。
なんやかんや、そういう「調べて何とかする」技術・姿勢がエンジニアとしての資質になると思いますので...(言い訳)

注意点2

最終的に「Render」というサービスにアプリをデプロイします。
無料で使えるサービスですが、DBのほうは無料期間に限りがあり、それを過ぎると動かなくなるはずですので実際に運用し続けるアプリのデプロイには向かないこと、ご了承ください

Linuxの準備

まずはLinuxとは?というところからですが、これはWindowsやMacなどと同じOSの一種です。
一般に利用するPCで使うことはあまりないと思いますが、Webアプリの世界ではむしろLinuxがスタンダートです。

Linuxが無料で使えるオープンソースのOSであったために、サーバーのOSとして爆発的に広がり、今や各種サーバーの周辺ツールはLinuxであることがデフォルトになっています。

そんなLinuxですが、新たにPCを買ったりせずともWindows上に仮想のLinuxを作成することができ、それをWSL2(Windows Subsystem for Linux 2)と言います。
まずはこれを使って自PCの中でアプリを作成し、それを後からRenderにデプロイします

WSL2の設定

コマンドラインにて、下記を実行します

wsl --install
wsl --install -d Ubuntu

これらコマンドで、WSL2を有効化したうえで、UbuntuというLinuxの一種をWidows上で使えるようになりました。
ここまで来たら一度再起動し、

wsl -l -v

でUbuntuがちゃんとインストールされているか確認してください。
その後、

sudo apt update
sudo apt upgrade

で今後動かすコマンドラインの機能を最新化しておきます。
パスワードを求められたら、Windowsにログインするときと同じパスワードでOKなはずです。
Do you want to continue? [Y/n]
みたいに「アップデートする?(意訳)」を求められたら、「y」を入力してエンターで処理が実行されます。

ここまで出来たら、「\wsl$\Ubuntu\home」をエクスプローラーで開き、ユーザー名のフォルダが作成されていることを確認してください。
今後、そのフォルダの中を基準に各種操作を行います。

Pythonの設定

さて、Windows PCと今回準備したWSL2のUbuntuは独立しているので、WindowsにPythonが入っていてもUbuntu側でPythonの準備が必要になることがあります。

wsl

を実行し、インストールしたWSL2を起動します。

起動出来たら、下記を実行しPythonの有無を確認してください

python3 --version

私は最初からインストールされていました。

インストールされていなければ

sudo apt install python3 python3-venv python3-pip

で必要な機能をインストールします。

最初から入っていた人も、Pythonの仮想環境を作る機能はインストールされていないことがあるので

sudo apt install python3-venv

にてインストールを行いましょう。

仮想環境とは?

Pythonは豊富なライブラリがあり、それが人気言語である理由でもあるのですが、それゆえにちゃんと整理しないとライブラリがぐっちゃぐちゃになります。

そこで、整理用の箱を作って、その中にライブラリを入れていくことで整理しやすくしながら後続の作業を進めていきます。
この整理用の箱を「仮想環境」と言います。

Djangoプロジェクトの準備

専用フォルダの準備

いよいよPythonのフレームワーク、Djangoを使ってアプリを作っていきます。

まずは、「\wsl$\Ubuntu\home<ユーザー名>」のフォルダにプロジェクト用のフォルダを作りましょう。
エクスプローラで開いているのであれば、いつもWindowsでフォルダを作るように操作してもらえればフォルダ作成することができます。
※もちろんコマンドラインで作成いただいても構いません。

私はmyporjectという名称で作成しています。

準備ができたら

cd ~/myproject

で、作成したフォルダにコマンドラインの基準を移動する。

続いて、

python3 -m venv venv

でPythonの仮想環境を作成し

source venv/bin/activate

で作成した仮想環境を有効にします。
下図のように、(venv) ...という表記になればOKです

Djangoのインストール・アプリ作成

pip install django psycopg2-binary dj-database-url gunicorn

上記にて、仮想環境にDjangoと必要なライブラリをインストールします。

  • psycopg2-binary:PostgreSQL用ドライバ
  • dj-database-url:DATABASE_URLの読み込みを簡単にする
  • gunicorn:本番用のWSGIサーバ
    ※wsgiとは、Python(Django)を実行するAPサーバーの役割と理解するといいと思います。
django-admin startproject mysite

これで、mysiteというDjangoのアプリが出来上がります。

作成出来たら

cd mysite
python manage.py migrate
python manage.py runserver

を実行し、Djangoアプリを起動します。

PHPでやったときの、ローカルサーバーを立ち上げた状態になるのでブラウザから
http://127.0.0.1:8000/
へアクセスし、Djangoのデフォルト画面が表示されればOKです

migrateについて

Djangoは、直接SQLを記述しなくともDBへのアクセスを可能にする機能があります。
その1種として、Pythonで記述したモデルをもとにテーブルを作成することができるのですが、この「モデルをもとにDBへテーブル情報を連携する」のがmigrateの役割です。
デフォルトでもいくつかモデルが用意されているので、まずはmigrateを行いその情報をDBへ反映します。

※この時、DBはmysite内に作成されたdb.sqlite3が該当します

簡易アプリの作成

デプロイが目的なので詳細を作りこんだりはしませんが、少なくともDBを使うようなアプリケーションにはしたいので、

  • ユーザー登録
  • ログイン
  • (ログイン後に)ユーザー名を表示する
    機能を作ろうと思います。

Djangoには、複数のアプリを組み合わせて1つのプロジェクトとする概念が存在しているので

  • createUser
  • loginApp
  • user
    という3つのアプリを、下記コマンドにて作成します
python manage.py startapp <appname>

※appnameに各アプリ名を記述してください

ちなみに、usersテーブルがデフォルトで存在しているので、テーブルを準備する必要はありません。

createUser

Djangoそのものについてはこの記事では詳しく触れません。
基礎はいろんなところに転がっているので、気になる人は先に見てもいいかもです。

ググったら出てきた↓
https://www.youtube.com/watch?v=r9QUdzVGHJU

各種コードの中身を記載していきます。

まず、エクスプローラーでcreateUserフォルダを開き、適当なファイルを開きます。

おそらく上記のようなメッセージが出るので、「Allow」をクリックしてコードを開きます。

※Windowsと準備したWSL2は別環境として扱われているので、ざっくりいうと「なんか知らんPCのフォルダ開こうとしてるけどホントに開いていい?」と聞かれています。ドラッグアンドドロップだと上記制限で開けないこともあるので注意。

さて、まずはurls.pyを開き、下記を記述してください

from django.urls import path
from . import views

urlpatterns = [
    path('register/', views.register, name='register'),
]

※上側にコメントで使い方が記述されていると思いますが、これは残しても残さなくてもいいです。

register/ っていうリンクがクリックされたらviewsファイルのregisterを実行するよというコードです。また、そのリンクにregisterという名前を付けています。

続いて、views.pyを開き

from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login

def register(request):
    if request.method == "POST":
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect('profile')
        else:
            return render(request, 'createUser/register.html', {'form': form})
    else:
        form = UserCreationForm()
        return render(request, 'createUser/register.html', {'form':form})

を記述。

データがPOSTされていれば有効性を確認してユーザー登録。
有効でない、またはPOSTではないときはformの情報を渡してregister.htmlへ遷移するというコードになっています。

そのregister.htmlは、createUserのフォルダの中にtemplates\createUser\register.htmlという階層で作成します。

<head>
    <meta charset="utf-8" />
    <meta lang="ja" />
    <title>ユーザー登録</title>
</head>
<body>
    <h2>ユーザー登録</h2>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p}}
        <button type="submit">登録する</button>
    </form>
</body>

csrf_tokenとはDjangoデフォルトのセキュリティ機能として必須なトークン
form.as_pは、registerメソッドから渡されたformの情報を表示しています。

loginApp

同様にloginAppとuserのほうも作成を進めていきます。

urls.py

from django.urls import path, include
from . import views

urlpatterns = [
    path('login/', views.login_page, name='login')
]

views.py

from django.shortcuts import render, redirect
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import login, logout
from django.contrib import messages

def login_page(request):
    if request.user.is_authenticated:
        return redirect('profile')

    if request.method == 'POST':
        form = AuthenticationForm(request, data=request.POST)
        if form.is_valid():
            user = form.get_user()
            login(request, user)
            return redirect('profile')  # プロフィールページへリダイレクト
        else:
            messages.error(request, 'ユーザー名かパスワードが間違っています。')
    else:
        form = AuthenticationForm()
    return render(request, 'loginApp/login.html', {'form': form})

def logout_view(request):
    logout(request)
    return redirect('login')

templates\loginApp\login.html

<head>
    <meta charset="utf-8" />
    <meta lang="ja" />
    <title>ログインページ</title>
</head>
<body>
    <h2>ログイン</h2>
    <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">ログイン</button>
    </form>
    {% if messages %}
    {% for message in messages %}
        <p style="color:red">{{ message }}</p>
    {% endfor %}
    {% endif %}
</body>

user

urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('profile/', views.profile, name='profile'),
]

views.py

from django.shortcuts import render
from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    return render(request, 'user/profile.html')

※@login_requiredとは、文字通り「ログイン状態じゃなきゃ動かせないよ」という記述です。

templates\user\profile.html

<head>
    <meta charset="utf-8" />
    <meta lang="ja" />
    <title>プロフィール</title>
</head>
<body>
    <h2>プロフィール</h2>
    <p>ようこそ、{{ request.user.username }} さん!</p>
</body>

mysite

さて、ここまで作ってきた3つの機能を統合します。
mysiteフォルダにはもう1つmysiteというフォルダがあると思うので、そこに入ると、プロジェクト全体の設定等があるのでそこを整理していきます。

urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('createUser/', include('createUser.urls')),
    path('user/', include('user.urls')),
    path('login/', include('loginApp.urls')),
]

includeは、各アプリの入り口だけ設定しておき、後はアプリ側のurls.pyの設定に従うというイメージです。
例えば、createUser/registerにアクセスされると、createUser/urls.pyに記述した/registerの挙動をします。
※このあたりは動かしてみるとイメージがつかみやすいと思います。

settings.py
ここのINSTALLED_APPSに、今回作ったcreateUser, loginApp, userを追加

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'createUser',
    'user',
    'loginApp',
]

また、未ログイン時にログイン必須ページへアクセスしたときの情報もsettings.pyに追記します

# 未ログイン時
LOGIN_URL = '/login/login'

挙動の確認

python manage.py runserver

にてローカルサーバーを起動し、
http://127.0.0.1:8000/createUser/register

http://127.0.0.1:8000/login/login

が動くか確認してみましょう。

Renderの準備

さて、ここまでsqlite3をDBとして扱ってきましたが、実際の開発ではDBは外部サービスを使うことも多いです。
※AWSのRDSなど

そこで今回は無料で使える(期間制限あり)DBサービスの1つ、Render postgresを使っていきます。

GitHubのアカウント作成

後続対応で、GitHubにアップロードしたDjangoアプリをRenderでビルド・デプロイする...という流れをとるので、RenderにはGitHubアカウントでサインアップ・ログインしたいです。

まず、GitHubアカウントを持っていない人はアカウントを作成しましょう
GitHubアカウント作成・ユーザーID確認マニュアル

作成出来たら
https://dashboard.render.com/register
へアクセスして、GitHubでユーザー登録をします

Render postgresへの接続

ログインできたら、「Create new project」から新しいプロジェクトを作成。
プロジェクトに入り、右上にある+NewからPostgresを選択する

Nameはお好みで、Projectでは先ほど作成したプロジェクトを選択。
Instance TypeをFreeにして、DBを作成します。
※プロジェクト作成時やDB作成時に地域(Region)選択が必要なところがありますが、特にこだわりがなければ物理的に位置の近いSingaporeでいいと思います。

例えばDjango-sampleという名前で作成すると、プロジェクト内に下記のようにDBが作成されます

※もう1つ、地球マークのDjango-sampleはデプロイしたWebアプリになるので、現時点では存在しないはずです。

このリンクの中に入り、Connectionsの欄に接続情報が記載されています。

  • Hostname
  • Port
  • Database
  • Username
  • Password
    を確認のうえ、Djangoのsettings.pyを書き換えていきます
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('DB_NAME'),
        'USER': os.getenv('DB_USER'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST'),
        'PORT': os.getenv('DB_PORT', '5432'),
        'OPTIONS': {
            'sslmode': 'require', 
        },
    }
}

それぞれ、オープンにしていい情報ではないので、このコードではos.getenvとして環境変数から情報を取得するようにしています。(後ほど解説します)

自PCで動かすうちはべた書きで問題ないかと思いますので、NAME~PORTまでRenderで確認した情報を記述してください。

各種準備が完了したら

python manage.py migrate

を実行し、デフォルトのテーブル情報をRender postgresに連携します。

その後、

python manage.py runserver

でローカルサーバーを起動し、同じように動くか確認してください

注意

HOSTは、Renderで表示されたHostnameをそのまま記述してもつながりません。
シンガポールにDBを作成したなら
.singapore-postgres.render.com
をHostnameに追加した値をセットしてください

余談:クラウドとは

今回利用したRenderがまさにクラウドのイメージです。
従来であれば、DB専用のコンピューターを購入し、そこへPostgreSQLをインストール。
その後、そのコンピューターへ接続できるようにアプリを設定するという対応が必要だったのですが、そんなめんどくさいことしなくても、こうやって画面でポチポチすればDBやサーバーを利用できるようになったのがクラウドが主流になっていった理由です。

※その分ちゃんと使うと利用料がかかりますが...それでも、サーバールームとかを用意しなくてもよいなどメリットのほうが大きいと判断されています。

Dockerの準備

自PCでの準備、最後のステップです。
Dockerそのものの解説はここではスキップします。

ものすごくざっくりいうと、開発環境の再現を簡単にする技術です。
今回例えばPythonとDjangoの準備をして、postgresへの接続ライブラリもpipダウンロードして...と行ってきましたが、本来であればこれは実際にアプリをデプロイするサーバーでも必要な作業になります。

この位簡易なアプリであればさほど大変ではないですが、これが大規模アプリになって、さらにテスト環境が必要で...とかになってくると設定がかなり大変になります。
それでいて本番環境とテスト環境にずれが生じると、テストで動いていたところが本番でバグになったり...

めんどくさくてやってられないので、環境を持ち運べるようにするイメージで「コンテナ」技術と呼ばれたりします。

ググったら出てきた↓
YouTubeのvideoIDが不正ですhttps://www.youtube.com/watch?v=YfaB3PJv1f

Dockerfileの準備

さて、コンテナを作るための情報をこのDockerfileに記述していきます
Dockerファイルは、myproject直下に作成してください。

# ベースイメージを指定(Python 3.12.3の軽量版を使用)
FROM python:3.12.3-slim

# コンテナ内の作業ディレクトリを設定(以降のコマンドはこの場所で実行される)
WORKDIR /app/mysite

# ホストのrequirements.txtをコンテナ内にコピー(Pythonパッケージのインストール用)
COPY requirements.txt /app/mysite

# 依存パッケージをインストール(キャッシュを使わずすっきりインストール)
RUN pip install --no-cache-dir -r requirements.txt

# プロジェクトの全ファイルをコンテナ内にコピー
COPY mysite/ /app/mysite

# Pythonの標準出力をバッファリングしない設定(ログがリアルタイムに見える)
ENV PYTHONUNBUFFERED=1

# コンテナの8000番ポートを開放(Djangoのデフォルトポート)
EXPOSE 8000

# コンテナ起動時にDjango開発サーバーを起動
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

さらにDjangoなど、インストールが必要なものはrequirement.txtにまとめます。
これもmyproject直下に作成してください

Django==5.2.8
bcrypt==3.2.2
cryptography==41.0.7
PyJWT==2.7.0
requests==2.31.0
pytz==2024.1
setuptools==68.1.2
wheel==0.42.0
gunicorn==20.1.0
python-dotenv
psycopg2-binary

.envの作成

さて、ここでさらに.envというファイルを用意します。(これもmyproject直下に)
例えばDBの接続情報とか、settings.pyの中に記述されているSECRET_KEYとか、そのままGitHubに上げるとヤバそうな情報があります。
これを.envというファイルにまとめ、そこから読み込む形に変えておきます。

実際にはRender側に環境変数というものを準備し、そこを読み込むのですが、開発時にはそれと同じ挙動を再現するために.envに必要情報を記述していきます。
※つまり、実際には.envファイルもGitHubにはアップロードしません。

SECRET_KEY='django-xxx'

DB_NAME=xxx
DB_USER=xxx
DB_PASSWORD=xxx
DB_HOST=xxx.singapore-postgres.render.com
DB_PORT=5432

SECRET_KEYには、もともとsettings.pyに記述されていた値を入力してください。
DBの情報は、一度settings.pyにべた書きしたものをこちらに記述してください。
そのうえで、settings.pyを書き換えていきます。

# 一番上に
from dotenv import load_dotenv
import os
load_dotenv()

# 中略
SECRET_KEY = os.getenv('SECRET_KEY')

# 中略
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('DB_NAME'),
        'USER': os.getenv('DB_USER'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST'),
        'PORT': os.getenv('DB_PORT', '5432'),
        'OPTIONS': {
            'sslmode': 'require', 
        },
    }
}

Docker Desktopのインストール

Windowsにこのツールをインストールし、WSL2で相乗りするかたちでDockerを利用します。

https://docs.docker.com/desktop/setup/install/windows-install/
こちらにアクセスし、Windows用インストーラをダウンロードしてください。

インストールまで完了したら、Docker Desktopを起動します。

図のように、WSL2でDockerが使えるように設定を変更、Docker Desktopを立ち上げなおします。

docker --version

でバージョン情報が返ってきたら準備OKです。

※Docker Desktopに相乗りしている形なので、Windows側でDocker Desktopが起動していないと実行できません。

cd ~/myproject

でmyproject直下へ移動、下記でDockerイメージを作成します。

docker build -t render_project .

イメージの作成が完了したら、

docker run -d --name render_project -p 8000:8000 --env-file .env render_project

で.envを読み込みながら、作成したDjangoアプリがDockerのコンテナとして起動します。

この時点で、ローカルサーバーが立ち上がっている状態なので、
http://127.0.0.1:8000/login/login
等にアクセスして、問題なく動作するか確認してください

動かなかったとき

※各種エラーが起き、その原因を修正したのちのことを想定しています。
修正が完了したら、再度docker buildコマンドを実行してイメージを作り直します。
そして、エラーを起こしているコンテナが残ったままになっているので

docker stop render_project
docker rm render_project

でコンテナを削除し、

docker run -d --name render_project -p 8000:8000 --env-file .env render_project

でコンテナを作成しなおして下さい。

GitHubへアップロードし、Render上でデプロイする

Dockerfileの修正

一部開発用の機能になっている部分を修正します

# CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
CMD ["gunicorn", "mysite.wsgi:application", "--bind", "0.0.0.0:8000"]

runserverではなく、wsgiを使った方法へ切り替えています。

WSL2へGitのインストール

sudo apt install -y git

インストールされたかどうかは下記で確認できます

git --version

基本的な情報をconfigに登録します

git config --global user.name "あなたのGitHubユーザー名"
git config --global user.email "あなたのGitHub登録メールアドレス"

認証

次にGitHubへデータをアップロードする(push)ための設定ですが、現在(25/11)時点でGitHubはパスワードを使ったpushを認めていないようなので、パスワード以外の認証設定をします。

SSHキーを作成

ssh-keygen -t ed25519 -C "your_email@example.com"

※何度か確認があるがエンターキー連打でOK

公開鍵の情報を取得

cat ~/.ssh/id_ed25519.pub

GitHub右上のプロフィール → Settings → SSH and GPG keys → New SSH key
→ さきほどの内容を貼り付け

接続確認

ssh -T git@github.com

GitHubへpush

myprojectをGitHubへアップロードしていきます。

Gitリポジトリを作成

cd ~/myproject
git init

.gitignore を設定

echo ".env" >> .gitignore
echo "__pycache__/" >> .gitignore
echo "*.pyc" >> .gitignore
echo "*.sqlite3" >> .gitignore
echo "venv/" >> .gitignore

これで、.envを含めてGitに上げないファイルが設定されます。

初回コミット

git add .
git commit -m "Initial commit"

GitHub上で新しいリポジトリを作成

GitHubにログインし、Dashboardを開くと左上にNewがあるのでそこからリポジトリを作成

push

git remote set-url origin git@github.com:<ユーザー名>/<リポジトリ名>.git
git push

これでGitHub上にアップロード(push)は完了です。

Renderで新しい「Web Service」を作成

postgresを作成したとき同様に、プロジェクト内の+Newから「Web Service」を選択。
先ほどpushまで完了したGitリポジトリを選択します。

  • Nameはお好みで設定
  • Projectはpostgresの時に作成したプロジェクトを選択
  • LanguageはDocke
  • Instance TypeをFree
    に設定します。

そして、下部にEnvironment Variablesを入力するところがあるので、ここに.envを同じ情報を追加していきます。

そしてDeploy Web Serviceをクリック。

settings.pyの修正

これで完了...と思いきや、最後に1つ修正すべきところがあります。
上記まで完了してようやくリンクが確定するので、ALLOWED_HOSTSに
your-app-name-xxx.onrender.com
の部分を追加して、

git add .
git commit -m "settings.py修正"
git push

でpushしなおします。

Render側で再度自動でビルドしなおすので、それが完了次第、できたページが問題なく動作することを完了し、Webサービスのデプロイは完成です🎉

Discussion