IBM Cloud VPCの仮想インスタンスでPython(Django)アプリ作成

# 1.はじめに
IBM Cloudの Virtual Private Cloud(VPC)の仮想インスタンスにPythonのアプリケーションフレームワークであるDjangoのアプリケーションを起動するまでの手順になります。
Django自体、もう目新しいものではありませんが、アプリケーションを評価したい時に、その時のバージョンで、すぐに作れるように、改めて動作を確認した結果として新規に投稿します。

今回は、VPCの仮想インスタンスで、Webサーバ、DBサーバを作成した後、Djangoの[はじめての Django アプリ作成](https://docs.Djangoproject.com/ja/4.1/intro/tutorial01/)で作成する「簡単な投票 (poll) アプリケーション」をデプロイし動作を確認するところまでの流れになります。 
   
# 2.システム構成
![システム構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209170/6fe3d3b0-3596-9c5e-dfe9-9d7440947271.png)
IBM Cloudの東京リージョンにVPCを作成し、ゾーン東京１に２つの仮想インスタンスを作成しています。

役割
- 仮想インスタンス1(外部アクセスからのアクセス可能)
    - Webサーバ
        - OS: Ubuntu 20.04 
        - Webサーバ : Djangoのrunserver
        - Python : 3.8.5
        - Django : 4.0.7

- 仮想インスタンス2(外部アクセスからのアクセス不可)
    - DBサーバ 
        - OS: Ubuntu 20.04 
        - PostgreSQL : 12

Virtual Private Cloud(VPC)と仮想インスタンスの作成は、下記のQiita記事を参考に構築しました。

https://qiita.com/6onoda/items/06f9a4b1090d05ea0813

# 3.作成するアプリケーションのイメージ
このアプリは、Djangoの[はじめての Django アプリ作成](https://docs.Djangoproject.com/ja/4.1/intro/tutorial01/)で作成する「簡単な投票 (poll) アプリケーション」で
今日の天気を投票してもらうサンプルデータを作成して、実行しています。
![アプリケーションのイメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209170/3c250bfa-09fd-97bb-e2f6-dbdd9bf7e268.png)

# 4.構築手順

## Ubuntuの初期設定

- Webサーバ・DBサーバ共通作業
1. リモートサーバへのログイン
まずはリモートサーバにログインします。

    ```console
    ※クライアントでの作業
    ssh -i [秘密鍵のファイル名] root@[WEBサーバのIPアドレス]
    ```

    (注)DBサーバーは、フローティングIPを設定していないため、外部からSSHでログインすることができません。
    Webサーバーに、DBサーバーに接続するための秘密鍵を配置して、DBサーバにSSHでログインします。

    ```console
    ※Webサーバでの作業
    $ ssh -i [秘密鍵のファイル名] root@[DBサーバのIPアドレス]
    ```

1. ユーザー作成
    VPCの仮想インスタンスの初期状態では一般ユーザーが存在しない為、作業用のsudo ができる一般ユーザーを作成します。　

    ```console
    $ adduser [ユーザー名]
    $ gpasswd -a [ユーザー名] sudo
    ```
    
    ユーザーが作成できたら、作成したユーザーにスイッチして、作成ユーザーにて作業を行います。
    ```console
    $ su - [ユーザー名]
    ```

1. パッケージの一覧更新
    パッケージリストの更新を実行
    ```console
    $ sudo apt-get update
    ```

## Python(周辺モジュール)、PostgreSQLクライアント導入

- Webサーバーでの作業
1. Python3、pip、PostgreSQLのクライアントのインストール
   Python3、pip、PostgreSQLへ接続するためのクライアントをインストールします。 
    ```console
    $sudo apt-get install python3-pip python3-dev libpq-dev postgresql-client-12
    ```

## PostgreSQL導入
- DBサーバーでの作業
1. PostgreSQLをインストールします。

    ```console
    $ sudo apt-get install postgresql postgresql-contrib
    ```
1. PostgreSQLのバージョンとPostgreSQLが起動していることを確認します。 
    Ubuntu 20.04ではPostgreSQL Version12が導入されます。

    ```console
    $ psql --version
    psql (PostgreSQL) 12.12 (Ubuntu 12.12-0ubuntu0.20.04.1)
    ```
    
    ```console
    $ sudo systemctl status postgresql
    ● postgresql.service - PostgreSQL RDBMS
         Loaded: loaded (/lib/systemd/system/postgresql.service; enabled; vendor preset: enabled)
         Active: active (exited) since Fri 2022-09-30 15:05:10 UTC; 2min 33s ago
       Main PID: 3642 (code=exited, status=0/SUCCESS)
          Tasks: 0 (limit: 9508)
         Memory: 0B
         CGroup: /system.slice/postgresql.service
    
    Sep 30 15:05:10 poll-db systemd[1]: Starting PostgreSQL RDBMS...
    Sep 30 15:05:10 poll-db systemd[1]: Finished PostgreSQL RDBMS.
    ```

1. PostgreSQLにDBと接続ユーザーを作成します。

    PostgreSQLのデータベースにログインします。
    ```console
    $ sudo su - postgres
    $ psql
    psql (12.12 (Ubuntu 12.12-0ubuntu0.20.04.1))
    Type "help" for help.
    ```

    DBの作成
    ```sql
    postgres=# CREATE DATABASE polls;
    CREATE DATABASE
    ```

    PostgreSQLのユーザーの作成(Djangoから接続するためのユーザー、パスワード)
    ```sql
    postgres=# CREATE USER poll_user WITH PASSWORD 'poll_password';
    CREATE ROLE
    ```
    作成したユーザーに権限追加
    ```sql
    postgres=# GRANT ALL PRIVILEGES ON DATABASE polls TO poll_user;
    GRANT
    ```
    DBからログアウト
    ```console
        postgres=# \q
    ```

1. PostreSQLの設定ファイル(postgresql.conf)を変更します。

    listen_addressesの設定がデフォルトでは、 localhostのみlistenしている状態なので、 DBサーバ外部からも接続できるようにlisten_addressesを「 * 」に変更します。

    ```console
    $ cd /etc/postgresql/12/main/
    $ vi postgresql.conf
    ```

    - 変更前 (６０行目）
    ```console
    #listen_addresses = 'localhost'		# what IP address(es) to listen on;
    ```
    - 変更後 (６０行目）
    ```console 
    listen_addresses = '*'		# what IP address(es) to listen on;
    ```

1. PostgreSQLの設定ファイル(pg_hba.conf)を変更。
    WebサーバからPostgreSQLに接続できるようにpg_hba.confに Webサーバのサブネット(例:10.10.0.0/24 )からmd5認証で接続できるように設定を追加します。

    ```console
    $ vi pg_hba.conf
    ```  

    - 追加内容(末尾)
 
    ```console
    host    all             all             10.10.0.0/24            md5
    ```

1. postgresqlユーザーから 作業用ユーザーに戻ります。

    ```console
    $ exit
    logout
    ```

1. PostgreSQLを再起動して設定を反映します。[作業ユーザーで実行]

    ```console
    $ sudo systemctl restart postgresql
    ```
    
    PostgreSQLの状態を確認すると設定ミスがない限りは起動しているはずです。
    ```console
    $ sudo systemctl status postgresql
    ● postgresql.service - PostgreSQL RDBMS
         Loaded: loaded (/lib/systemd/system/postgresql.service; enabled; vendor preset: enabled)
         Active: active (exited) since Sun 2022-10-02 02:44:42 UTC; 7s ago
        Process: 16723 ExecStart=/bin/true (code=exited, status=0/SUCCESS)
       Main PID: 16723 (code=exited, status=0/SUCCESS)
    
    Oct 02 02:44:42 poll-db systemd[1]: Starting PostgreSQL RDBMS...
    Oct 02 02:44:42 poll-db systemd[1]: Finished PostgreSQL RDBMS.
    ```

## 接続確認(WebサーバからPostgreSQLまで)
- Webサーバーでの作業
PostgreSQLサーバの設定が終わったので、WebサーバからPostgreSQLサーバへの接続を確認します。

1. PostgreSQLのpsqlクライアントが導入されていることを確認します。
    導入されていれば、 Version 12であることが確認できます。　psqlのコマンドがない場合は、 Ubuntuの初期設定の最初から確認してください。
    ```console
    $  psql --version
    psql (PostgreSQL) 12.12 (Ubuntu 12.12-0ubuntu0.20.04.1)
    ```
    
1. psqlコマンドを使ってPostgreSQLのDBに接続します。

    ```console
    $ psql -h [PostgreSQLサーバのIPアドレス] -U [PostgreSQLのユーザー名] [DB名]
    Password for user poll_user: [パスワード入力]
    ```

    正常にDBに接続できたなら、以下の様にプロンプトが表示されます。
    ```console
    psql (12.12 (Ubuntu 12.12-0ubuntu0.20.04.1))
    SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
    Type "help" for help.
    ```

1. DBに接続できているか、「 ¥l 」 コマンドを実行して、DBの一覧を表示します。

    template0、template1、postgresqlに加えて、作成したDB(例: polls)が出力されることを確認してください。   
    ```console
    polls=> \l
                                  List of databases
       Name    |  Owner   | Encoding | Collate |  Ctype  |   Access privileges    
    -----------+----------+----------+---------+---------+------------------------
     polls     | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =Tc/postgres          +
               |          |          |         |         | postgres=CTc/postgres +
               |          |          |         |         | poll_user=CTc/postgres
     postgres  | postgres | UTF8     | C.UTF-8 | C.UTF-8 | 
     template0 | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =c/postgres           +
               |          |          |         |         | postgres=CTc/postgres
     template1 | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =c/postgres           +
               |          |          |         |         | postgres=CTc/postgres
    (4 rows)
    ```

これで、WebサーバからDBサーバの接続まで構築が完了しました。　次からは、Django環境を作成します。

## Djangoの環境作成

- Webサーバでの作業
1. pipコマンドを使って、Djangoと PythonからPostgreSQLに接続するためのアダプタ「psycopg2」をインストールします。

    ```console
    $ sudo pip3 install Django==4.0.7 psycopg2
    ```

    今回、Djangoは、バージョン4.0.7を指定して導入しましたが、導入可能なバージョンの一覧は、以下の出力の様に、存在しないバージョンを存在しない文字列を入力して、 
    
    ```console
    $ sudo pip3 install Django==4.xxxx
    ERROR: Could not find a version that satisfies the requirement Django==4.xxxx (from versions: 1.1.3, 1.1.4, 1.2, 1.2.1, 1.2.2, 1.2.3, 1.2.4, 1.2.5, 1.2.6, 1.2.7, 1.3, 1.3.1, 1.3.2, 1.3.3, 1.3.4, 1.3.5, 
    <<省略>>
    3.2.14, 3.2.15, 4.0a1, 4.0b1, 4.0rc1, 4.0, 4.0.1, 4.0.2, 4.0.3, 4.0.4, 4.0.5, 4.0.6, 4.0.7, 4.1a1, 4.1b1, 4.1rc1, 4.1, 4.1.1)
    ERROR: No matching distribution found for Django==4.xxxx
    ```

## Djangoアプリケーションの準備

今回は、Djangoの[はじめての Django アプリ作成](https://docs.Djangoproject.com/ja/4.1/intro/tutorial01/)で作成する「簡単な投票 (poll) アプリケーション」を使って動作確認を行います。

古いソースコードですが、Githubの `DigitalOcean Community` に「簡単な投票 (poll) アプリケーション」があったので、このソースコードを使用します。

1.  「簡単な投票 (poll) アプリケーション」を GitHubのリポジトリからクローンします。

    ```console
    $ git clone https://github.com/do-community/Django-polls.git
    ```

1. クローンが完了すると `Django-polls` フォルダが作成されます。 今回は `/app`フォルダで動作させたいので、`Django-polls`を`/app`フォルダに変更し、フォルダの権限も `作業ユーザー`(例: dai.dai)に変更します。 

    ```console
    $ sudo mv Django-polls /app
    $ sudo chown -R dai.dai /app
    ```

    設定が完了すると/appのフォルダ構成は以下のように、Djangoのプロジェクトのファイルで構成されています。

    ```console
    $ ls -l /app
    total 24
    -rw-rw-r-- 1 dai dai 1079 Oct  2 02:50 LICENSE
    -rw-rw-r-- 1 dai dai 1359 Oct  2 02:50 README.md
    -rwxrwxr-x 1 dai dai  538 Oct  2 02:50 manage.py
    drwxrwxr-x 2 dai dai 4096 Oct  2 02:50 mysite
    drwxrwxr-x 5 dai dai 4096 Oct  2 02:50 polls
    drwxrwxr-x 3 dai dai 4096 Oct  2 02:50 templates
    ```

### settings.pyの設定(Djangoの設定ファイル)

Djangoの設定ファイル `settings.py` を編集します。

変更箇所は、次の３つのパラメーターです。

    - SECRET_KEY 
    - ALLOWED_HOSTS
    - DATABASE

1. secret_keyの再生成
    SECRET_KEYは、パラメータは、以下の様に解除されているので、再生成します。
    `SECRET_KEY = 'your_secret_key_here'`

    Django の管理用のスクリプト manage.py を使ってShellを起動します。
    
    ```console
    $ python3 manage.py shell
    Python 3.8.10 (default, Jun 22 2022, 20:18:18) 
    [GCC 9.4.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    (InteractiveConsole)
    >>>
    ```

    Shellが起動したら以下の内容を実行して、secret_keyの再生成を行います。
    ```console
    from Django.core.management.utils import get_random_secret_key
    secret_key = get_random_secret_key()
    text = 'SECRET_KEY = \'{0}\''.format(secret_key)
    print(text)
    ```

    出力された内容をsettyngs.pyのSECRET_KEYの値に記入します。
    - 変更前
    ```console
    SECRET_KEY = 'your_secret_key_here'
    ```
    - 変更後
    ```console
    SECRET_KEY = 'xxxtx!u3j@ix8h59_xxxbjc8u1l_y+f&d%_l!rc_cus@ua9xxx'
    ```

1. ALLOWED_HOSTS (アクセス許可設定)

    デフォルトでは `[]`ローカルホストのみ接続許可になっているので、
    全て許可の `*`、もしくはWebサーバのIPアドレスを設定します。

    - 変更前
    ```console
    ALLOWED_HOSTS = []
    ```
    - 変更後
    ```console 
    ※全て許可の場合
    ALLOWED_HOSTS = ['*']
    ＊IPアドレス指定の場合
    ALLOWED_HOSTS = ['111.222.333.444']
    ```

1. DATABASE設定
    DjangoからDBに接続するための設定を行います。初期設定では`sqlite3`が設定されているのでPostgreSQLの設定に変更します。
    
    - 変更前
    ```console
    DATABASES = {
        'default': {
            'ENGINE': 'Django.db.backends.sqlite3',
            'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        }
    }
    ```
    - 変更後
    ```console
    DATABASES = {
        'default': {
            'ENGINE': 'Django.db.backends.postgresql_psycopg2',
            'NAME': 'DB名',
            'USER': 'PostgreSQLの接続ユーザー名',
            'PASSWORD': 'PostgreSQLの接続ユーザーのパスワード',
            'HOST': 'DBの接続先IPアドレス',
            'PORT': '接続先のPostgresQLのポート番号',
        }
    }
    ```

### マイグレーションの実行

1. Djangoのマイグレーションを実行し、DBのスキーマーを生成します。

    ```console
    $ python3 manage.py migrate
    ```
    コマンドを実行すると以下の様に,出力され、最後に`Applying`の文字列が続きます。何も出力されない場合は、DBへの接続できてない場合が想定されます。settings.pyの設定を見直してください。
    ```console
    System check identified some issues:
    
    WARNINGS:
    polls.Choice: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'Django.db.models.AutoField'.
    	HINT: Configure the DEFAULT_AUTO_FIELD setting or the PollsConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'Django.db.models.BigAutoField'.
    polls.Question: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'Django.db.models.AutoField'.
    	HINT: Configure the DEFAULT_AUTO_FIELD setting or the PollsConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'Django.db.models.BigAutoField'.
    Operations to perform:
      Apply all migrations: admin, auth, contenttypes, polls, sessions
    Running migrations:
      Applying contenttypes.0001_initial... OK
      Applying auth.0001_initial... OK
      Applying admin.0001_initial... OK
      Applying admin.0002_logentry_remove_auto_add... OK
      Applying admin.0003_logentry_add_action_flag_choices... OK
      Applying contenttypes.0002_remove_content_type_name... OK
      Applying auth.0002_alter_permission_name_max_length... OK
      Applying auth.0003_alter_user_email_max_length... OK
      Applying auth.0004_alter_user_username_opts... OK
      Applying auth.0005_alter_user_last_login_null... OK
      Applying auth.0006_require_contenttypes_0002... OK
      Applying auth.0007_alter_validators_add_error_messages... OK
      Applying auth.0008_alter_user_username_max_length... OK
      Applying auth.0009_alter_user_last_name_max_length... OK
      Applying auth.0010_alter_group_name_max_length... OK
      Applying auth.0011_update_proxy_permissions... OK
      Applying auth.0012_alter_user_first_name_max_length... OK
      Applying polls.0001_initial... OK
      Applying sessions.0001_initial... OK
    ```

### Djangoのスーパーユーザーの登録
Djangoのスーパーユーザーを登録します。ここで作成したユーザーは、「簡単な投票 (poll) アプリケーション」の管理画面にログインする時に使用します。

```console
$ python3 manage.py createsuperuser

Username (leave blank to use 'dai'): [ユーザー名を入力]
Email address: [メールアドレスを入力]
Password:  [パスワードを入力]
Password (again): [パスワードを入力] 
Superuser created successfully.
```

### デモデータの作成

これから起動する「簡単な投票 (poll) アプリケーション」のデモデータを作成します。
今回は、以下の様に、今日の天気を投票してもらうデータを作成します。

- 今日の天気
    - 質問: 今日の天気は？
    - 回答: 1.雨、2.曇り、3.晴れ

1.  Django の管理用のスクリプト manage.py を使ってShellを起動します。
    ```console
    $ python3 manage.py shell
    ```

1. Shellの中で以下の内容を実行して、DBにデータを登録します。
    ```console
    from polls.models import Choice, Question
    from Django.utils import timezone
    q = Question(question_text="今日の天気", pub_date=timezone.now())
    q.save()
    q.question_text = "今日の天気は？"
    q.save()
    q.choice_set.create(choice_text='雨', votes=0)
    q.choice_set.create(choice_text='曇り', votes=0)
    q.choice_set.create(choice_text='晴れ', votes=0)
    exit
    ```

これで、準備が終わりました。あとは、Webサーバを起動するだけです！

## 動作確認:Webサーバーの起動

1. Djangoのrunserver[テスト用簡易サーバ]を使って起動します。
    0.0.0.0:8000はどこのIPアドレスからでも、8000番ポートで受け付けるという意味です。

    ```console
    $ python3 manage.py runserver 0.0.0.0:8000
    ```
    正常に起動すると以下の様に `Starting development server・・・`とメッセージが出力されます。
    ```console
    System check identified 2 issues (0 silenced).
    October 02, 2022 - 03:05:12
    Django version 4.0.7, using settings 'mysite.settings'
    Starting development server at http://0.0.0.0:8000/
    Quit the server with CONTROL-C.
    ```

2. ブラウザで WebサーバのフローティングIPアドレスを使って次のURLで 「 http://(フローティングIPアドレス):8000/polls 」アクセスします。

問題がなければブラウザに以下の画像のように「 今日の天気は？ 」とメッセージが表示されるはずです。下記、赤矢印に沿って画面遷移を続けてと投票を継続すると投票数が増えていくことを確認できます。
![アプリケーションの画面遷移図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209170/693dec40-5bfd-1f74-1fe5-1cf11a59e049.png)


ブラウザアクセスをしている時のコンソールでは、以下の様にアクセスログが流れているのを確認できます。
```console
[03/Oct/2022 15:38:07] "GET /polls/ HTTP/1.1" 200 549
[03/Oct/2022 15:38:07] "GET /static/polls/style.css HTTP/1.1" 404 179
[03/Oct/2022 15:38:07] "GET /polls/1/ HTTP/1.1" 200 945
[03/Oct/2022 15:38:07] "POST /polls/1/vote/ HTTP/1.1" 302 0
[03/Oct/2022 15:38:07] "GET /polls/1/results/ HTTP/1.1" 200 568
[03/Oct/2022 15:38:07] "GET /polls/1/ HTTP/1.1" 200 945
[03/Oct/2022 15:38:08] "POST /polls/1/vote/ HTTP/1.1" 302 0
[03/Oct/2022 15:38:08] "GET /polls/1/results/ HTTP/1.1" 200 568
[03/Oct/2022 15:38:08] "GET /polls/1/ HTTP/1.1" 200 945
[03/Oct/2022 15:38:08] "POST /polls/1/vote/ HTTP/1.1" 302 0
[03/Oct/2022 15:38:08] "GET /polls/1/results/ HTTP/1.1" 200 568
[03/Oct/2022 15:38:08] "GET /polls/1/ HTTP/1.1" 200 945
[03/Oct/2022 15:38:09] "GET /polls/ HTTP/1.1" 200 549
```

# 5.最後に
これで、IBM Cloudの Virtual Private Cloud(VPC)の仮想インスタンスで Djangoのアプリケーションを動かすことができました。

次のステップとして、インフラ視点、アプリ視点での可視化・モニタリングに挑戦した時のQiita記事を作成しました。以下のQiita記事も併せてご一読ください。

- インフラ視点での可視化・モニタリング

https://qiita.com/daihiraoka/items/c6fd7cfa9e4a59b3038a

- アプリ視点での可視化・モニタリング

https://qiita.com/daihiraoka/items/2e3f66a61e61afa60324

※Djangoのアプリケーションを動かすWebサーバとして Djangoの runserverを使用しましたが、 uWSGI やGunicornでの動作は、別の投稿で書きたいと思います。