# Day 15 - 部署Web App
作為一個合格的開發者,在本地環境下完成開發還遠遠不夠,我們需要把Web App部署到遠程服務器上,這樣,廣大用戶才能訪問到網站。
很多做開發的同學把部署這件事情看成是運維同學的工作,這種看法是完全錯誤的。首先,最近流行[DevOps](http://zh.wikipedia.org/wiki/DevOps)理念,就是說,開發和運維要變成一個整體。其次,運維的難度,其實跟開發質量有很大的關系。代碼寫得垃圾,運維再好也架不住天天掛掉。最后,DevOps理念需要把運維、監控等功能融入到開發中。你想服務器升級時不中斷用戶服務?那就得在開發時考慮到這一點。
下面,我們就來把awesome-python3-webapp部署到Linux服務器。
## 搭建Linux服務器
要部署到Linux,首先得有一臺Linux服務器。要在公網上體驗的同學,可以在Amazon的[AWS](http://aws.amazon.com/)申請一臺EC2虛擬機(免費使用1年),或者使用國內的一些云服務器,一般都提供Ubuntu Server的鏡像。想在本地部署的同學,請安裝虛擬機,推薦使用[VirtualBox](https://www.virtualbox.org/)。
我們選擇的Linux服務器版本是[Ubuntu Server 14.04 LTS](http://www.ubuntu.com/download/server),原因是apt太簡單了。如果你準備使用其他Linux版本,也沒有問題。
Linux安裝完成后,請確保ssh服務正在運行,否則,需要通過apt安裝:
```
$ sudo apt-get install openssh-server
```
有了ssh服務,就可以從本地連接到服務器上。建議把公鑰復制到服務器端用戶的`.ssh/authorized_keys`中,這樣,就可以通過證書實現無密碼連接。
## 部署方式
利用Python自帶的asyncio,我們已經編寫了一個異步高性能服務器。但是,我們還需要一個高性能的Web服務器,這里選擇Nginx,它可以處理靜態資源,同時作為反向代理把動態請求交給Python代碼處理。這個模型如下:

Nginx負責分發請求:

在服務器端,我們需要定義好部署的目錄結構:
```
/
+- srv/
+- awesome/ <-- Web App根目錄
+- www/ <-- 存放Python源碼
| +- static/ <-- 存放靜態資源文件
+- log/ <-- 存放log
```
在服務器上部署,要考慮到新版本如果運行不正常,需要回退到舊版本時怎么辦。每次用新的代碼覆蓋掉舊的文件是不行的,需要一個類似版本控制的機制。由于Linux系統提供了軟鏈接功能,所以,我們把`www`作為一個軟鏈接,它指向哪個目錄,哪個目錄就是當前運行的版本:

而Nginx和gunicorn的配置文件只需要指向`www`目錄即可。
Nginx可以作為服務進程直接啟動,但gunicorn還不行,所以,[Supervisor](http://supervisord.org/)登場!Supervisor是一個管理進程的工具,可以隨系統啟動而啟動服務,它還時刻監控服務進程,如果服務進程意外退出,Supervisor可以自動重啟服務。
總結一下我們需要用到的服務有:
* Nginx:高性能Web服務器+負責反向代理;
* Supervisor:監控服務進程的工具;
* MySQL:數據庫服務。
在Linux服務器上用apt可以直接安裝上述服務:
```
$ sudo apt-get install nginx supervisor python3 mysql-server
```
然后,再把我們自己的Web App用到的Python庫安裝了:
```
$ sudo pip3 install jinja2 aiomysql aiohttp
```
在服務器上創建目錄`/srv/awesome/`以及相應的子目錄。
在服務器上初始化MySQL數據庫,把數據庫初始化腳本`schema.sql`復制到服務器上執行:
```
$ mysql -u root -p < schema.sql
```
服務器端準備就緒。
## 部署
用FTP還是SCP還是rsync復制文件?如果你需要手動復制,用一次兩次還行,一天如果部署50次不但慢、效率低,而且容易出錯。
正確的部署方式是使用工具配合腳本完成自動化部署。[Fabric](http://www.fabfile.org/)就是一個自動化部署工具。由于Fabric是用Python 2.x開發的,所以,部署腳本要用Python 2.7來編寫,本機還必須安裝Python 2.7版本。
要用Fabric部署,需要在本機(是開發機器,不是Linux服務器)安裝Fabric:
```
$ easy_install fabric
```
Linux服務器上不需要安裝Fabric,Fabric使用SSH直接登錄服務器并執行部署命令。
下一步是編寫部署腳本。Fabric的部署腳本叫`fabfile.py`,我們把它放到`awesome-python-webapp`的目錄下,與`www`目錄平級:
```
awesome-python-webapp/
+- fabfile.py
+- www/
+- ...
```
Fabric的腳本編寫很簡單,首先導入Fabric的API,設置部署時的變量:
```
# fabfile.py
import os, re
from datetime import datetime
# 導入Fabric API:
from fabric.api import *
# 服務器登錄用戶名:
env.user = 'michael'
# sudo用戶為root:
env.sudo_user = 'root'
# 服務器地址,可以有多個,依次部署:
env.hosts = ['192.168.0.3']
# 服務器MySQL用戶名和口令:
db_user = 'www-data'
db_password = 'www-data'
```
然后,每個Python函數都是一個任務。我們先編寫一個打包的任務:
```
_TAR_FILE = 'dist-awesome.tar.gz'
def build():
includes = ['static', 'templates', 'transwarp', 'favicon.ico', '*.py']
excludes = ['test', '.*', '*.pyc', '*.pyo']
local('rm -f dist/%s' % _TAR_FILE)
with lcd(os.path.join(os.path.abspath('.'), 'www')):
cmd = ['tar', '--dereference', '-czvf', '../dist/%s' % _TAR_FILE]
cmd.extend(['--exclude=\'%s\'' % ex for ex in excludes])
cmd.extend(includes)
local(' '.join(cmd))
```
Fabric提供`local('...')`來運行本地命令,`with lcd(path)`可以把當前命令的目錄設定為`lcd()`指定的目錄,注意Fabric只能運行命令行命令,Windows下可能需要[Cgywin](http://cygwin.com/)環境。
在`awesome-python-webapp`目錄下運行:
```
$ fab build
```
看看是否在`dist`目錄下創建了`dist-awesome.tar.gz`的文件。
打包后,我們就可以繼續編寫`deploy`任務,把打包文件上傳至服務器,解壓,重置`www`軟鏈接,重啟相關服務:
```
_REMOTE_TMP_TAR = '/tmp/%s' % _TAR_FILE
_REMOTE_BASE_DIR = '/srv/awesome'
def deploy():
newdir = 'www-%s' % datetime.now().strftime('%y-%m-%d_%H.%M.%S')
# 刪除已有的tar文件:
run('rm -f %s' % _REMOTE_TMP_TAR)
# 上傳新的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)
# 重置軟鏈接:
with cd(_REMOTE_BASE_DIR):
sudo('rm -f www')
sudo('ln -s %s www' % newdir)
sudo('chown www-data:www-data www')
sudo('chown -R www-data:www-data %s' % newdir)
# 重啟Python服務和nginx服務器:
with settings(warn_only=True):
sudo('supervisorctl stop awesome')
sudo('supervisorctl start awesome')
sudo('/etc/init.d/nginx reload')
```
注意`run()`函數執行的命令是在服務器上運行,`with cd(path)`和`with lcd(path)`類似,把當前目錄在服務器端設置為`cd()`指定的目錄。如果一個命令需要sudo權限,就不能用`run()`,而是用`sudo()`來執行。
## 配置Supervisor
上面讓Supervisor重啟awesome的命令會失敗,因為我們還沒有配置Supervisor呢。
編寫一個Supervisor的配置文件`awesome.conf`,存放到`/etc/supervisor/conf.d/`目錄下:
```
[program:awesome]
command = /srv/awesome/www/app.py
directory = /srv/awesome/www
user = www-data
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`。
然后重啟Supervisor后,就可以隨時啟動和停止Supervisor管理的服務了:
```
$ sudo supervisorctl reload
$ sudo supervisorctl start awesome
$ sudo supervisorctl status
awesome RUNNING pid 1401, uptime 5:01:34
```
## 配置Nginx
Supervisor只負責運行gunicorn,我們還需要配置Nginx。把配置文件`awesome`放到`/etc/nginx/sites-available/`目錄下:
```
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/`目錄下創建軟鏈接:
```
$ pwd
/etc/nginx/sites-enabled
$ sudo ln -s /etc/nginx/sites-available/awesome .
```
讓Nginx重新加載配置文件,不出意外,我們的`awesome-python3-webapp`應該正常運行:
```
$ sudo /etc/init.d/nginx reload
```
如果有任何錯誤,都可以在`/srv/awesome/log`下查找Nginx和App本身的log。如果Supervisor啟動時報錯,可以在`/var/log/supervisor`下查看Supervisor的log。
如果一切順利,你可以在瀏覽器中訪問Linux服務器上的`awesome-python3-webapp`了:

如果在開發環境更新了代碼,只需要在命令行執行:
```
$ fab build
$ fab deploy
```
自動部署完成!刷新瀏覽器就可以看到服務器代碼更新后的效果。
## 友情鏈接
嫌國外網速慢的童鞋請移步網易和搜狐的鏡像站點:
[http://mirrors.163.com/](http://mirrors.163.com/)
[http://mirrors.sohu.com/](http://mirrors.sohu.com/)
## 參考源碼
[day-15](https://github.com/michaelliao/awesome-python3-webapp/tree/day-15)
- JavaScript教程
- JavaScript簡介
- 快速入門
- 基本語法
- 數據類型和變量
- 字符串
- 數組
- 對象
- 條件判斷
- 循環
- Map和Set
- iterable
- 函數
- 函數定義和調用
- 變量作用域
- 方法
- 高階函數
- map/reduce
- filter
- sort
- 閉包
- 箭頭函數
- generator
- 標準對象
- Date
- RegExp
- JSON
- 面向對象編程
- 創建對象
- 原型繼承
- 瀏覽器
- 瀏覽器對象
- 操作DOM
- 更新DOM
- 插入DOM
- 刪除DOM
- 操作表單
- 操作文件
- AJAX
- Promise
- Canvas
- jQuery
- 選擇器
- 層級選擇器
- 查找和過濾
- 操作DOM
- 修改DOM結構
- 事件
- 動畫
- 擴展
- underscore
- Collections
- Arrays
- Functions
- Objects
- Chaining
- Node.js
- 安裝Node.js和npm
- 第一個Node程序
- 模塊
- 基本模塊
- fs
- stream
- http
- buffer
- Web開發
- koa
- mysql
- swig
- 自動化工具
- 期末總結
- Python 2.7教程
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷和循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 函數式編程
- 高階函數
- map/reduce
- filter
- sorted
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 使用__future__
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 面向對象高級編程
- 使用__slots__
- 使用@property
- 多重繼承
- 定制類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- 常用第三方模塊
- PIL
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 協程
- gevent
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫數據庫模塊
- Day 3 - 編寫ORM
- Day 4 - 編寫Model
- Day 5 - 編寫Web框架
- Day 6 - 添加配置文件
- Day 7 - 編寫MVC
- Day 8 - 構建前端
- Day 9 - 編寫API
- Day 10 - 用戶注冊和登錄
- Day 11 - 編寫日志創建頁
- Day 12 - 編寫日志列表頁
- Day 13 - 提升開發效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 編寫移動App
- 期末總結
- Python3教程
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- Python代碼運行助手
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷
- 循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 迭代器
- 函數式編程
- 高階函數
- map/reduce
- filter
- sorted
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 實例屬性和類屬性
- 面向對象高級編程
- 使用__slots__
- 使用@property
- 多重繼承
- 定制類
- 使用枚舉類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- StringIO和BytesIO
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- datetime
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- urllib
- 常用第三方模塊
- PIL
- virtualenv
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 異步IO
- 協程
- asyncio
- async/await
- aiohttp
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫Web App骨架
- Day 3 - 編寫ORM
- Day 4 - 編寫Model
- Day 5 - 編寫Web框架
- Day 6 - 編寫配置文件
- Day 7 - 編寫MVC
- Day 8 - 構建前端
- Day 9 - 編寫API
- Day 10 - 用戶注冊和登錄
- Day 11 - 編寫日志創建頁
- Day 12 - 編寫日志列表頁
- Day 13 - 提升開發效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 編寫移動App
- FAQ
- 期末總結
- Git教程
- Git簡介
- Git的誕生
- 集中式vs分布式
- 安裝Git
- 創建版本庫
- 時光機穿梭
- 版本回退
- 工作區和暫存區
- 管理修改
- 撤銷修改
- 刪除文件
- 遠程倉庫
- 添加遠程庫
- 從遠程庫克隆
- 分支管理
- 創建與合并分支
- 解決沖突
- 分支管理策略
- Bug分支
- Feature分支
- 多人協作
- 標簽管理
- 創建標簽
- 操作標簽
- 使用GitHub
- 自定義Git
- 忽略特殊文件
- 配置別名
- 搭建Git服務器
- 期末總結