Notes On Cloudformation

Table of Contents

References

Synopsis

# Step 1. Install serverless globally
$ npm install serverless -g

To workaround error:
sudo npm install -g try-thread-sleep
sudo npm install -g serverless --ignore-scripts spawn-sync

# Step 2. Create a serverless function
$ serverless create --template hello-world

# Step 3. deploy to cloud provider
$ serverless deploy

# Your function is deployed!
$ http://xyz.amazonaws.com/hello-world

Examples:

t-lambda.yml  (service: t-lambda)
Cloudformation Stack Name:  t-lambda-dev
API URL https://dbp1ufswd1.execute-api.us-east-1.amazonaws.com/dev/
The API Name is called "t-lambda-dev" with physical ID: dbp1ufswd1

Various Resources Created::

------------------------------------------------------------------------------
Resource-Type                Physical-ID           Comments
------------------------------------------------------------------------------

AWS::ApiGateway::RestApi     dbp1ufswd1            Single Rest API per stack.
                                                   event.requestContext.apiId

AWS::Lambda::Function        t-lambda-dev-hello    <stack-name>-<function>

AWS::Lambda::Function        t-lambda-dev-pubhello <stack-name>-<function>

AWS::ApiGateway::Resource    n1wvn3                ApiGatewayResourceHello 
                                                   This is /hello path resource

AWS::ApiGateway::Resource    p4udjd                ApiGatewayResourcePubhello  
                                                   This is /pubhello path resource
                                                   event.requestContext.resourceId
                                                   resourcePath is /pubhello.

AWS::Logs::LogGroup          /aws/lambda/t-lambda-dev-hello 
                                                   One LogGroup per Lambda Function
AWS::IAM::Role               t-lambda-dev-us-east-1-lambdaRole   
                 # Note: Single lambda exec Role for all functions by default.

AWS::S3::Bucket            t-lambda-dev-serverlessdeploymentbucket-1pa32il0ys7t3
                 # Single deployment S3 bucket.

Other Abstract Artifacts:
AWS::Lambda::Permission    # One permission per function.
                 # Says who can invoke this lambda function.
                 # Used in statement-id within Function Policy permissions.

AWS::ApiGateway::Deployment    # 1:1 relation with AWS::ApiGateway::RestApi  
                 #  The need to have this resource is unclear.
                 #  Can you keep RestAPI but still undeploy it ? Not sure.
AWS::version             # One per function.

Lambda Function (Permission) Policy

Describes who can invoke this function ...

{
 ...
 "Statement": [
  {
   "Sid": "t-lambda-d
   "Effect": "Allow",
   "Principal": { "Service": "apigateway.amazonaws.com" },
   "Action": "lambda:InvokeFunction",
   "Resource": "arn:aws:lambda:us-east-1:027212312845:function:t-lambda-dev-hello",
   # To restrict API principal to invoke lambda only when API URL is like:
   #   https://dbp1ufswd1.execute-api.us-east-1.amazonaws.com/dev/
   "Condition": {
     "ArnLike": {
       "AWS:SourceArn": "arn:aws:execute-api:us-east-1:027212312845:dbp1ufswd1/*/*"
       # event.requestContext.identity.userarn 
       # arn:aws:execute-api:us-east-1:027212312845:dbp1ufswd1/*/GET/hello
     }
   }
  },
  ....
   // Also if principal : sns.amazonaws.com
   // Restrict source-arn to be specific topic:
   //            arn:aws:sns:us-east-1:123456789012:my-topic
   ...
}

For Amazon S3, the source is a bucket whose ARN doesn't have an account ID in it. It's possible that you could delete the bucket and another account could create a bucket with the same name. Use the account-id option to ensure that only resources in your account can invoke the function. :

$ aws lambda add-permission 
    --function-name my-function 
    --action lambda:InvokeFunction 
    --statement-id s3-account
    --principal s3.amazonaws.com 
    --source-arn arn:aws:s3:::my-bucket-123456 
    --source-account 123456789012

To grant access to another account root user:

aws lambda add-permission --function-name my-function:prod 
   --statement-id xaccount --action lambda:InvokeFunction \
   --principal 210987654321 --output text

You can remove part of the policy permission using satement-id option :

$ aws lambda get-policy --function-name my-function:PROD
             # policy contains permissions each with one statement id.
$ aws lambda remove-permission --function-name my-function --statement-id sns

ToDO: Create S3 bucket through API and see who is listed as owner of that bucket.

Deploy function changes

serverless deploy function --function helloWorld
serverless deploy function --function helloWorld --stage dev --region us-east-1
# Only update config changes ...
serverless deploy function --function helloWorld --update-config

Allow API Gateway to log

First get your API Gateway account properties to see if cloudwatchRoleArn is present :

$  aws apigateway get-account

{
    "apiKeyVersion": "4",
    "throttleSettings": {
        "rateLimit": 10000.0,
        "burstLimit": 5000
    },
    "features": [ "UsagePlans" ]
}

You need something like below in your apigateway account settings:

"cloudwatchRoleArn": 
      (your own Role )
   "arn:aws:iam::123412341234:role/APIGatewayToCloudWatchLogsRole"  
   with Amazon managed policy:
    "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"

But make sure that role has trusted entity set to :

- apigateway.amazonaws.com

serverless Framework

  • Single serverless.yml file is for single "Service" e.g. myService myBookings
# See Also:
# https://lorenstewart.me/2017/09/18/serverless-framework-terminal-commands/

sls create --template aws-nodejs
sls deploy -s dev                    # -s is --stage
sls remove -s dev

###   Show Info
sls info   -s dev  -v  (verbose)

###   Monitor Logs ....
sls logs -f my_function              # Default -s is for --stage dev
sls logs -f my_function -s production -r us-west-2
sls logs -f my_function --startTime 30m
sls logs -f my_function -t           # See live stream of logs

###  Local Invocation ...
sls invoke local -f my_function
sls invoke local -f my_function -d 'Hello!'   # Body contains 'Hello!'
sls invoke local -f my_function -d '{"productName": 'Thing', "price": 12.99}'
sls invoke -s production -f my_function -d '{...json-data...}'

### Local Dev Plugins
npm install --save-dev serverless-offline

         # serverless.yml 
         provider:
           name: aws
           runtime: nodejs6.10
           stage: qa

         plugins:
             - serverless-offline

###  Start local API server
serverless offline start --port 3333     # Default port is 3000

serverless plugin install -n serverless-offline

### Login
sls login
sls run       # Run event gateway and emulator

### Dir structure

|-- mailman
|-- README.md
|-- frontend
`-- services


See https://github.com/serverless/event-gateway-example

Examples

sls create --help :

// Available Templates for sls create -t template_name 

"aws-nodejs",
"aws-nodejs-typescript",
"aws-nodejs-ecma-script",
"aws-python",
"aws-python3",
"aws-java-maven",
"fn-nodejs", 
"google-nodejs", 
"google-python", 
"hello-world"

aws-node-fetch-file-and-store-in-s3/
aws-node-env-variables/
aws-node-dynamic-image-resizer/
aws-node-dynamodb-backup/
aws-node-recursive-function/
aws-node-rekognition-analysis-s3-image/
aws-node-auth0-cognito-custom-authorizers-api/
aws-node-auth0-custom-authorizers-api/
aws-node-rest-api-mongodb/
aws-node-rest-api-with-dynamodb/
aws-node-scheduled-cron/
aws-node-serve-dynamic-html-via-http-endpoint/
aws-node-simple-http-endpoint/
aws-node-single-page-app-via-cloudfront/
aws-node-stripe-integration/  (Payment gateway)
aws-node-text-analysis-via-sns-post-processing/
aws-node-twilio-send-text-message/
aws-python-rest-api-with-dynamodb/
aws-python-scheduled-cron/
aws-python-simple-http-endpoint/

Notes

At API Integration Console:

Token Source: method.request.header.Authorization    (Header name)

serverless.yml recipies

Keep env in separate file

# serverless.yml file ...
environment: ${file(env.yml):${self:provider.stage}}

# env.yml file ...
dev:
   EMAIL_POP3_HOST: “pop.gmail.com”
   EMAIL_POP3_USERNAME: “me@gmail.com”

# The env file could have .json extension as well .. e.g. env.json
{
   "dev" : { "EMAIN_POP3_HOST" : "pop.gmail.com" , ... }
}

# You can directly refer to nested values in files.
# Immediately after file you should have ":" else all "." only.

environment:
  DB_NAME: ${file(config.json):${self:provider.stage}.database}

Invoke function locally

serverless invoke local -f getProduct [ -p example.json ]

# example.json -- stringified json
{
   "body": "{ \"somekey\" : \"somevalue\" }" 
}

Accessing Arn

# Assign unique IAM role to Function.
functions:
  usersCreate: # A Function
    handler: users.create
    # role: arn:aws:iam::XXXXXX:role/role # IAM
    role: Fn::GetAtt:[MyLocalRoleInResources, Arn]
    # You can also just refer to the role !!!
    role:  MyLocalRoleInResources

If you want to get Arn of the function:

Define Role with inline policies

Global roles defined under provider is applicable to all functions. These roles can not be addressed from other places using Ref: or Fn::GetAtt: and such functions :

provider:

  iamRoleStatements:
    - Effect: Allow
      Action:
        - sqs:SendMessage
     Resource:
       Fn::GetAtt: [ MessageQueue, Arn ]

  iamManagedPolicies:
    - 'some:aws:arn:xxx:*:*'

Note that Amazon predefined iamManagedPolicies are merged into the default service wide generated Role. You can also define provider:role directly :

provider:
  role: ....
  # iamRoleStatements and iamManagedPolicies are ignored.

API Gateway Resource Policy for IP whitelisting

  • API Gateway resource policy is declared as Provider:resourcePolicy
provider:
  name: aws
  runtime: nodejs8.10
  resourcePolicy: ${self:custom.resourcePolicies}

custom:
  resourcePolicies:
      - Effect: Allow
        Principal: "*"
        Action: execute-api:Invoke
        Resource:
          - execute-api:/*/*/*
        Condition:
          IpAddress:
           aws:SourceIp: ${self:custom.ipWhitelist}
  ipWhitelist:
    - "123.123.123.123/24"

Using Ref: right way

  • You can use something like: Ref: ServerlessDeploymentBucket

Using Fn::Join right way

provider:
  iamRoleStatements: 
    - Effect: 'Allow'
      Action:
        - 's3:ListBucket'
      Resource:
        Fn::Join:
          - ''
          - - 'arn:aws:s3:::'
            - Ref: ServerlessDeploymentBucket

Accessing Function ARN

serverless.xml does not directly support accessing lambda function ARN. However, since the arn follows this format, you can use this :

provider:
  environment:
     AWS_ACCOUNT: 1234567890 # use your own AWS ACCOUNT number here

     # define the ARN of the function that you want to invoke
     FUNCTION_ARN: "arn:aws:lambda:${self:provider.region}:
            ${self:provider.environment.AWS_ACCOUNT}:
            function:${self:service}-
                     ${self:provider.stage}-lambdaTwo"  

From within inline policy you can refer like this:

Statement:
 - Effect: "Allow"
   Action:
     - "lambda:InvokeFunction"
   Resource: "${self:provider.environment.FUNCTION_ARN}"

Access Cloudformation functions from serverless.yml

  • Using #{MyResource} to be rewritten to ${MyResource}, which is roughly equivalent to {"Ref": "MyResource"}
  • Using #{MyResource.Arn} to be rewritten to ${MyResource.Arn}, which is roughly equivalent to {"Fn::GetAtt": ["MyResource", "Arn"]}
plugins:
  - serverless-pseudo-parameters


# You can now use #{AWS::AccountId}, #{AWS::Region}, etc. 
# Proceses this with Fn::Sub CloudFormation function.

# This works now:  #{myAwesomeResource}, 
# This also works: #{myAwesomeResource.property}

Define individual Roles for functions

plugins:
  - serverless-iam-roles-per-function

# Define iamRoleStatements definitions at the function level:

functions:
  func1:
    handler: handler.get
    # Optional custom role name vs default generated one
    iamRoleStatementsName: my-custom-role-name 
    iamRoleStatements:
      - Effect: "Allow"        
        Action:
          - dynamodb:GetItem        
        Resource: "arn:aws:dynamodb:us-east-1:*:table/mytable"

Create RDS Resource

provider:
  environment:
    DB_ADDR:
      Fn::GetAtt:
        - DBCluster
        - Endpoint.Address

resources:
  Resources: 
    DBCluster:
      Type: AWS::RDS::DBCluster
      Properties:
        Engine: aurora
        ...

Control the output

If you want to see if the output produces what you want .. try...

provider:
  environment:
     myenvvar :  ${someResource}

resources:
  Resources:
    someResource:
      Type: AWS::...

Outputs:
  UsersTableArn:
    Description: The ARN for the User's Table.
    Value: 'Fn::GetAtt:[usersTable, Arn]'

# Curretly the default output is 

FunctionQualifiedArn (one for each function)
ServiceEndpoint
ServerlessDeploymentBucketName

$ sls print

Create Role as Resource

resources:
  Resources:
    myCustRole0:
      Type: AWS::IAM::Role
      Properties:       
        Path: /my/cust/path/
        RoleName: MyCustRole0
        AssumeRolePolicyDocument: # Define Trusted Entities.
          Version: '2017'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        Policies:
          - PolicyName: myPolicyName
            PolicyDocument:
              Version: '2017'
              Statement:
                 - Effect: Allow
                   Action:
                     - logs:CreateLogGroup
                       ...

JSON to YML Tips

YML is a superset of JSON.

# flow style               # block style
{ foo: 1, bar: 2 }         foo: 1 
                           bar: 2 

[ foo, bar ]               - foo 
                           - bar      

{"AAPL": [                       AAPL:                 
   {                               - shares: -75.088   
     "shares": -75.088,              date: 11/27/2015  
     "date": "11/27/2015"          - shares: 75.088    
   },                                date: 11/26/2015  
   {
     "shares": 75.088,
     "date": "11/26/2015"
   },
]}

Minimal Default Resources Created from serverless.yml