普通视图

发现新文章,点击刷新页面。
昨天以前the5fire的技术博客

通过github actions部署aws lambda记录 - s3部署、ECR部署 以及固定出口IP

作者 the5fire
2024年6月26日 17:22

背景

一个每天只需要运行一次的策略(每天只需要去交易所拿一次行情数据,计算完成后输出结果),放到EC2里跑有点浪费资源,不如部署到aws lambda中。

本篇内容主要记录在使用lambda时遇到的依赖资源过大的问题。

下面主要分两部分来记录,

第一部分:lambda使用

关于lambda应该不用详细介绍,直接看这里即可:lambda文档

lambda的使用比较简单,创建时有三种方式,前两种(从头开始创建和使用蓝图)可以直接在aws面板上编辑代码,或者上传代码压缩包(zip)来部署,第三种是基于容器来部署。

根据项目不同情况,有三种部署方式:

1. 没有外部依赖的代码,比如:

import json


def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': json.dumps({'content': 'hi the5fire'})
    }

可以直接在面板编辑并更新项目。

2. 有外部依赖的情况,比如:

import json

import requests


def lambda_handler(event, context):
    r = requests.get('https://api.github.com/user', auth=('user', 'pass'))

    return {
        'statusCode': 200,
        'body': json.dumps({'content': f'hi the5fire, code:{r.status_code}'})
    }

这种情况需要处理外部依赖,有两个方案:

方案一:在部署项目时需要把依赖打包一起部署。打包命令如下 :

pip install -r requirements.txt --target .
zip -r lambda_function.zip . -x '*.git*'

之后把zip包在aws lambda管理面板上传即可。

方案二:可以通过layer的方式处理

即在lambda管理面包上找到【层】的菜单,进去创建一个新的层,把依赖单独打包上传到该层,最后关联到你的lambda函数下面即可。

这两个方案都有一个限制就是你的整个项目+依赖包的大小不能超过250M,如果超了,那就看第三种方式。

3. 使用容器部署的方式,也就是ECR

简单来说就是把你的代码放进容器里,镜像打包注册到aws的ecr,然后lambda直接运行容器.

在项目中增加一个Dockerfile:

FROM public.ecr.aws/lambda/python:3.12

# Copy requirements.txt
COPY .  ${LAMBDA_TASK_ROOT}

# Install the specified packages
RUN pip install -r requirements.txt

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "lambda_function.lambda_handler" ]

项目结构如下:

.
|-- .github/workflows/deploy.yml
|-- Dockerfile
|-- README.md
|-- lambda_function.py
|-- requirements.txt
|-- strategy.py

部署的话需要使用docker和aws cli工具,参考这里:https://docs.aws.amazon.com/zh_cn/lambda/latest/dg/python-image.html,我自己的使用方式也会放到下面github action workflow配置中。

第二部分:github action配置

使用github action部署lambda之前,需要先去aws的IAM管理后台创建访问KEY,然后配置到github 的action/secrets中。

对于使用zip部署的方式也分两种情况,压缩包小于10M可以直接在上传,大于10M、小于250M的需要通过s3来上传。

所以这部分的action配置也分两种。

  1. 直接上传zip文件的方式:
# .github/workflows/deploy.yml
name: Deploy Lambda Function

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Set up Python 3.12
      uses: actions/setup-python@v2
      with:
        python-version: 3.12

    - name: Install & zip
      run: |
        pip install --upgrade pip
        pip install -r ./requirements.txt
        zip -r lambda_function.zip . -x '*.git*'

    - name: Deploy to AWS Lambda
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}

      run: |
        # Assuming you have an IAM role ARN for your Lambda function
        aws lambda update-function-code \
          --function-name helloworld \
          --zip-file fileb://lambda_function.zip
  1. 通过 s3 上传zip包并部署:

这里需要先到aws s3上创建桶

name: Deploy Lambda Function

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Set up Python 3.12
      uses: actions/setup-python@v2
      with:
        python-version: 3.12

    - name: Install & zip
      run: |
        pip install --upgrade pip
        pip install -r ./requirements.txt --target .
        zip -r lambda_function.zip . -x '*.git*'

    - name: Deploy to AWS Lambda
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}

      run: |
        # Assuming you have an IAM role ARN for your Lambda function
        aws s3 cp ./lambda_function.zip s3://${{ secrets.BUCKET_NAME }}/
        aws lambda update-function-code --function-name ${{ secrets.FUNCTION_NAME }} \
          --s3-bucket ${{ secrets.BUCKET_NAME }} --s3-key lambda_function.zip
  1. 使用ecr方式部署:

这一步需要配置aws account id,你在ECR创建完存储库之后就可以从URI上看到。

name: Deploy Lambda Function

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: get login
      run: |
        aws ecr get-login-password --region ${{ secrets.AWS_REGION}}| docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com

    - name: update image

      run: |
        docker build -t docker-image:test .
        docker tag docker-image:test ${{secrets.AWS_ACCOUNT_ID }}.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/${{ secrets.FUNCTION_NAME }}:latest
        docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/${{ secrets.FUNCTION_NAME }}:latest

    - name: update function
      run: |
         aws lambda update-function-code \
            --function-name ${{ secrets.FUNCTION_NAME }} \
            --image-uri ${{secrets.AWS_ACCOUNT_ID}}.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/${{ secrets.FUNCTION_NAME }}:latest

需要说明的是,我这里都是只使用github action来做update function code,并没有使用它来创建function的需求,因此没写这创建的逻辑。对于使用ecr来部署的lambda function来说,需要先提交代码到github上,运行action后生成镜像,然后再创建lambda。

最后,关于出口IP固定的配置,参考文档:使用 Lambda 函数、Amazon VPC 和无服务器架构生成静态出站 IP 地址

示例代码: https://github.com/the5fire/action-lambda-demo/commits/main/

阅读原文
Django视频教程

Django源码解析第一季 剧终

作者 the5fire
2019年10月26日 08:12

更新了近两年的时间,对于早期就支持的读者确实表示抱歉,也很感谢各位的理解。

今天终于上传了最后一节,非常感谢各位读者的支持。

源码阅读如果没有切实的场景确实不那么容易,如果你是检查每节都看过来并且自己翻了源代码的,我对你表示佩服。

事实上the5fire 个人认为,这套源码的课程更适合遇到对应的问题,或者有对应的场景,刚好要看对应的源码,可以顺带着看看 the5fire 的讲解。

源码里的很多地方的应用场景其实已经超过了我的使用场景,因此只能是推测、尝试他的用法和这么写的原因。

需要说明的是,我的解读也不一定正确,受限于个人的使用场景、个人的精力和时间投入。如果读者发现其中哪些内容有误或跟你理解的有误差,欢迎反馈。

最后再次感谢。

接下来会搞一些新的事情。

Django-inside-video

阅读原文
Django视频教程

【Django源码阅读】Django 自定义异常处理页面源码解读

作者 the5fire
2019年8月10日 12:02

Django 自定义异常处理页面源码解读

这个解读来源于一个读者的反馈,于是花了几分钟看了下这部分源码,打算用十分钟的时间写一下,预计阅读需要 5 分钟。

自定义异常页面

Django 提供了常见的错误的页面,比如

  • 说用户访问了一个不存在的路径,引发的 404
  • 系统发生了一个异常,出现了 500

一个好的网站应该可以给用户友好的信息提示,比如:“服务器提了一个问题”之类的,然后给用户一个引导。对于商业网站需要注意的是错误页面的流量也是流量,应该有明确的引导。

在 Django 中定义这类处理很简单,只需要在 urls.py 中配置:

# 参考:https://github.com/the5fire/typeidea/blob/deploy-to-cloud/typeidea/typeidea/urls.py#L24
handler404 = Handler404.as_view()
handler500 = Handler50x.as_view()

当然你需要定义这里面的 Handler50x:

class Handler404(CommonViewMixin, TemplateView):
    template_name = '404.html'

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context, status=404)


class Handler50x(CommonViewMixin, TemplateView):
    template_name = '50x.html'

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context, status=500)

这样就可以简单的控制出错时展示给用户的页面了。需要注意的是,这个配置只会在非 Debug 模式下有效。

Django Error Handler 源码解析

要看这部分源码的第一步是判断 Django 可能会在哪处理这个异常。有很多方法,这里是说一种,从请求的入口开始撸。

注意我看到版本是 Django 2.0.1

1 WSGI Handler 的部分

# 代码:https://github.com/the5fire/django-inside/blob/84f272e1206554b43c86c0f7a50f37d1f3efbc28/django/core/handlers/wsgi.py#L135
class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __init__(self, *args, **kwargs):
        super(WSGIHandler, self).__init__(*args, **kwargs)
        self.load_middleware()

    def __call__(self, environ, start_response):
        set_script_prefix(get_script_name(environ))
        signals.request_started.send(sender=self.__class__, environ=environ)
        request = self.request_class(environ)
        response = self.get_response(request)  # the5fire: 注意这儿
        # ... the5fire:省略其他

2 BaseHandler 中的 get_response

    # ref: https://github.com/the5fire/django-inside/blob/84f272e1206554b43c86c0f7a50f37d1f3efbc28/django/core/handlers/base.py#L94
    def get_response(self, request):
        """Return an HttpResponse object for the given HttpRequest."""
        # Setup default url resolver for this thread
        set_urlconf(settings.ROOT_URLCONF)

        response = self._middleware_chain(request)  # the5fire: 这里进去

        response._closable_objects.append(request)

        # If the exception handler returns a TemplateResponse that has not
        # been rendered, force it to be rendered.
        if not getattr(response, 'is_rendered', True) and callable(getattr(response, 'render', None)):
            response = response.render()

        if response.status_code == 404:
            logger.warning(
                'Not Found: %s', request.path,
                extra={'status_code': 404, 'request': request},
            )

        return response

3 被包装的 _middleware_chain

    # https://github.com/the5fire/django-inside/blob/84f272e1206554b43c86c0f7a50f37d1f3efbc28/django/core/handlers/base.py#L76
    def load_middleware(self):
        """
        Populate middleware lists from settings.MIDDLEWARE.

        Must be called after the environment is fixed (see __call__ in subclasses).
        """
        self._request_middleware = []
        self._view_middleware = []
        self._template_response_middleware = []
        self._response_middleware = []
        self._exception_middleware = []

        handler = convert_exception_to_response(self._get_response)
        for middleware_path in reversed(settings.MIDDLEWARE):
            middleware = import_string(middleware_path)
            # ...  the5fire:忽略中间这些代码
            handler = convert_exception_to_response(mw_instance)

        # We only assign to this when initialization is complete as it is used
        # as a flag for initialization being complete.
        self._middleware_chain = handler

4 具体处理异常的部分

    def convert_exception_to_response(get_response):
        """
        Wrap the given get_response callable in exception-to-response conversion.

        All exceptions will be converted. All known 4xx exceptions (Http404,
        PermissionDenied, MultiPartParserError, SuspiciousOperation) will be
        converted to the appropriate response, and all other exceptions will be
        converted to 500 responses.

        This decorator is automatically applied to all middleware to ensure that
        no middleware leaks an exception and that the next middleware in the stack
        can rely on getting a response instead of an exception.
        """
        @wraps(get_response)
        def inner(request):
            try:
                response = get_response(request)
            except Exception as exc:
                response = response_for_exception(request, exc)  # the5fire: 这里进去
            return response
        return inner


    def response_for_exception(request, exc):
        if isinstance(exc, Http404):
            if settings.DEBUG:
                response = debug.technical_404_response(request, exc)
            else:
                response = get_exception_response(request, get_resolver(get_urlconf()), 404, exc)

        # ... the5fire: 省略掉一大坨类似的代码

        else:
            signals.got_request_exception.send(sender=None, request=request)
            # the5fire: 下面这一行,具体的处理逻辑。
            response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())

        # Force a TemplateResponse to be rendered.
        if not getattr(response, 'is_rendered', True) and callable(getattr(response, 'render', None)):
            response = response.render()

        return response

5 异常处理逻辑

    # https://github.com/the5fire/django-inside/blob/84f272e1206554b43c86c0f7a50f37d1f3efbc28/django/core/handlers/exception.py#L107
    def handle_uncaught_exception(request, resolver, exc_info):
        """
        Processing for any otherwise uncaught exceptions (those that will
        generate HTTP 500 responses).
        """
        if settings.DEBUG_PROPAGATE_EXCEPTIONS:
            raise

        logger.error(
            'Internal Server Error: %s', request.path,
            exc_info=exc_info,
            extra={'status_code': 500, 'request': request},
        )

        if settings.DEBUG:
            return debug.technical_500_response(request, *exc_info)

        # Return an HttpResponse that displays a friendly error message.
        # the5fire: 这里会解析到对应的handler ,比如我们定义的那个
        callback, param_dict = resolver.resolve_error_handler(500)
        return callback(request, **param_dict)

6 最终解析到 urls/resolvers.py 中

    # 完整代码: https://github.com/the5fire/django-inside/blob/84f272e1206554b43c86c0f7a50f37d1f3efbc28/django/urls/resolvers.py#L555
    def resolve_error_handler(self, view_type):
        callback = getattr(self.urlconf_module, 'handler%s' % view_type, None)  # the5fire: 这里就是去获取 urls.py 中对应的配置
        if not callback:
            # No handler specified in file; use lazy import, since
            # django.conf.urls imports this file.
            from django.conf import urls
            callback = getattr(urls, 'handler%s' % view_type)
        return get_callable(callback), {}

最后

实际上花了比预计更多的时间来把完整的代码贴出来,以及明确对应的版本。在 Django 1.11 中的处理逻辑有些不同。

实际阅读时间也会比预计的久,但如果能理解这个过程,你对于Django也会有更深的进步。

阅读原文
Django视频教程

Python音频播客推荐:捕蛇者说

作者 the5fire
2019年5月5日 21:59

关注我公众号比较久的人应该都看到过我经常会推一些好的播客内容,比较遗憾的是很多中文类的技术播客更新一段时间之后就停更了,或者间歇性停更或者永久「弃坑」。但不管怎么说,很多被记载、传播的音频还是有其价值的,我们需要更多的记录着真实开发者想法、经验、牢骚的声音。

希望「捕蛇者说」能保持更新。

简单介绍下播客的几个主播:

其他的我就不多介绍了,相对于网上一水的技术类的营销文章,来自一线开发者的讨论还是很值得一听的,反正我听完后是有挺多收获的。

下面我直接把最新一期的内容贴上来,链接:https://pythonhunter.org/episodes/2

--- copy 分割线 ----

本期主持

  • laike9m
  • laixintao
  • Adam Wen
  • Manjusaka

本期提要

  • 00:00:35 嘉宾介绍
  • 00:03:15 开发中踩过的坑
  • 00:04:20 Requests UA 带来的 Github 误封问题
  • 00:08:06 单元测试遇到的坑
  • 00:11:53 非法 Cookies 引发的坑
  • 00:19:38 一个不合法的 HTTP Header
  • 00:25:01 glibc 引发的内存泄漏
  • 00:30:20 werkzeug 的 bug 复现 PR
  • 00:32:42 关于一些不好的库,文档,feature 的吐槽
  • 00:33:13 Python 的 LEGB 问题
  • 00:43:06 一些不好的库与文档, 生产环境不推荐的一些做法
  • 00:52:20 函数参数的种类
  • 00:57:11 Celery, asyncio, os 的一些槽点
  • 01:12:14 一些疑难问题排查的技巧与工具
  • 01:12:30 Py-Spy, 一个 Python 进程取样分析工具
  • 01:17:13 构造最小可复现样例
  • 01:18:30 Debug 技巧
  • 01:23:40 用 PDB 来 Debug
  • 01:26:25: pyrasite, attached 到 Python 进程的 REPL
  • 01:29:25 休息,提升 Debug 效率的方法
  • 01:30:50 能复现的 Bug 情况都是幸福的
  • 01:38:05 库与文章的推荐

播客中提到的内容

  • Github REST API v3
  • Mock
  • Python Cookie 标准库实现
  • Tornado 4.0 Cookies Parse 实现
  • RFC 7230 Section 5.4 Host
  • Fix memory leak in Rule function builder
  • Short description of the scoping rules?
  • lxml
  • Kafka Python
  • Golang Functional options for friendly APIs
  • Beautiful
  • Regular expression Denial of Service - ReDoS
  • tox
  • Netty Request Demo
  • linux环境内存分配原理
  • Buildout
  • Google Python Style Guide
  • 理解Python的UnboundLocalError(Python的作用域)
  • PEP 3102 -- Keyword-Only Arguments
  • PEP 0570 -- Positional-Only Arguments
  • Digg's v4 launch: an optimism born of necessity.
  • let me google that for you
  • celery
  • BPO-36054
  • BPO-29406
  • asyncio
  • uvloop
  • Py-Spy: A sampling profiler for Python programs.
  • Sentry
  • PDB
  • pyrasite
  • FreezeGun: Let your Python tests travel through time
  • Hidden features of Python
  • pingtop
  • Awesome Python
  • Click
  • What the f*ck Python
  • Gevent
  • PySnooper
  • Curious Course on Coroutines and Concurrency

公众号点击{阅读原文}进行收听和查看相关链接: https://pythonhunter.org/episodes/2

阅读原文
Django视频教程

知乎回答:你是如何学习Django的

作者 the5fire
2019年4月11日 21:42

还是源于知乎的一个问题:

本人是大三的学生,最近在自学django,水平算是半只脚刚刚入了门。由于本人的英文水平不怎么样,所以一直以来都是靠着一些教学视频和谷歌翻译艰难学习。刚开始什么都不会的时候进展还算快,可是最近我感觉越来越力不从心了,对于一些具体的问题始终找不到办法解决,请问知乎上的大家有过这样的经历吗?你是怎么解决的呢?

对应链接:https://www.zhihu.com/question/59426020/answer/615516853 (点赞走一走,学啥都好上手~)

简单说下我的学习方式,仅供参考。

在正式转到 Python Web 开发之前(2011年的时候)是在写 Java 和一堆其他的语言,毕竟是小公司,啥都做,有啥项目就用啥语言。 之后换了另外一家创业公司,用的 Django,怎么转呢?其实不需要太多时间,对于一个有一些 Web 开发经验的人来说。

新手阶段

刚开始阶段 步骤如下:

  • 花两三天的时间看了下《简明 Python 教程》
  • 看了下其他同事写的 Django 项目,以及一本在线中译之后的《The Django Book》(现在不推荐看这本书,版本太老了
  • 开始维护项目,看着其他人怎么写我也怎么写。 这个阶段中可以产出项目了,毕竟照猫画虎也不是多复杂,但是对于 Django 以及 Python 的掌握都很有限。

正式上手

后来加入 sohu,算是正儿八经的开始撸 Django 的官方文档,以及 admin 部分的源码(当时的业务有基于 admin 的定制)。

现在回过头来看,学习 Django 比较有效的方式还是去做实际的项目,比如 我会去撸文档、撸源码,是因为发现了项目中有很多我不能 hold 住的东西。另外,入门可能需要看书,但是掌握到一定程度之后还是以官方内容(文档和源码)为主,因为这个才是根本,也会让你在学习上产生滚雪球的效应——随着你越来越熟悉它,你能越来越快的熟悉跟它像的内容。

可以参考我之前的年度总结:

个人回忆录之一年总结(记2012.01至2013.03) | the5fire的技术博客

再往后慢慢掌握了公司中涉及到的技术栈之后,自己用 Django 重写了个人博客系统,一样的技术栈,类似的开发和部署逻辑,不一样的是,在博客系统中可以大胆的做各种尝试,比如始终保持使用最新的版本,无论是 Django 还是 Python。这样可以比在公司中更快的得到升级版本上的一些经验。 当然,再之后,还是不断的撸源码,因为当你对源码有了一定了解之后,会发现,看源码会比看文档更快和直接,尤其是现在各种先进的 IDE 提供了自动补全和跳转到定义的功能。

关于题主最后说到的问题,你可以需用先理解数据的传递流程,从用户注册,到最终写数据库,以及具体写哪些表。理解了表结构,然后在对应到 Django 的 Model 上,对于额外的用户信息,可能需要建一个 OneToOne 的字段,每次更新 User 时,同步更新下对应的 UserProfile ,这部分需要你自己来重写 User Model,参考这里:Customizing authentication in Django

最后广告:

阅读原文
Django视频教程

中奖名单|《Django企业开发实战》重印之赠书活动

作者 the5fire
2019年4月9日 23:07

铛铛铛铛~,话不多说,直接公布中奖名单,如下图:

恭喜以上中奖的读者,认真的评论值得点赞, the5fire 也始终在坚持认真的写一些原创的文章,希望大家读完本书后能部署完自己的博客,能够对 Django 掌握的更加深入。

对于喜欢写书评的朋友,欢迎写书评,投稿到本公众号(Python 程序员杂谈)。

请中奖的读者在「公众号」的「对话界面」发给我邮寄地址,与奖品完美擦肩而过的读者,也欢迎到微店或者京东支持我的这本书,从目前收到的反馈来看,还是值得一读的。另外,the5fire 计划不定期的给读者搞一些福利,这类的抽奖活动后续还会继续进行,欢迎关注。

购买地址:

https://item.jd.com/12537842.html

阅读原文
Django视频教程
❌
❌