02.27 「Serverless」我就這樣做了一個Django的Component

之前有過朋友問我Flask、Express這些框架是如何在函數中運行,他是怎麼樣的一個機制?還有人問我如何做一個Component?看了一下騰訊雲Serverless架構現在支持的框架:

「Serverless」我就這樣做了一個Django的Component

我發現雖然支持了很多,但是我比較鍾愛的Django貌似沒有,正好想到了部分人的疑惑,所以在這裡,我就簡單的和大家說一下,我如何做一個Django的Component。


首先第一步,我們要知道其他的框架是怎麼運行的,例如Flask等,我們先通過騰訊雲的Flask-Component,按照他的說明部署一下:

「Serverless」我就這樣做了一個Django的Component

非常簡單輕鬆愉快的部署上線,然後在函數的控制檯,我們把部署好的下載下來,研究一下:

「Serverless」我就這樣做了一個Django的Component

下載解壓之後,我們可以看這樣一個目錄結構:

「Serverless」我就這樣做了一個Django的Component

藍色框起來的,是依賴包,黃色的app.py是我們的自己寫的代碼,那麼紅色圈起來的是什麼?這兩個文件從哪裡出來的?

api_server.py文件內容:

<code>import app  # Replace with your actual applicationimport severless_wsgi# If you need to send additional content types as text, add then directly# to the whitelist:## serverless_wsgi.TEXT_MIME_TYPES.append("application/custom+json")def handler(event, context):    return severless_wsgi.handle_request(app.app, event, context)/<code>

可以看到,這裡面是將我們創建的app.py文件引入,並且拿到了app這個對象,並且將event和context同時傳遞給severless_wsgi.py中的handle_reques方法中,那麼問題來了,這個方法是什麼?

「Serverless」我就這樣做了一個Django的Component

這個方法內容好多......看著有點眼暈,但是,我們可以直接發現這一段代碼:

「Serverless」我就這樣做了一個Django的Component

這一段是什麼呢?這一段實際上就是將我們拿到的參數(event和context)進行轉換,轉換之後統一environ中,然後接下來通過werkzeug這個依賴,將這個內容變成request對象,並且與我們剛才說的app對象一起調用from_app方法。獲得到反饋:

「Serverless」我就這樣做了一個Django的Component

並且按照API網關的響應集成的格式,將結果返回。

此時此刻,各位看官可能有點想法了,貌似有一丟丟靈感出現了,那麼我們不妨看一下Flask/Django這些框架的實現原理:

「Serverless」我就這樣做了一個Django的Component

通過這個簡版的原理圖,和我剛才說的內容,我們可以想到,實際上正常用的時候要通過web_server,進入到下一個環節,而我們雲函數更多是一個函數,本不需要啟動web server,所以我們就可以直接調用wsgi_app這個方法,其中這裡的environ就是我們剛才的通過對event/context等進行處理後的對象,start_response可以認為是我們的一種特殊的數據結構,例如我們的response結構形態等。所以,如果我們自己想要實現這個過程,不使用騰訊雲flask-component,可以這樣做:

<code>import systry:    from urllib import urlencodeexcept ImportError:    from urllib.parse import urlencodefrom flask import Flasktry:    from cStringIO import StringIOexcept ImportError:    try:        from StringIO import StringIO    except ImportError:        from io import StringIOfrom werkzeug.wrappers import BaseRequest__version__ = '0.0.4'def make_environ(event):    environ = {}    for hdr_name, hdr_value in event['headers'].items():        hdr_name = hdr_name.replace('-', '_').upper()        if hdr_name in ['CONTENT_TYPE', 'CONTENT_LENGTH']:            environ[hdr_name] = hdr_value            continue        http_hdr_name = 'HTTP_%s' % hdr_name        environ[http_hdr_name] = hdr_value    apigateway_qs = event['queryStringParameters']    request_qs = event['queryString']    qs = apigateway_qs.copy()    qs.update(request_qs)    body = ''    if 'body' in event:        body = event['body']    environ['REQUEST_METHOD'] = event['httpMethod']    environ['PATH_INFO'] = event['path']    environ['QUERY_STRING'] = urlencode(qs) if qs else ''    environ['REMOTE_ADDR'] = 80    environ['HOST'] = event['headers']['host']    environ['SCRIPT_NAME'] = ''    environ['SERVER_PORT'] = 80    environ['SERVER_PROTOCOL'] = 'HTTP/1.1'    environ['CONTENT_LENGTH'] = str(len(body))    environ['wsgi.url_scheme'] = ''    environ['wsgi.input'] = StringIO(body)    environ['wsgi.version'] = (1, 0)    environ['wsgi.errors'] = sys.stderr    environ['wsgi.multithread'] = False    environ['wsgi.run_once'] = True    environ['wsgi.multiprocess'] = False    BaseRequest(environ)    return environclass LambdaResponse(object):    def __init__(self):        self.status = None        self.response_headers = None    def start_response(self, status, response_headers, exc_info=None):        self.status = int(status[:3])        self.response_headers = dict(response_headers)class FlaskLambda(Flask):    def __call__(self, event, context):        if 'httpMethod' not in event:            print('httpMethod not in event')            return super(FlaskLambda, self).__call__(event, context)        response = LambdaResponse()        body = next(self.wsgi_app(            make_environ(event),            response.start_response        ))        return {            'statusCode': response.status,            'headers': response.response_headers,            'body': body        }/<code>

這樣一個流程,就會變得更加簡單,清楚。整個實現過程,可以認為是對web server部分進行了一種“截斷”或者是“替換”:

「Serverless」我就這樣做了一個Django的Component

這就是對Flask-Component的基本分析思路,那麼按照這個思路,我們是否可以將Django框架部署上Serverless架構呢?那麼Flask和Django有什麼區別呢?我這裡的區別特指的是在運行啟動過程中。

仔細想一下,貌似並沒有區別,那麼我們是不是可以直接用Flask這個轉換邏輯,將flask的app替換成django的app呢?

把:

<code>from flask import Flaskapp = Flask(__name__)/<code>

替換成:

<code>import osfrom django.core.wsgi import get_wsgi_applicationos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mydjango.settings')application = get_wsgi_application()/<code>

是否就能解決問題呢?

我們不妨試一下:

「Serverless」我就這樣做了一個Django的Component

建立好Django項目,直接增加index.py:

<code># -*- coding: utf-8 -*-​import osimport sysimport base64from werkzeug.datastructures import Headers, MultiDictfrom werkzeug.wrappers import Responsefrom werkzeug.urls import url_encode, url_unquotefrom werkzeug.http import HTTP_STATUS_CODESfrom werkzeug._compat import BytesIO, string_types, to_bytes, wsgi_encoding_danceimport mydjango.wsgi​TEXT_MIME_TYPES = [    "application/json",    "application/javascript",    "application/xml",    "application/vnd.api+json",    "image/svg+xml",]​​def all_casings(input_string):    if not input_string:        yield ""    else:        first = input_string[:1]        if first.lower() == first.upper():            for sub_casing in all_casings(input_string[1:]):                yield first + sub_casing        else:            for sub_casing in all_casings(input_string[1:]):                yield first.lower() + sub_casing                yield first.upper() + sub_casing​​def split_headers(headers):    """    If there are multiple occurrences of headers, create case-mutated variations    in order to pass them through APIGW. This is a hack that's currently    needed. See: https://github.com/logandk/serverless-wsgi/issues/11    Source: https://github.com/Miserlou/Zappa/blob/master/zappa/middleware.py    """    new_headers = {}​    for key in headers.keys():        values = headers.get_all(key)        if len(values) > 1:            for value, casing in zip(values, all_casings(key)):                new_headers[casing] = value        elif len(values) == 1:            new_headers[key] = values[0]​    return new_headers​​def group_headers(headers):    new_headers = {}​    for key in headers.keys():        new_headers[key] = headers.get_all(key)​    return new_headers​​def encode_query_string(event):    multi = event.get(u"multiValueQueryStringParameters")    if multi:        return url_encode(MultiDict((i, j) for i in multi for j in multi[i]))    else:        return url_encode(event.get(u"queryString") or {})​​def handle_request(application, event, context):​    if u"multiValueHeaders" in event:        headers = Headers(event["multiValueHeaders"])    else:        headers = Headers(event["headers"])​    strip_stage_path = os.environ.get("STRIP_STAGE_PATH", "").lower().strip() in [        "yes",        "y",        "true",        "t",        "1",    ]    if u"apigw.tencentcs.com" in headers.get(u"Host", u"") and not strip_stage_path:       >

然後我們部署到函數上,看一下效果:

函數信息:

<code>from django.shortcuts import renderfrom django.http import HttpResponsefrom django.views.decorators.csrf import csrf_exempt# Create your views here.@csrf_exemptdef hello(request):    if request.method == "POST":        return HttpResponse("Hello world ! " + request.POST.get("name"))    if request.method == "GET":        return HttpResponse("Hello world ! " + request.GET.get("name"))/<code>

通過部署完成,並綁定apigw觸發器,然後在postman中進行測試:

get:

「Serverless」我就這樣做了一個Django的Component

post:

「Serverless」我就這樣做了一個Django的Component

可以看到,通過我們對運行原理的基本剖析和對django的改造,我們已經通過增加一個文件和相關依賴的方法,實現了Django上Serverless的過程。

接下來,我們看一下,如何將這個代碼寫成一個Component:

首先Clone下來Flask-Component的代碼:

「Serverless」我就這樣做了一個Django的Component

然後,我們按照Django的部分模式進行修改:

「Serverless」我就這樣做了一個Django的Component

第一部分,是我們可能會依賴的一個依賴包,以及我們剛才放入的index.py文件。在用戶調用這個Component的時候,我們會把這兩個文件,放入用戶的代碼中,一併上傳。

第二部分是Serverless.js部分,這裡的一個基本格式:

<code>const { Component } = require('@serverless/core')class TencentDjango extends Component {  async default(inputs = {}) {  }  async remove(inputs = {}) {  }}module.exports = TencentDjango/<code>

用戶在執行sls的時候,會默認調用default的方法,在執行sls remove的時候會調用remove的方法,所以可以認default的內容是部署,而remove的內容是移除。

部署這裡主要流程也蠻簡單的,首先將文件進行復制和處理,然後直接調用雲函數的組件,通過函數中的include參數將這些文件額外加入,再通過調用apigw的組件來進網關的管理,而用戶寫的yaml中inpust的內容,會在inputs中獲取,我們要做的就是對應的傳給不同的組件:

「Serverless」我就這樣做了一個Django的Component

當然除了這兩部分對應放過去,上面的region等一些信息也要對應的進行處理。而調用底層組件方法也很簡單:

<code>const tencentCloudFunction = await this.load('@serverless/tencent-scf'const tencentCloudFunctionOutputs = await tencentCloudFunction(inputs)/<code>

處理好這裡之後,只需要修改一下package.json和readme就可以了。

「Serverless」我就這樣做了一個Django的Component

「Serverless」我就這樣做了一個Django的Component

目前,我已經完成了開源:https://github.com/gosls/tencent-django

也在NPM上進行了發佈:https://www.npmjs.com/package/@gosls/tencent-django

在使用的時候,只需要引入這個Component就好:

<code>DjangoTest:  component: '@serverless/tencent-django'  inputs:    region: ap-guangzhou    functionName: DjangoFunctionTest    djangoProjectName: mydjango    code: ./    functionConf:      timeout: 10      memorySize: 256      environment:        variables:          TEST: vale      vpcConfig:        subnetId: ''        vpcId: ''    apigatewayConf:      protocols:        - http      environment: release/<code>

至此,完成了Django Component的開發和測試。


分享到:


相關文章: