部署Python Web App

Linux端(服务器)Ubuntu18.04.3:

安装:

ssh服务;
Nginx (web服务器);
Supervisor(python3)监控服务,管理进程;
Mysql;
jinjia2;
aiomysql;
aiohttp;
python3;

1
2
3
$ sudo apt-get install nginx supervisor python3 mysql-server

$ sudo pip3 install jinja2 aiomysql aiohttp

初始化Mysql数据库:

进入mysql命令行,使用source + sql文件路径执行脚本创建表;

定义目录结构:

/
+- srv/
+- awesome/ <– Web App根目录
+- www/ <– 存放Python源码
| +- static/ <– 存放静态资源文件
+- log/ <– 存放log

使用虚拟环境:

安装:

1
sudo apt install virtualenv

创建:

1
virtualenv .env --python=python3

激活:

1
source .env/bin/activate

安装supervisor

生成配置文件:

1
echo_supervisord_conf > supervisord.conf

配置:

1
2
awesome.conf(位于etc/supervisor/conf.d):
command = /home/such/.env/bin/python3 /srv/awesome/www/app.py runserver

注意:supervisor不要添加多个command,如果其中一个command出错,而另一个正常,则整体还是可以运行的!!
即:command = 。。。
command =。。。

编写一个Supervisor的配置文件awesome.conf,存放到/etc/supervisor/conf.d/目录下:

1
2
3
4
5
6
7
8
9
[program:awesome]
command = /home/such/.env/bin/python3 /srv/awesome/www/app.py runserver
directory = /srv/awesome/www
user = such
startsecs = 3
redirect_stderr = true
stdout_logfile_maxbytes = 50MB
stdout_logfile_backups = 10
stdout_logfile = /srv/awesome/log/app.log

配置文件通过[program:awesome]指定服务名为awesome,command指定启动app.py。

1
2
3
4
5
执行配置更新:
$sudo supervisorctl reread
$sudo supervisorctl update
$sudo supervisorctl reload
$sudo supervisorctl start awesome

nginx配置:

/etc/nginx/sites-available/下有个default文件,添加新配置awesome并不会生效。因此直接修改default文件进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 80; # 监听80端口
root /srv/awesome/www;
access_log /srv/awesome/log/access_log;
error_log /srv/awesome/log/error_log;
# server_name awesome.liaoxuefeng.com; # 配置域名
# 处理静态文件/favicon.ico:
location /favicon.ico {
root /srv/awesome/www;
}
# 处理静态资源:
location ~ ^\/static\/.*$ {
root /srv/awesome/www;
}
# 动态请求转发到9000端口:
location / {
proxy_pass http://127.0.0.1:9000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

然后在/etc/nginx/sites-enabled/目录下创建软链接:

1
2
3
4
5
$ pwd
/etc/nginx/sites-enabled
$ sudo ln -s /etc/nginx/sites-available/awesome
让Nginx重新加载配置文件:
$ sudo /etc/init.d/nginx reload

获取ip:

1
ifconfig -a

未找到dos2unix报错:

1.安装tofrodos

sudo apt-get install tofrodos
实际上它安装了两个工具:todos(相当于unix2dos),和fromdos(相当于dos2unix)

2.做一些优化

由于习惯了unix2dos和dos2unix的命令,可以把上面安装的两个工具链接成unix2dos 和dos2unix,或者仅仅是起个别名,并放在启动脚本里。
步骤:

1
2
ln -s /usr/bin/todos /usr/bin/unix2dos 
ln -s /usr/bin/fromdos /usr/bin/dos2unix

开发机:

Fabric(python2.7)自动化部署工具

若使用windows,则安装Fabric3(python3)

1
pip install fabric3

fabfile.py: 放在awesome-python-webapp,与www同级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# -*- coding: utf-8 -*-
'''
Deployment toolkit in windows envirement.
'''
import os, re, tarfile
from datetime import datetime
from fabric.api import *
env.user = 'root'
env.sudo_user = 'root'
# env.hosts = ['123.123.123.123']
env.host_string = '47.106.33.242' # 改成你的服务器ip
db_user = 'Blog'
db_password = '1234567'
_TAR_FILE = 'dist-awesome.tar.gz'
_REMOTE_TMP_TAR = '/tmp/%s' % _TAR_FILE
_REMOTE_BASE_DIR = '/srv/awesome'
def _current_path():
return os.path.abspath('.')
def _now():
return datetime.now().strftime('%y-%m-%d_%H.%M.%S')
def build():
# includes = ['static', 'templates', 'transwarp', 'favicon.ico', '*.py']
# excludes = ['test', '.*', '*.pyc', '*.pyo']
local('del dist\\%s' % _TAR_FILE) # 删除旧压缩包
tar = tarfile.open("dist/%s" % _TAR_FILE,"w:gz") # 创建新压缩包
for root,_dir,files in os.walk("www/"): # 打包www文件夹
for f in files:
if not (('.pyc' in f) or ('.pyo' in f)): # 排除开发过程调试产生的文件,为了简单点实现,此处没有完全照搬廖老师的参数
fullpath = os.path.join(root,f)
tar.add(fullpath)
tar.close()
def deploy():
newdir = 'www-%s' % _now()
run('rm -rf %s' % _REMOTE_TMP_TAR)
put('dist/%s' % _TAR_FILE, _REMOTE_TMP_TAR)
with cd(_REMOTE_BASE_DIR):
sudo('mkdir %s' % newdir)
with cd('%s/%s' % (_REMOTE_BASE_DIR, newdir)):
sudo('tar -xzvf %s' % _REMOTE_TMP_TAR) # 解压
sudo('mv www/* .') # 解压后多一层www文件夹,因此向上移动一层
sudo('rm -rf www') # 删除空文件夹www
sudo('dos2unix app.py') # 解决windows和linux行尾换行不同问题
sudo('chmod a+x app.py') # 使app.py可直接执行
with cd(_REMOTE_BASE_DIR):
sudo('rm -rf www') # 删除旧软链接
sudo('ln -s %s www' % newdir) # 创建新链接
sudo('chown root:root www') # user改为你的linux服务器上的用户名
sudo('chown -R root:root %s' % newdir) # 同上
with settings(warn_only=True):
sudo('supervisorctl stop awesome') # supervisor重启app
sudo('supervisorctl start awesome')
sudo('/etc/init.d/nginx reload') # nginx重启
RE_FILES = re.compile('\r?\n')
def rollback():
'''
rollback to previous version
'''
with cd(_REMOTE_BASE_DIR):
r = run('ls -p -1')
files = [s[:-1] for s in RE_FILES.split(r) if s.startswith('www-') and s.endswith('/')]
files.sort(reverse=True)
r = run('ls -l www')
ss = r.split(' -> ')
if len(ss) != 2:
print('ERROR: \'www\' is not a symbol link.')
return
current = ss[1]
print('Found current symbol link points to: %s\n' % current)
try:
index = files.index(current)
except ValueError as e:
print('ERROR: symbol link is invalid.')
return
if len(files) == index + 1:
print('ERROR: already the oldest version.')
old = files[index + 1]
print('==================================================')
for f in files:
if f == current:
print(' Current ---> %s' % current)
elif f == old:
print(' Rollback to ---> %s' % old)
else:
print(' %s' % f)
print('==================================================')
print('')
yn = input ('continue? y/N ')
if yn != 'y' and yn != 'Y':
print('Rollback cancelled.')
return
print('Start rollback...')
sudo('rm -rf www')
sudo('ln -s %s www' % old)
sudo('chown www-data:www-data www')
with settings(warn_only=True):
sudo('supervisorctl stop awesome')
sudo('supervisorctl start awesome')
sudo('/etc/init.d/nginx reload')
print('ROLLBACKED OK.')
def backup():
'''
Dump entire database on server and backup to local.
'''
dt = _now()
f = 'backup-awesome-%s.sql' % dt
with cd('/tmp'):
run('mysqldump --user=%s --password=%s --skip-opt --add-drop-table --default-character-set=utf8 --quick awesome > %s' % (db_user, db_password, f))
run('tar -czvf %s.tar.gz %s' % (f, f))
get('%s.tar.gz' % f, '%s/backup/' % _current_path())
run('rm -rf %s' % f)
run('rm -rf %s.tar.gz' % f)
def restore2local():
'''
Restore db to local
'''
backup_dir = os.path.join(_current_path(), 'backup')
fs = os.listdir(backup_dir)
files = [f for f in fs if f.startswith('backup-') and f.endswith('.sql.tar.gz')] # 获取备份文件列表
files.sort(reverse = True) # 最近的文件排在前面
if len(files)==0:
print('No backup files found.')
return
print('Found %s backup files:' % len(files))
print('==================================================')
n = 0
for f in files:
print('%s: %s' % (n, f))
n = n + 1
print('==================================================')
print('')
try:
num = int(input ('Restore file: ')) # 选择恢复哪个备份
except ValueError:
print('Invalid file number.')
return
restore_file = files[num]
yn = input('Restore file %s: %s? y/N ' % (num, restore_file)) # 确定开始恢复
if yn != 'y' and yn != 'Y':
print('Restore cancelled.')
return
print('Start restore to local database...')
p = input('Input mysql root password: ')
sqls = [
'drop database if exists awesome;',
'create database awesome;',
'alter database awesome default character set utf8 collate utf8_general_ci;' # 修改为utf8字符集
'grant select, insert, update, delete on awesome.* to \'%s\'@\'localhost\' identified by \'%s\';' % (db_user, db_password)
]
for sql in sqls:
local(r'mysql -uroot -p%s -e "%s"' % (p, sql)) # 删除旧数据库,新建数据库,授权给用户
extract('backup\\%s' % restore_file, 'backup\\') # 解压
with lcd('backup'):
# linux系统和windows系统之间数据库导入导出,可能因为字符集不同出现'unknown command \\'错误
# 通过在创建数据库后修改为utf8字符集,以及导入时指定--default-character-set=utf8,解决这个问题
local(r'mysql -uroot -p%s --default-character-set=utf8 awesome < %s' % (p, restore_file[:-7])) # 导入数据库
local('del %s' % restore_file[:-7]) # 删除解压出的文件
def extract(tar_path, target_path):
'''
解压tar.gz文件到目标目录
'''
try:
tar = tarfile.open(tar_path, "r:gz")
file_names = tar.getnames()
for file_name in file_names:
tar.extract(file_name, target_path)
tar.close()
except Exception as e:
raise e
if __name__ == '__main__':
build()
deploy()
# rollback()
# backup()
# restore2local()
input()

直接运行fabfile.py即可连接linux服务器;

如果使用Linux:

提交修改:

1
fab build

实行应用:

1
fab deploy