Skip to content

Commit

Permalink
Start sketching out lambda support
Browse files Browse the repository at this point in the history
  • Loading branch information
Jc2k committed Dec 12, 2015
1 parent 9b60800 commit 09107f3
Show file tree
Hide file tree
Showing 17 changed files with 605 additions and 12 deletions.
88 changes: 88 additions & 0 deletions docs/config/aws/lambda.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
Lambda
======

.. module:: touchdown.aws.lambda_
:synopsis: Executing code units on AWS infrastructure without managing servers


Function
--------

.. class:: Function

You can register a lambda function against an Amazon account resource::

def hello_world(event, context):
print event

aws.add_lambda_function(
name = 'myfunction',
role = aws.add_role(
name='myrole',
#..... snip ....,
),
code=hello_world,
handler="main.hello_world",
)

.. attribute:: name

The name for the function, up to 64 characters.

.. attribute:: description

A description for the function. This is shown in the AWS console and API but is not used by lambda itself.

.. attribute:: role

A :class:`~touchdown.aws.iam.Role` resource.

The IAM role that Lambda assumes when it executes your function to access any other Amazon Web Services (AWS) resources.

.. attribute:: code

A python callable. For example::

def hello_world(event, context):
print event

aws.add_lambda_function(
name='hello_world',
code=hello_world,
handler='main.hello_world'
...
)

It must take 2 arguments only - ``event`` and ``context``.

This is intended for proof of concept demos when first starting out with lambda - there is no mechanism to ship dependencies of this function, it is literally the output of `inspect.getsource()` that is uploaded.

.. attribute:: code_from_bytes

.. attribute:: code_from_s3

An S3 :py:class:`~touchdown.aws.s3.File`.

A new version of the lambda function is published when touchdown detects that the date/time stamp of this file is newer than the last modified stamp on the lambda function.

.. attribute:: handler

The entry point to call.

For the ``python2.7`` runtime with a ``shrink_image.py`` module containing a function called ``handler`` the handler would be ``shrink_image.handler``.

For the ``node`` runtime with a ``CreateThumbnail.js`` module containing an exported function called ``handler``, the handler is ``CreateThumbnail.handler``.

For the ``java8`` runtime, this would be something like ``package.class-name.handler`` or just ``package.class-name``.

.. attribute:: timeout

An integer. The number of seconds (between 1 and 300) that a lambda function is allowed to execute for before it is interrupted. The default is 3 seconds.

.. attribute:: memory

The amount of RAM your lambda function is given. The amount of CPU is assigned based on this as well - more RAM means more CPU is allocated.

The default value is 128mb, which is also the minimum. Can assign up to 1536mb.

.. attribute:: publish
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Resources
:doc:`Encryption Key Management <config/aws/kms>` |
:doc:`Key Value Stores <config/aws/elasticache>` |
:doc:`Identity & Access Management <config/aws/iam>` |
:doc:`Lambda zero-admin compute <config/aws/lambda>` |
:doc:`Load Balancing <config/aws/elb>` |
:doc:`Monitoring <config/aws/cloudwatch>` |
:doc:`Networking <config/aws/vpc>` |
Expand Down
1 change: 1 addition & 0 deletions docs/tutorial/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ Tutorials
.. toctree::

hello_world
s3_events_with_lambda
django
61 changes: 61 additions & 0 deletions docs/tutorial/s3_events_with_lambda.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
Handling S3 events with lambda functions
========================================

Suppose you store incoming media (such as .jpg or .png) in an incoming bucket and want to resize it into an output bucket. In this walkthrough we will use AWS Lambda to perform the transformation automatically - triggered by S3 ``object-created`` events.

.. warning::

This example assumes 2 separate buckets are used. Attempting to use one bucket will result in recursion.

::

aws = workspace.add_aws(
access_key_id='AKI.....A',
secret_access_key='dfsdfsdgrtjhwluy52i3u5ywjedhfkjshdlfjhlkwjhdf',
region='eu-west-1',
)

resize_role = aws.add_role(
name="resize-role",
policies={
"logs": {
"Statement": [{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": "arn:aws:logs:*:*:*"
}]
}
},
assume_role_policy={
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}],
},
)

resize = aws.add_lambda_function(
name="resize-media",
role=role,
handler="main.resize_handler",
)

incoming = aws.add_bucket(
name="incoming",
notify_lambda=[{
"name": "resize",
"events": ["s3:ObjectCreated:*"],
"function": resize,
}],
)

resized = aws.add_bucket(
name="resized",
)
2 changes: 2 additions & 0 deletions touchdown/aws/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
elb,
iam,
kms,
lambda_,
logs,
rds,
route53,
Expand All @@ -48,6 +49,7 @@
'elb',
'iam',
'kms',
'lambda_',
'logs',
'rds',
'route53',
Expand Down
2 changes: 2 additions & 0 deletions touchdown/aws/iam/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class Role(Resource):

resource_name = "role"

arn = argument.Output("Arn")

name = argument.String(field="RoleName")
path = argument.String(field='Path')
assume_role_policy = argument.Dict(field="AssumeRolePolicyDocument", serializer=serializers.Json())
Expand Down
24 changes: 24 additions & 0 deletions touchdown/aws/lambda_/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2015 Isotoma Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from .function import Function
from .permission import Permission
from .s3 import S3LambdaNotification


__all__ = [
'Function',
'Permission',
'S3LambdaNotification',
]
155 changes: 155 additions & 0 deletions touchdown/aws/lambda_/function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Copyright 2015 Isotoma Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
import hashlib
import inspect
import zipfile
from six import StringIO

from touchdown.core.resource import Resource
from touchdown.core.plan import Plan, Present, XOR
from touchdown.core import argument, serializers
from touchdown.aws.iam import Role
from ..account import BaseAccount
from ..common import SimpleDescribe, SimpleApply, SimpleDestroy


class FunctionSerializer(serializers.Formatter):

MAIN_PY = zipfile.ZipInfo(
'main.py',
date_time=(2004, 7, 6, 0, 0, 0),
)

def render(self, runner, func):
buf = StringIO()
zf = zipfile.ZipFile(
buf,
mode='w',
compression=zipfile.ZIP_DEFLATED
)
zf.writestr(self.MAIN_PY, inspect.getsource(func))
zf.close()
return buf.getvalue()


class Function(Resource):

resource_name = "lambda_function"

name = argument.String(field="FunctionName", min=1, max=140)
description = argument.String(name="Description")
timeout = argument.Integer(name="Timeout", default=3)
runtime = argument.String(
field="Runtime",
default='python2.7',
choices=['nodejs', 'java8', 'python2.7'],
)
handler = argument.String(field="Handler", min=0, max=128)
role = argument.Resource(
Role,
field="Role",
serializer=serializers.Property("Arn"),
)

code = argument.Function(
field="Code",
serializer=serializers.Dict(
ZipFile=FunctionSerializer(),
)
)

code_from_bytes = argument.String(
field="Code",
serializer=serializers.Dict(
ZipFile=serializers.String(),
)
)

code_from_s3 = argument.Resource(
"touchdown.aws.s3.File",
field="Code",
serializer=serializers.Dict(
S3Bucket=serializers.Property("Bucket"),
S3Key=serializers.Property("Key"),
# S3ObjectionVersion=,
)
)

memory = argument.Integer(name="MemorySize", default=128, min=128, max=1536)
publish = argument.Boolean(name="Publish", default=True)

account = argument.Resource(BaseAccount)


class Describe(SimpleDescribe, Plan):

resource = Function
service_name = 'lambda'
describe_action = "get_function_configuration"
describe_notfound_exception = "ResourceNotFoundException"
describe_envelope = "@"
key = 'FunctionName'


class Apply(SimpleApply, Describe):

create_action = "create_function"
create_envelope = "@"
update_action = "update_function_configuration"

signature = (
Present('name'),
Present('role'),
Present('handler'),
# Runtime must be present - but default is valid
# Present('runtime'),
XOR(
Present('code'),
Present('code_from_bytes'),
Present('code_from_s3'),
),
)

def update_object(self):
update_code = False
if self.object:
serialized = serializers.Resource().render(self.runner, self.resource)
if 'ZipFile' in serialized['Code']:
hasher = hashlib.sha256(serialized['Code']['ZipFile'])
digest = base64.b64encode(hasher.digest()).decode('utf-8')
if self.object['CodeSha256'] != digest:
update_code = True
elif 'S3Bucket' in serialized['Code']:
f = self.get_plan(self.resource.code_from_s3)
if f.object['LastModified'] > self.object['LastModified']:
update_code = True

if update_code:
kwargs = {
"FunctionName": self.resource.name,
"Publish": True,
}
kwargs.update(serialized['Code'])
yield self.generic_action(
"Update function code",
self.client.update_function_code,
**kwargs
)


class Destroy(SimpleDestroy, Describe):

destroy_action = "delete_function"
Loading

0 comments on commit 09107f3

Please sign in to comment.