[aws ec2] Ubuntu20.04 x Apache2 x Django3.0 x MySQL8系 環境構築

結論から言うと、ec2 x Djangoでapache2 or Nginxを使う場合、仮想環境を作らないと、Djangoのパスが通らずに、「ModuleNotFoundError: No module named ‘django’」とエラーになる。開発環境では、manage.py runserverで確認すれば良かったが、ec2にデプロイしてapache2もしくはNginxを通す場合は、仮想環境の構築が必須。
ec2でapache2もしくはNginxを構築する前に、uWSGIの基礎を学んでから構築した方が、理解が深まるし、トラブルシューティングしやすくなる。
それを省略したので、結局2日かかった orz…

以下が構築手順。Ubuntu20.04のインスタンスが出来ている状態から始める。

### 1.インスタンスログイン
$ ssh ubuntu@${public ip} -i ~/.ssh/*.pem
$ sudo apt update
$ sudo apt upgrade
$ sudo apt install python3-pip

### 2.MySQL8系インストールとdjango用のdb作成
$ sudo apt install mysql-client-core-8.0
$ sudo apt-get update
$ sudo apt install mysql-server
$ sudo service mysql start
$ sudo mysql_secure_installation
$ sudo mysql -u root -p
mysql>set global validate_password.length=6;
mysql>set global validate_password.policy=LOW;
mysql>CREATE USER ‘admin’@’%’ IDENTIFIED BY ‘hogehoge’;
mysql>GRANT ALL PRIVILEGES ON *.* TO ‘admin’@’%’ WITH GRANT OPTION;
mysql>FLUSH PRIVILEGES;
mysql>create database hoge;

### 3.apacheインストール
$ sudo apt update
$ sudo apt install apache2
$ sudo ufw app list
$ sudo ufw allow ‘Apache Full’
$ sudo ufw status
$ sudo systemctl status apache2

### 4.git clone
// 所有権
$ sudo chown ubuntu /home
$ git clone https://github.com/hoge/hoge.git
$ cd hoge

#### 5.django商用設定
settings.py

DEBUG = False
ALLOWED_HOSTS = ['*']  #もしくはEC2のIP
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
migration

// migration
$ python3 manage.py makemigrations hoges
$ python3 manage.py migrate

### 6. 仮想環境構築(!!重要!!)
$ sudo apt install -y python3-wheel python3-venv python3-dev
$ python3 -m venv env
$ source env/bin/activate
$ pip install wheel
// 各種ライブラリインストール 省略
$ apt-get install apache2-dev
$ sudo pip3 install mod_wsgi
$ mod_wsgi-express module-config
LoadModule wsgi_module “/usr/local/lib/python3.8/dist-packages/mod_wsgi/server/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so”
WSGIPythonHome “/usr”
$ python3 manage.py collectstatic
$ deactivate

### 7. Apache設定
$ sudo vi /etc/apache2/sites-available/django.conf

LoadModule wsgi_module /usr/local/lib/python3.8/dist-packages/mod_wsgi/server/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so

WSGIPythonHome /usr
WSGIScriptAlias / /home/ubuntu/hoge/hoge/wsgi.py
WSGIPythonPath /home/ubuntu/hoge:/home/ubuntu/hoge/env/lib/python3.8/site-packages

<Directory /home/ubuntu/hoge/hoge>
  <Files wsgi.py>
    Require all granted
  </Files>
</Directory>

Alias /static/ /home/ubuntu/hoge/hoge/
<Directory /home/ubuntu/hoge/static>
  Require all granted
</Directory>

$ sudo a2dissite 000-default
$ sudo a2ensite django
$ sudo systemctl restart apache2
$ sudo systemctl enable apache2

### 8.挙動確認
EC2のpublic IPを叩く

### 9.AMI作成
– インスタンスのバックアップ

お疲れ様でした。

ちなみにこれ、仮想環境構築をすっ飛ばしてインスタンス作成を5〜6回ぐらいやり直してapacheのエラーログ見て悶絶してた。まあ、ここを乗り切ればハードルは一気に下がりますね。

[uWSGI] DjangoをUbuntu+Nginx+uWSGIの構成で動かしたい

### uWSGIとは?
-WSGIは仕様で、uWSGIは既存のWebサーバに機能を追加
-WebサーバにNginx, WSGIコンテナにuWSGI(Nginxとは別プロセス)
-Apache2, Nginx, cherokee, lighttpdに対応
-gunicornというWSGIも有名
-Apacheは同時接続数が極端に多くなると対応できないが、Nginxはレスポンスが早い

### Django構築
$ django-admin startproject testSite
$ python3 manage.py startapp testapp

settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'testapp',
]
ALLOWED_HOSTS = ['*']

views.py

from django.http import HttpResponse
# Create your views here.

def hello(request):
	return HttpResponse("hello, Nginx!")

urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.hello, name='hello'),
]
urlpatterns = [
    path('admin/', admin.site.urls),
    path('testapp/', include('testapp.urls')),
]

### UbuntuにNginx
$ sudo apt update
$ apt show nginx
Package: nginx
Version: 1.18.0-0ubuntu1
$ sudo apt install nginx
$ nginx -v

### djangoでuWSGI
$ python3 manage.py runserver 192.168.33.10:8000

$ uwsgi –http :8000 –module testSite.wsgi

testApp/uwsgi_params

uwsgi_param  QUERY_STRING       $query_string;
uwsgi_param  REQUEST_METHOD     $request_method;
uwsgi_param  CONTENT_TYPE       $content_type;
uwsgi_param  CONTENT_LENGTH     $content_length;
 
uwsgi_param  REQUEST_URI        $request_uri;
uwsgi_param  PATH_INFO          $document_uri;
uwsgi_param  DOCUMENT_ROOT      $document_root;
uwsgi_param  SERVER_PROTOCOL    $server_protocol;
uwsgi_param  REQUEST_SCHEME     $scheme;
uwsgi_param  HTTPS              $https if_not_empty;
 
uwsgi_param  REMOTE_ADDR        $remote_addr;
uwsgi_param  REMOTE_PORT        $remote_port;
uwsgi_param  SERVER_PORT        $server_port;
uwsgi_param  SERVER_NAME        $server_name;

testApp/testSite_nginx.conf

upstream django {
	server 192.168.33.10:8001;
}

server {
	listen 8000;
	server_name 192.168.33.10;
	charset utf-8;

	location /static {
		alias /home/vagrant/other/testSite/static;
	}

	location / {
		uwsgi_pass django;
		include /home/vagrant/other/testSite/testSite/uwsgi_params;
	}
}

$ sudo ln -s /home/vagrant/other/testSite/testSite/testSite_nginx.conf /etc/nginx/sites-enabled/

setting.py

STATIC_ROOT = os.path.join(BASE_DIR, "static/")

$ python3 manage.py collectstatic

$ sudo nginx
failed (Result: exit-code)

$ sudo lsof -i:80
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 75758 root 6u IPv4 87712 0t0 TCP *:http (LISTEN)
nginx 75758 root 7u IPv6 87713 0t0 TCP *:http (LISTEN)
nginx 75759 www-data 6u IPv4 87712 0t0 TCP *:http (LISTEN)
nginx 75759 www-data 7u IPv6 87713 0t0 TCP *:http (LISTEN)
nginx 75760 www-data 6u IPv4 87712 0t0 TCP *:http (LISTEN)
nginx 75760 www-data 7u IPv6 87713 0t0 TCP *:http (LISTEN)

$ uwsgi –socket :8001 –module testSite.wsgi buffer-size=32768

うーん、ちょっとよくわからんな。。

[aws ec2]ubuntu20.04にningxを入れて起動させたい

1. SSHログイン
ssh ubuntu@* -i ~/.ssh/*.pem

2. apt update
$ sudo apt update
$ sudo apt upgrade

3. mysql
$ sudo apt install mysql-client-core-8.0
$ sudo apt-get update
$ sudo apt install mysql-server
$ mysqld –version
$ sudo service mysql start
$ sudo mysql_secure_installation
$ sudo mysql -u root -p
mysql> create database hanbai;

4. Nginx install
$ sudo apt install nginx

5. 仮想環境作成
$ sudo apt install python3-venv python3-pip python3-dev
$ sudo chown ubuntu /home
$ git clone https://github.com/*/*.git
$ cd hanbai
$ ls
README.md db.sqlite3 hanbai manage.py sales
$ python3 -m venv vdjango

$ . vdjango/bin/activate
(vdjango)$ pip3 install Django==3.0.4
(vdjango)$ pip3 install uwsgi

$ sudo ufw allow 80

6. Nginx設定
$ cd /etc/nginx/conf.d
$ sudo vi project.conf

server{
    listen 80;
    server_name ${publicIp};

    location / {
        proxy_pass http://127.0.0.1:8000;
    }
}

7. iniファイル
$ ls
README.md db.sqlite3 hanbai manage.py sales vdjango
$ vim django.ini

[uwsgi]
module          =  project.wsgi:application
master          =  true
pidfile         =  django.uwsgi.pid
enable-threads  = true
http            =  127.0.0.1:8000
processes       =  5
harakiri        =  50
max-requests    =  5000
vacuum          =  true
home            =  vdjango
daemonize       =  django.uwsgi.log

8. library install & migration
// 省略

9. Nginxとuwsgi起動
$ sudo service nginx start
$ sudo apt-get install -y uwsgi
$ sudo apt install uwsgi-plugin-python3

うーん、
$ sudo python3 manage.py runserver 0.0.0.0:8000 で8000ポート開けても動くんだけど、なんか違うんだよな。。

[aws ec2]Apache2を入れるが、Internal Server Error

### apache2 インストール
$ sudo apt update
$ sudo apt install apache2
$ sudo ufw app list
$ sudo ufw allow ‘Apache Full’
$ sudo ufw status
$ sudo systemctl status apache2
$ hostname -I

### mod_wsgi
$ apt-get install apache2-dev
$ pip3 install mod_wsgi

### settings.py

ALLOWED_HOSTS = ['*'] 

$ mod_wsgi-express module-config
LoadModule wsgi_module “/home/ubuntu/.local/lib/python3.8/site-packages/mod_wsgi/server/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so”
WSGIPythonHome “/usr”

### migrate
$ python3 manage.py makemigrations sales
$ python3 manage.py migrate

### apache設定
sudo vi /etc/apache2/sites-available/000-default.conf

LoadModule wsgi_module "/usr/lib/apache2/modules/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so"
WSGIScriptAlias / /var/www/hanbai/hanbai/wsgi.py
WSGIPythonHome "/usr"
WSGIPythonPath "/var/www/hanbai"

<VirtualHost *:80>
        ServerAdmin webmaster@localhost
        # DocumentRoot /var/www/hanbai

        # WSGIScriptAlias / /var/www/hanbai/hanbai/wsgi.py
        # WSGIPythonPath /var/www/hanbai/        
        <Directory /var/www/hanbai/hanbai/>
            <Files wsgi.py>
                Order deny,allow
                AllowOverride None
                require all granted
            </Files>
        </Directory>
</VirtualHost>

$ sudo /etc/init.d/apache2 restart

$ /var/log/apache2/error.log
[client 59.126.236.35:46554] from django.core.wsgi import get_wsgi_application
[Wed Oct 28 23:46:17.546723 2020] [wsgi:error] [pid 10444:tid 139970643724032] [client 59.126.236.35:46554] ModuleNotFoundError: No module named ‘django’

$ pip3 freeze | grep wsgi
mod-wsgi==4.7.1

WSGIPythonPath /var/www/hanbai:/home/ubuntu/.local/lib/python3.8/site-packages

何故だ。。Djangoをインストールした場所が悪かった?
もう一回やるか。。

[Django3.0]send_mailでメール送信

ログイン時にユーザ名を忘れたユーザがいたら、メールフォームにemailアドレスを入力してもらって、auth_usersにメールアドレスがあれば、そのメールアドレスにユーザ名を送信したい。パスワードは送信しない。

ログイン画面

メール入力画面

公式ドキュメント: send email

SMTPは開発環境なので、mailtrapを使います。
EMAIL_BACKEND = ‘django.core.mail.backends.console.EmailBackend’とすれば、コマンドラインに表示されます。

settings.py

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'

EMAIL_HOST = 'smtp.mailtrap.io'
EMAIL_PORT = 587
EMAIL_HOST_USER = '******'
EMAIL_HOST_PASSWORD = '******'
EMAIL_USE_TLS = True

### views.py
auth_userテーブルから、一致するemailがあるか確認し、あれば、そのメールアドレスにusernameを送信する。
送信はsend_mailを使用する。
本文の改行は”\n”

from django.core.mail import send_mail

def username_send(request):
	if(request.method == 'POST'):
		data = User.objects.filter(email=request.POST['email']).first()
		if data is not None:
			subject = "【Hanai】ユーザ名のお知らせ"
			message = 'お問い合わせありがとうございます。\nログインに必要なユーザ名をお知らせします。\n\nユーザ名:' + data.username + '\n\n\n※本メールは販売管理システムの\n送信専用のメールアドレスから自動送信されています。\nご返信いただいても返信できませんのでご了承ください。\n本サービスに心上がりがない場合など、お問い合わせください。\n\nHanbai'
			from_email = 'master@hanbai.com'
			recipient_list = [ data.email ]
			send_mail(subject, message, from_email, recipient_list) 
		else:
			params = {
				'message': 'メールアドレスの登録がありません。'
			}
			return render(request, 'sales/username_forget.html', params)
	return render(request, 'sales/username_send.html')

メール受信

OK! 大分来た
さあ、この調子で続けてラズパイとopenCVやります。

[Django3.0]CSVダウンロード機能を実装する

顧客一覧をメニューからダウンロードできるようにする。
ローカルで実装したように、csvをimportする。
HttpResponseは’minetype’ではなく’content_type’にする
csvの日本語化はcontent_type=’text/csv;charset=utf_8_sig’とする。
あとは、forループで回すだけ。

### views.py

import csv

def csv_export(request):
	filename='clients.csv'
	response = HttpResponse(content_type='text/csv;charset=utf_8_sig')
	response['Content-Disposition'] = "attachment;  filename='{}'; filename*=UTF-8''{}".format(filename, filename)

	w = csv.writer(response)
	w.writerow(['会社名','会社名カナ','事業所名','部署名','役職','担当者名','メールアドレス','住所','電話','FAX','役職','代表者','備考'])
	
	data = Clients.objects.all()
	for item in data:
		w.writerow([item.name, item.name_kana, item.office, item.department, item.position, item.charge, item.charge_mail, item.zipcode + ' ' + item.prefecture + item.address, item.tel, item.fax, item.position_top, item.name_top, item.remark])
	return response

OK、あとはいよいよAuth機能の実装。
メール機能も実装したいが、まずはAuthか。

[Django3.0]商品登録時にレコードidを付番したqrコードを生成したい

### template側
widget_tweaksでforms.pyからformを作っていきます。

### views.py
mysqlに生成した直後に、生成したばかりのレコードidを*.objects.order_by(“id”).last()で取得して、qrコードのurlに付番する。

def stock_complete(request):
	if(request.method == 'POST'):
		data = StocksForm(request.POST)
		if data.is_valid():
			// データ作成
			category_id = request.POST['category']
			name = request.POST['name']
			unit = request.POST['unit']
			price = request.POST['price']
			total = request.POST['total']
			remark = request.POST['remark']
			stock = Stocks(category_id=category_id, name=name, unit=unit, price=price, total=total, remark=remark)
			stock.save()

			// qrコード作成
			new_data = Stocks.objects.order_by("id").last()
			qr = qrcode.QRCode()
			qr.add_data('http://192.168.33.10:8000/qrcode/' + str(new_data.id))
			qr.make()
			img = qr.make_image()
			img.save('./sales/media/img/qrcode/'+ str(new_data.id) +'.png')

			// 完了画面に遷移
			return render(request, 'sales/stock_complete.html')
		else:
			params = {
				'form': StocksForm(request.POST),
			}
			return render(request, 'sales/stock_input.html', params)
	return redirect('/stock/input')

### mysql側

mysql> select * from sales_stocks;
+—-+——————————+——+——-+——–+——————-+—————————-+—————————-+————-+
| id | name | unit | price | total | remark | created_at | updated_at | category_id |
+—-+——————————+——+——-+——–+——————-+—————————-+—————————-+————-+
| 4 | Foot Massage Machine Shiatsu | 5 | 50880 | 254400 | US MakertPlace社 | 2020-10-09 18:44:48.012892 | 2020-10-09 18:44:48.012925 | 5 |
+—-+——————————+——+——-+——–+——————-+—————————-+—————————-+————-+
1 row in set (0.00 sec)

### qrコード
id4でqrコードが生成されています。

urlもきちんと反映されています。

商品登録時にレコードidを取得するアイディアが思いつかず、商品詳細画面表示に初めてqrコードを生成するべきかずっと悩んでいましたが、order_by(“id”).last()を思いついたら割と簡単にできた。

続けて登録完了画面にqrコードを表示させて、ダウンロードできるように少し改良しました。

[Django3.0]メディアファイルの取り扱い

プロジェクトルートにmediaフォルダを作成します。

settings.pyでmediaルートを設定します。os.path.joinの第二引数はアプリケーションのフォルダ、第三引数はフォルダ名(media)です。

settings.py

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

STATIC_ROOT = os.path.join(BASE_DIR, 'sales', 'static')
STATIC_URL = '/static/'

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
	// 省略
]


urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

mediaルートに画像を置きます。

templateで呼び出してみましょう。

<img src="/media/img/qrcode.png" width=100 height=100>

うまく表示されています。

さて、次はどこでqrコードを生成するかです。
商品生成時にidで作りたいが、idはmysql側で付与されるから、
*save() とした後に以下のようにすれば良いのかな。
*.objects.order_by(“id”).last()

[Django3.0]ページネーションを実装する

urls.py

urlpatterns = [
	// 省略
	path('client/<int:num>', views.client, name='client'),
        // 省略
]

views.py

from django.core.paginator import Paginator

def client(request, num=1):
	data = Clients.objects.all()
	page = Paginator(data, 3)
	params = {
		'data' : page.get_page(num)
	}
	return render(request, 'sales/client.html', params)

templateにページネーションを実装します。
*.html

<div class="list">
				<ul class="pagination justify-content-end">
					{% if data.has_previous %}
					<li class="page-item">
						<a class="page-link" href="/client/1">&laquo; first</a>
					</li>
					<li class="page-item">
						<a class="page-link" href="/client/{{data.previous_page_number}}">&laquo; prev</a>
					</li>
					{% else %}
					<li class="page-item">
						<a class="page-link">&laquo; first</a>
					</li>
					<li class="page-item">
						<a class="page-link">&laquo; prev</a>
					</li>
					{% endif %}
					<li class="page-item">
						<a class="page-link">{{data.number}}/{{data.paginator.num_pages}}</a>
					</li>
					{% if data.has_next %}
					<li class="page-item">
						<a class="page-link" href="/client/{{data.next_page_number}}">next &raquo;</a>
					</li>
					<li class="page-item">
						<a class="page-link" href="/client/{{data.paginator.num_pages}}">last &raquo;</a>
					</li>
					{% else %}
					<li class="page-item">
						<a class="page-link">next &raquo;</a>
					</li>
					<li class="page-item">
						<a class="page-link">last &raquo;</a>
					</li>
					{% endif %}
				</ul>
			</div>

あとは、client/にリクエストがあった時に、Page not foundとレスポンスを返してしまうので、urls patternでclient/ではなく、client/で来た時に、client/1にリダイレクトさせたいな。

[Django]ユニットテスト

– from django.test import TestCaseでTestCaseを継承
– メソッドはtest_* にしなければならない
/sns/tests.py

from django.test import TestCase

class SnsTests(TestCase):

	def test_check(self):
		x = True
		self.assertTrue(x)
		y = 100
		self.assertGreater(y, 0)
		arr = [10, 20, 30]
		self.assertIn(20, arr)
		nn = None
		self.assertIsNone(nn)

$ python manage.py test sns
Creating test database for alias ‘default’…
System check identified no issues (0 silenced).
.
———————————————————————-
Ran 1 test in 0.001s

OK
Destroying test database for alias ‘default’…

### チェックメソッド
– assertTrue, assertFalse, assertIsl, assertIsNot, assertEqual, assertNoEqual, assertGreater, assertGreaterEqual, assertLess, assertLessEqual, assertIsNone, assertIsNotNone, assertIsIn, assertNotIn

### データベースのチェック

from django.test import TestCase

from django.contrib.auth.models import User
from .models import Message

class SnsTests(TestCase):

	def test_check(self):
		usr = User.object.first()
		self.assertIsNotNone(usr)
		msg = Message.objects.first()
		self.assertIsNotNone(msg)

– テスト用のデータベースを都度作って使用している

from django.test import TestCase

from django.contrib.auth.models import User
from .models import Group, Message

class SnsTests(TestCase):

	@classmethod
	def setUpClass(cls):
		super().setUpClass()
		(usr, grp) = cls.create_user_and_group()
		cls.create_message(usr, grp)

	@classmethod
	def create_user_and_group(cls):
		# Create public user & public group
		User(username="public", password="public", is_staff=False, is_active=True).save()
		pb_usr = User.objects.filter(username='public').first()
		Group(title='public', owner_id=pb_usr.id).save()
		pb_grp = Group.objects.filter(title='public').first()

		# Create test user
		User(username="test", password="test", is_staff=True, is_active=True).save()
		usr = User.objects.filter(username='test').first()

		return (usr, pb_grp)

	@classmethod
	def create_message(cls, usr, grp):
		# Create test massage
		Message(content='this is test message.', owner_id=usr.id, group_id=grp.id).save()
		Message(content='test', owner_id=usr.id, group_id=grp.id).save()
		Message(content="ok", owner_id=usr.id, group_id=grp.id).save()
		Message(content="ng", owner_id=usr.id, group_id=grp.id).save()
		Message(content='finish', owner_id=usr.id, group_id=grp.id).save()

	def test_check(self):
		usr = User.objects.first()
		self.assertIsNotNone(usr)
		msg = Message.objects.first()
		self.assertIsNotNone(msg)