Otherカテゴリの記事

nginx + uwsgi + django on dockerでサイトを公開する

詳細

nginx + uwsgi + django on dockerでサイトを構築し,localhost上で公開するまでの解説です.

webサーバーの動き方に関して

djangoにはpython3 manage.py runserverで起動できる便利な開発サーバー機能が備わっています. しかし,本番環境ではこの機能は使わずに,apacheやnginxなどのwebサーバーを使用することが推奨されています. 今回は,nginxを使用してdjangoアプリケーションデプロイすることにします. また,環境の移植性を考慮して,dockerを使用したデプロイを行います.

nginx + uwsgi + djangoの具体的な処理の流れはこちらで解説されていますので,ぜひ参照してください. 非常に分かりやすく書かれており,これを読むだけである程度の設定はできてしまいます.

Setting up Django and your web server with uWSGI and nginx

簡単に図解すると,下の図のようにリクエストが流れていくことになります.

webクライアント -> nginx(80,443ポート) -> uwsgi(8000ポート) -> djangoバックエンド
  1. nginxがwebサーバーとして機能しwebクライアントからリクエストを受け取る.
  2. uwsgiがアプリケーションサーバーとしてnginxからのリクエストを受け取る.
  3. uwsgiがdjangoを動かし,結果のレスポンスを返す

この構成を実現するため,以下ではnginx, uwsgi, djangoの各項目の設定事項を見ていきます.

docker-compose.ymlの中身

まず,全体の見通しをよくするために,docker-compose.ymlの中身を紹介しておきます.

version: "3"

services:
  nginx:
    image: nginx:latest
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
      - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params
      - ./django-project/myapp/static:/static
      - ./django-project/myapp/media:/media
    ports:
      - 8000:80
    environment:
      - LANG=C.UTF-8
      - LANGUAGE=en_US
    depends_on:
      - django-project
      - mysql

  django-project:
    build: ./django-project
    restart: always
    command: bash /init.sh
    tty: true
    volumes:
      - ./django-project/app:/myapp
      - ./django-project/init.sh:/init.sh
    environment:
      - DEBUG=False
      - LOCALHOST=localhost
      - DEPLOYHOST=#デプロイ先のドメイン名
    depends_on:
      - mysql
      - redis-server

  mysql:
    # mysqlに関する記述
    ....

各ファイルのフォルダ構成

project/
|__django-project/
|    |__myapp/
|    |    |__myapp/
|    |    |__static/
|    |    |__media/
|    |    |__manage.py
|    |__Dockerfile
|__nginx
|    |__conf/nginx.conf
|    |__uwsgi_params
|__mysql
|__docker-compose.yml

nginxの設定内容

まずnginxの設定を行います. ホストOS側で./nginx/conf/nginx.confを作成し,に以下の項目を書き足していきます.

upstream django {
    ip_hash;
    server django-project:8000;
}

server {
    listen 80;
    charset utf-8;

    client_max_body_size 75M;

    location /static {
        alias /static;
    }

    location /media {
        alias /media;
    }

    location / {
        uwsgi_pass django;
        include /etc/nginx/uwsgi_params;
    }
}

server_tokens off;

上記のポイントは,以下です.

  1. upstreamでuwsgiサーバーを示すホスト名:ポート名を指定
  2. listen 80で80番ポートをリッスン
  3. location /static, location /mediaで静的ファイルの場所を指定.docker-compose.yml内でマウントした/static, /media内のファイルを探します.
  4. location /でuwsgiをリクエストを流す先として指定します.uwsgi_paramsここからコピペで問題ありません.

uwsgiの設定内容

uwsgiはdjangoと同じdockerイメージの中に入れます../django-project/Dockerfile内に以下の内容を書き加えます.

FROM python:3.8-slim-buster

# djangoなど必要なライブラリをインストール
COPY ./requirements.txt /requirements.txt

# RUN source /venv/bin/activate
RUN python3.8 -m pip install --upgrade pip
RUN python3.8 -m pip install -r requirements.txt
RUN python3.8 -m pip install uwsgi # uwsgiを入れること

これでdjangoをuwsgiで動かす準備はできました. 次にuwsgiでリクエストを待ち受けるために,コンテナ起動時に実行するシェルスクリプトを書いていきます.

#!/bin/bash

cd /myapp;
bash -c "python3 manage.py makemigrations";
bash -c "python3 manage.py migrate";
bash -c "python3 manage.py collectstatic --noinput"
bash -c "uwsgi --socket :8000 --module myapp.wsgi --py-autoreload 1s";

uwsgi(8000番ポート)で待ち受けることを最後の行で書き加えています.

djangoの設定内容

最後に,djangoの設定を行なっていきます. 変更が必要なのは,/myapp/myapp/settings.pyのみです.

  1. デバッグモードのON/OFFをdocker-compose.ymlから指定できるようにする. docker-compose.ymlenvironmentDEBUG=True/Falseと指定することでデバッグモードが変えられるように,環境変数の読み込み部分を追記します. また,djangoはDEBUG=Falseの際には外部からの通信を受け付けないので,ALLOWED_HOSTSにも環境変数の読み込みを追加します. DEPLOYHOSTの部分にデプロイするサーバーのホスト名を書きます.
# settings.py
# SECURITY WARNING: don't run with debug turned on in production!
is_debug_mode = os.environ.get("DEBUG")
DEBUG = (is_debug_mode == "True")
ALLOWED_HOSTS = [os.environ.get("LOCALHOST"), os.environ.get("DEPLOYHOST")]
  1. nginxが静的ファイルをサーブできるように,staticファイルのルートを指定する. STATIC_ROOTMEDIA_ROOTを指定することで,DEBUG=Falseの際に静的ファイルはnginxが直接サーブするようにできます.
# settings.py
STATIC_URL = '/static/'
STATICFILE_DIRS = [
    os.path.join(BASE_DIR, 'userctrl/static/'),
    os.path.join(BASE_DIR, 'contest_manage/static/'),
    os.path.join(BASE_DIR, 'ace-builds')
]
STATIC_ROOT = os.path.join(BASE_DIR, "static")

MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"

このサイト(www.teenytinybot.dev)の構成に関して

詳細

最近更新しておりませんでした、IMAXおじさんです。 この頃本サイトに色々機能を足していたので、そろそろ全体をまとめた構成図を作ろうと思い立ちました。

1. コンテナの構成について

本サイトはさくらVPS上にデプロイしています。 当初はUbuntu Server上のネイティブで動かしていたのですが、お仕事でDockerをよく使うこともあり途中からDockerでのデプロイに切り替えました。

各コンポーネントの基本的な構成は以下となります。

  • OS:Ubuntu 18.04
  • 仮想化:Docker
  • リバースプロキシ:Nginx 1.21.6
  • アプリバックエンド:Python3, Django 3.0
  • DB:MySQL 5.7
  • 可視化ツール:Grafana 8.3.4
  • ログ監視:Splunk 8.2.5
  • ログ転送エージェント:UniversalForwarder(@Nginxコンテナ)

また、コンテナ間の関係とデータフローは以下の通りです。

2. Djangoで提供するアプリに関して

また、Djangoでは下記のアプリを作成して公開しています。

  • 質問箱

    • Djangoで初めて作ったWEBアプリです。
    • 中の人に匿名で質問が送れます。line-bot-sdkと連携しており、質問がくるとBotからLINEで中の人に通知が来ます。
    • (最近質問が来てなくてなんか寂しいので質問募集中です)
  • 自宅の温湿度監視

    • DjangoでRESTAPIを実装し、自宅のM5StackからAPI経由で気温・湿度・気圧をサーバに上げています。
    • Grafanaで監視用ダッシュボードを作成し、公開しています。
  • 技術ブログ

    • 技術系の記事を書いていくブログページです。
    • Markdown形式が使えるよう、Djangoプラグインであるmdeditorを入れています。
    • また、コードはシンタックスハイライトが行われるよう、aceというライブラリを入れています。
    • ↓こんな感じ
  • ログ監視

    • また、自明に公開はしていませんが、Nginxのログを見ると公開サーバへの攻撃と思しきアクセスがかなり多いので、監視ツールとしてSplunkを入れています。
    • 法人利用は有償ですが、個人利用であれば制限付きで一定期間は無償利用可能なようです。

3. 今後の予定

今後は以下のことに取り組みたいな〜と考えています。

  • AWSへのデプロイ(AWSできるようになりたい...)
  • elasticsearchとsplunkの比較
  • 機械学習がバックエンドで動くWEBアプリ
  • Vue.js+Django RESTAPIで動く構成に(モダンなWEBの構成にしたい...)

ソートアルゴリズムを真面目に実装する(応用情報技術者試験)

詳細

タイトルの通り、ソートアルゴリズムを真面目に実装します。 言語はPython、実装するソートアルゴリズムはクイックソート・ヒープソート・マージソートです。

ソートは基本的に標準ライブラリのsort関数に投げがちなので、内部がどうなっているかを気にすることがないかと思います。 今回は応用情報技術者試験の対策も兼ねて、真面目にソートを実装します。 なお、めんどくさいので昇順ソートしか実装しません()

参考文献

クイックソート

クイックソートは、分割統治の考え方でソートを行うアルゴリズムです。

下記のような配列を考えたとき、その配列に対してpivotと呼ばれる軸を設定し、そのpivot未満の配列とpivot以上の配列に分割していきます。 これを再帰的に繰り返すことで、整列済みの配列を得るアルゴリズムです。また、安定なソートではありません。

計算量

平均計算量は、O(NlogN)となります。 具体的な計算量の導出はこちらが参考になりました。

ざっくり言えば、適切にpivotを選んだ場合上記の画像で示す通りpivotによる分割と結合はそれぞれ回数H = logNで抑えることができます。 それぞれの高さに関して、N回比較が発生するので、計算量はO(NlogN)であるとわかります。

最悪計算量

ただし、予めソートされたデータに対して最大値/最小値をpivotとして取り続けた場合は最悪計算量がO(N^2)となります。

実装

A = list(map(int, input().split()))
l = 0
r = len(A) - 1

def quick_sort(tgt):
    tgt_len = len(tgt)
    if len(tgt) <= 1:
        return tgt

    pivot = tgt[0]
    left = []
    right = []

    for i in range(1, tgt_len):
        if tgt[i] <= pivot:
            left.append(tgt[i])
        else:
            right.append(tgt[i])

    return quick_sort(left) + [pivot] + quick_sort(right)

res = quick_sort(A)
print(res)

ヒープソート

ヒープソートはヒープの再構成がO(logN)でできることを利用したソートアルゴリズムです。 ヒープの根が常に配列の最小値となっているので、ヒープの根をpop→ヒープを再構築...を繰り返し、popした順に配列を作ればO(NlogN)でソートできます。

ヒープに突っ込むため、安定なソートではないですが、常に計算量がO(NlogN)となります。

実装

ヒープを自力で実装し、ヒープからどんどんpopしていく方針を取ります。

class Heap:
    def __init__(self, arr:list):
        self.arr = []

        for item in arr:
            self.push(item)

    def push(self, x):
        self.arr.append(x)
        child = len(self.arr) - 1

        while child > 0:
            parent = (child - 1)//2

            if self.arr[parent] < x:
                break
            else:
                self.arr[child] = self.arr[parent]
                child = parent

        self.arr[child] = x

    def root(self):
        return self.arr[0] if self.arr else None


    def pop(self):
        if not self.arr:
            return None

        root = self.arr[0] 
        self.arr[0] = self.arr[-1]
        self.arr.pop()

        parent = 0
        smallest_index = parent
        heap_size = len(self.arr)

        while 2*parent+1 < heap_size:
            l = 2*parent+1  # 左の子
            r = l+1         # 右の子

            # 自分、左の子、右の子の中で、最小のものを選び取る
            if (r < heap_size) and (self.arr[parent] >= self.arr[r]):
                smallest_index = r

            if (l < heap_size) and (self.arr[smallest_index] > self.arr[l]):
                smallest_index = l

            # 自分が最小になった時点で、ヒープ化されたのでbreak
            if smallest_index == parent:
                break

            # 親と子を入れ替える
            self.arr[parent], self.arr[smallest_index] = self.arr[smallest_index], self.arr[parent]
            parent = smallest_index

        return root

def heap_sort(arr):
    heap = Heap(arr)
    N = len(arr)

    print(heap.arr)

    result = []
    for _ in range(N):
        result.append(heap.pop())
        print(heap.arr)

    return result

if __name__ == "__main__":
    sample = [1, 1, 2, -1, 100, 92]

    result = heap_sort(sample)

    print(result)  # -> [-1, 1, 1, 2, 92, 100]

マージソート

マージソートも、分割統治法の考え方を用いてソートを行うアルゴリズムです。 下記画像のように、まず要素が一つになるまで分割を繰り返します。

その後、分割した配列を2つずつマージしていきます。 このとき、分割した配列を結合する際に、昇順になるように入れていきます。各配列はソート済みであるので、それぞれ先頭のindexから順に比較していけば良いことになります。

計算量

こちらの説明がとてもわかりやすいです。

上の図で示した通り、分割とマージのプロセスはそれぞれH = logNで抑えられます。 マージの際には、それぞれの分割のレイヤーでN回比較が走るので、結果的に計算量はO(2NlogN) = O(NlogN)となります。

実装

再帰的な実装を行います。(実は分割のところがO(N)かかっているのではないかという説があります)

def merge_sort(data):

    if len(data) <= 1: return data

    mid = len(data) // 2

    # 再帰的に分割していく
    left = merge_sort(data[:mid:])
    right = merge_sort(data[mid::])

    return merge(left, right)


def merge(left, right):
    result = []
    i, j = 0, 0

    # 配列の先頭が最小値になっているので、毎回比べながら追加していく
    while (i < len(left)) and (j < len(right)):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    # add the remainging part
    if i < len(left):
        result.extend(left[i:])

    if j < len(right):
        result.extend(right[j:])

    return result

if __name__ == "__main__":
    array = list(map(int, input().split()))

    res = merge_sort(array)

    print(res)

【資格】応用情報技術者試験に合格しました

詳細

応用情報に合格しました。午前午後ともに8割程度取れていました。次はNWスペシャリスト頑張ります。

【資格】ネットワークスペシャリスト試験に合格しました!

詳細

合格発表から時間が空いてしまいました。2023年4月にネットワークスペシャリスト試験を受験し、合格していました。

点数は午前2:8割、午後1:6割、午後2:8割といった具合で、落ちるなら午前1かなぁと思っていたところなんとか耐えていました。

次はセキスペとAWS SAAを受けようと思います。

About

IMAXおじさんが(主に)技術系記事を備忘録として残していくブログです.

Category

  1. Tech
  2. Daily
  3. Job
  4. Other