DEV Community

David Israel for Uclusion

Posted on • Edited on

Example production Serverless files

Update: Well turns out the new provisioned concurrency feature is mostly unusable. The hidden cost of Lambda provisioned concurrency pricing explains why but unfortunately the cost is not at all hidden. Uclusion was billed $3,347.63 for 803,424,893.570 Lambda-GB-Second on Lambdas that were not costing anything under the “warmup” way. We are disputing the charge but will have to revert to trying to solve cold starts with the synthetic traffic plugin.

Uclusion uses Serverless to deploy Lambdas in AWS. There are many aspects to our configuration so this post just walks you through a basic abbreviated file. You can use Serverless documentation to understand the key words.

service: uclusion-market-api

plugins:
  - serverless-plugin-aws-alerts
custom:
  alerts:
    topics:
      alarm:
        topic: uclusion-market-api-${opt:stage, self:provider.stage}-alerts-alarm
        notifications:
          - protocol: email
            endpoint: [an email address]
    alarms:
      - functionErrors
      - functionThrottles*

*provider:
  name: aws
  runtime: python3.7
  region: us-west-2
  versionFunctions: false
  usagePlan:
    quota:
      limit: ${ssm:/usageplan/limit}
      period: DAY
    throttle:
      burstLimit: 400
      rateLimit: 100
  apiGateway:
    apiKeySourceType: AUTHORIZER
  environment:
    usersServicePrefix: uclusion-users-dev-
    marketsServicePrefix: uclusion-markets-dev-
    USER_POOL_ID: us-west-2_${ssm:/userpool/id}
    COGNITO_CLIENT_ID: ${ssm:/cognito/client/id~true}
    GLOBAL_CAPABILITY_SECRET_KEY: ${ssm:/capability/secret/key~true}
    CAPABILITY_SIGNING_ALGORITHM: HS256
    myRegion: us-west-2

package:
  exclude:
    - node_modules/**
    - .idea/**
    - env/**
    - tests/**
    - README.md
    - setup.cfg
    - package.json
    - package-lock.json

functions:
  authorizerFunc:
    provisionedConcurrency: 5
    handler: authorizers/long_capability_auth.lambda_handler
    role: arn:aws:iam::${ssm:/account/id}:role/requests/generalapi/policy/GeneralAPIRoleShared
    layers: ${file(layers.yml):${opt:stage, self:provider.stage}}
  update_investment:
    provisionedConcurrency: ${ssm:/account/provisioned}
    role: arn:aws:iam::${ssm:/account/id}:role/requests/generalapi/policy/GeneralAPIRoleShared
    handler: handlers/investment_update.update
    layers: ${file(layers.yml):${opt:stage, self:provider.stage}}
    timeout: 20
    events:
      - http:
          path: invest
          method: post
          private: true
          cors: true
          authorizer: ${file(authorizer.yml):${opt:stage, self:provider.stage}}
  remove_investment:
    provisionedConcurrency: ${ssm:/account/provisioned}
    role: arn:aws:iam::${ssm:/account/id}:role/requests/generalapi/policy/GeneralAPIRoleShared
    handler: handlers/investment_remove.delete
    layers: ${file(layers.yml):${opt:stage, self:provider.stage}}
    timeout: 20
    events:
      - http:
          path: invest/{investible_id}
          method: delete
          private: true
          cors: true
          authorizer: ${file(authorizer.yml):${opt:stage, self:provider.stage}}
  get_market:
    provisionedConcurrency: ${ssm:/account/provisioned}
    role: arn:aws:iam::${ssm:/account/id}:role/requests/generalapi/policy/GeneralAPIRoleShared
    handler: handlers/get_market.get
    layers: ${file(layers.yml):${opt:stage, self:provider.stage}}
    events:
      - http:
          path: get
          method: get
          private: true
          cors: true
          authorizer: ${file(authorizer.yml):${opt:stage, self:provider.stage}}
  update_market:
    provisionedConcurrency: ${ssm:/account/provisioned}
    role: arn:aws:iam::${ssm:/account/id}:role/requests/generalapi/policy/GeneralAPIRoleShared
    handler: handlers/market_update.update
    layers: ${file(layers.yml):${opt:stage, self:provider.stage}}
    events:
      - http:
          path: update
          method: patch
          private: true
          cors: true
          authorizer: ${file(authorizer.yml):${opt:stage, self:provider.stage}}
resources:
  Resources:
    GatewayResponseDefault4XX: *# See https://serverless.com/blog/cors-api-gateway-survival-guide
      *Type: 'AWS::ApiGateway::GatewayResponse'
      Properties:
        ResponseParameters:
          gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
          gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
        ResponseType: DEFAULT_4XX
        RestApiId:
          Ref: 'ApiGatewayRestApi'
Enter fullscreen mode Exit fullscreen mode

Here the authorizer.yml is

dev:
  name: authorizerFunc
  authorizer_type: TOKEN
  identitySource: method.request.header.Authorization
Enter fullscreen mode Exit fullscreen mode

and the layers.yml is

dev:
  - ${cf:udcommon-layer-dev.UdcommonLayerExport}
  - ${cf:ucommon-layer-dev.UcommonLayerExport}
  - ${cf:ubcommon-layer-dev.UbcommonLayerExport}
Enter fullscreen mode Exit fullscreen mode

and you use the environment section in your Lambda Python code like so

os.environ['usersServicePrefix']
Enter fullscreen mode Exit fullscreen mode

One of the more confusing points when you initially setup using AWS Lambdas is the API Gateway stage feature. So far Uclusion has not found a way to make use of that feature at all. We created an organization with three accounts — dev, stage and production and use the same Serverless yml to deploy to each of them (using CircleCI). So each environment has only one API Gateway stage.

In theory you could canary test a new version of the API in production by having a second stage but in practice the new stage shares SSM configuration (see ssm: usage above), DynamoDB, DynamoDB streams, etc. and so if the canary dies the whole site might die as well.

Another confusing aspect is the provisionedConcurrency. Any call you make to an outside static resource like os.environ[‘usersServicePrefix’] must be globally declared in your Python file — not inside a method. Otherwise instead of the call being made once when the Lambda is provisioned it will be made every time the method is called.

Also confusing with provisionedConcurrency you will see in your logs something like

Init Duration: 2285.65 ms
Enter fullscreen mode Exit fullscreen mode

when you run a Lambda. Even more confusing is that if you run that Lambda twice in a row the init duration won’t be there on the second call. I can’t say for certain what is happening but it was the same with the old warm-up plugin running every 5 minutes.

Finally make sure to thoroughly read the API Gateway limits ahead of time; I promise that will be time well spent!

Top comments (0)