# 使用 GCP 架設 API Server
###### tags: `GCP` `VPS` `VM` `API Server` `Python` `flask`
這個文件主要是紀錄TBrain比賽的API Server的架設過程,所以有些設定是面向這次比賽所設定的,希望透過這個說明文件可以幫助大家了解GCP的基本設定與伺服器架設的過程。
## 建立VM執行個體
首先要建立一個虛擬機器(Virtual Machine, VM)作為我們的Server,先切換到Compute Engine中的VM執行個體功能。

進來之後,點擊「建立執行個體」

### VM的各項配置
開始設定VM的硬體、網路的配置
* 名稱:這台虛擬機器的名稱(可以自己決定要取什麼名字)
* 區域:虛擬機器的國家地區,選擇Asia-east1(台灣),右邊的區域可以自由選擇a、b或c。
* 機器設定:虛擬機器的硬體規格(CPU、RAM),可以參考[[GCP] 使用費率一覽](https://sammy197.tw/4257/),根據自己的需求選擇適合的配置,這裡我們先選擇N1系列的n1-standard-1,後續有需要可以在自由更換配置。

* 容器:使用Docker Image來建立VM,我們這裡不使用Docker來建置環境,所以就先不管他。
* 開機磁碟:使用哪種公開的作業系統映像檔或已經建立好的磁碟映像檔來建立開機磁碟,這裡我習慣使用Ubuntu作為架設伺服器的系統,也可以選擇其他自己習慣的作業系統來用。
* 身分及API存取權:設定這個VM的Google使用者權限的相關設定,這裡使用預設不用更改。
* 防火牆:根據需求可以勾選是否要允許HTTP/HTTPS的流量進入這台VM,這裡就兩個都打勾。
設定好之後打開「管理、安全性、磁碟、網路、單獨租用」,要來設定網路的部分。

開機磁碟映像檔的設定畫面如下,可以在公開映像檔選擇自己想要的作業系統。

展開網路選項後,切到「網路」的頁籤,在網路介面找到目前有的網路卡(這邊網路卡名稱是default,網路卡的名稱會根據不同作業系統會有不同的名稱),之後點擊default這張網路卡來進行設定修改。

在外部IP那欄從「臨時」改成「建立IP位址」讓VM的IP從浮動IP變成固定IP。

名稱就依照自己喜好輸入即可,網路服務級別選擇「進階」,之後按保留來儲存設定。

固定IP設定完成後按下「完成」,到這邊基本的VM設定就結束了,接著按下「建立」就會開始建立VM。

建立完成後就可以在總覽清單看到VM的狀態、內/外部IP等資訊。

### 防火牆設定
接著來設定防火牆,因為主辦方提供的範例程式使用的Port不在預設的1000內,所以需要額外設定,點擊Google Cloud Platform左側的選單列,找到網路 > VPC網路 > 防火牆。

然後建立一個新的規則,點選「建立防火牆新規則」

規則的名稱的可以隨便打,自己記的起來就可以,記錄檔與流量方向保持預設。

目標標記輸入`http-server`,來源IP範圍輸入`0.0.0.0/0`,通訊協定和通訊埠的地方,在tcp部分加入`8080`,之後按下「建立」,就完成了開啟8080 port的防火牆設定。

## 設定VM的環境、安裝套件
在上一張圖中點擊剛剛建立好的VM右邊的「SSH」按鈕,使用SSH連進去主機的Terminal,接著就跟平常使用正常的Linux介面一樣;跳出SSH的連線畫面後,就可以開始安裝環境。

接著輸入以下指令,先更新apt套件後再安裝pip3,Ubuntu預設已經安裝Python 2跟3,但是pip的部分沒有安裝,所以我們要手動安裝pip3來安裝後續的Python套件。
```shell=
sudo apt update # 更新apt套件
sudo apt-get install python3-pip # 安裝pip3
```
接著安裝所需的Python套件,這裡主辦方提供的範本是使用Flask(一個Python的網頁框架)來架設API Server,我習慣使用Python的虛擬環境virtualenv來管理套件,所以會多安裝一個virtualenv來管理套件。
```shell=
pip3 install virtualenv # 安裝virtualenv
# 如果使用Ubuntu 18.04遇到無法使用virtualenv的時候可以輸入以下指令來安裝virtualenv
# sudo apt-get install python-virtualenv
mkdir api_server # 建立一個名為api_server/的資料夾
cd api_server/ # 切換進剛剛建立的那個api_server/資料夾
virtualenv -p python3 venv # 使用Python3建立一個虛擬環境,虛擬環境的名稱是venv
source venv/bin/activate # 進入虛擬環境
pip3 install flask numpy==1.15 pandas # 安裝flask, numpy, pandas,可以依據自己所需要的套件安裝
```
安裝完套件後來把範例的API Server程式碼複製到VM上吧,可以透過vi、nano等編輯器來完成這個工作,這裡我使用vi來建立檔案並貼上範例程式碼
```shell=
vi api_server.py # 建立一個名叫api_server.py的檔案,並進入編輯
```
進入vi後,預設的操作模式是游標模式,這個時候按上下左右鍵可以移動輸入點,因為這個是一個空檔案,所以就直接按下鍵盤上的`i`鍵進入輸入模式(Insert),接著將下面這段範例程式的程式碼直接貼上。
```python=
from flask import Flask
from flask import request
from flask import jsonify
import time
import hashlib
import numpy as np
import pandas as pd
app = Flask(__name__)
####### PUT YOUR INFORMATION HERE #######
CAPTAIN_EMAIL = 'my@gmail.com' #
SALT = 'my_salt' #
#########################################
def generate_server_uuid(input_string):
""" Create your own server_uuid
@param input_string (str): information to be encoded as server_uuid
@returns server_uuid (str): your unique server_uuid
"""
s = hashlib.sha256()
data = (input_string+SALT).encode("utf-8")
s.update(data)
server_uuid = s.hexdigest()
return server_uuid
def predict(article):
""" Predict your model result
@param article (str): a news article
@returns prediction (list): a list of name
"""
####### PUT YOUR MODEL INFERENCING CODE HERE #######
prediction = ['aha','danny','jack']
####################################################
prediction = _check_datatype_to_list(prediction)
return prediction
def _check_datatype_to_list(prediction):
""" Check if your prediction is in list type or not.
And then convert your prediction to list type or raise error.
@param prediction (list / numpy array / pandas DataFrame): your prediction
@returns prediction (list): your prediction in list type
"""
if isinstance(prediction, np.ndarray):
_check_datatype_to_list(prediction.tolist())
elif isinstance(prediction, pd.core.frame.DataFrame):
_check_datatype_to_list(prediction.values)
elif isinstance(prediction, list):
return prediction
raise ValueError('Prediction is not in list type.')
@app.route('/healthcheck', methods=['POST'])
def healthcheck():
""" API for health check """
data = request.get_json(force=True)
t = int(time.time())
ts = str(t)
server_uuid = generate_server_uuid(CAPTAIN_EMAIL+ts)
server_timestamp = t
return jsonify({'esun_uuid': data['esun_uuid'], 'server_uuid': server_uuid, 'captain_email': CAPTAIN_EMAIL, 'server_timestamp': server_timestamp})
@app.route('/inference', methods=['POST'])
def inference():
""" API that return your model predictions when E.SUN calls this API """
data = request.get_json(force=True)
esun_timestamp = data['esun_timestamp'] #自行取用
t = int(time.time())
ts = str(int(t))
server_uuid = generate_server_uuid(CAPTAIN_EMAIL+ts)
try:
answer = predict(data['news'])
except:
raise ValueError('Model error.')
server_timestamp = t
return jsonify({'esun_timestamp': data['esun_timestamp'], 'server_uuid': server_uuid, 'answer': answer, 'server_timestamp': server_timestamp, 'esun_uuid': data['esun_uuid']})
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080, debug=True)
```
貼上完後,按下`Esc`鍵離開輸入模式,接著輸入`:wq`來存檔並離開,然後輸入`python api_server.py`來啟動API Server。
## 設定新帳戶、開啟SSH連線、設定FTP
未完待續...。
## 參考資料
1. [【Gotcha! 我抓得住你 - AML 焦點人物辨識】
API 開發說明文件](https://hackmd.io/@UcQg6jwlT_WL_ZNkPZMm6Q/BJfELe_c8)