Mitch Garnaat

Merge branch 'develop'

...@@ -2,3 +2,4 @@ include README.md ...@@ -2,3 +2,4 @@ include README.md
2 include LICENSE 2 include LICENSE
3 include requirements.txt 3 include requirements.txt
4 include kappa/_version 4 include kappa/_version
5 +recursive-include samples *.js *.yml *.cf *.json
......
1 +kappa
2 +=====
3 +
4 +**Kappa** is a command line tool that (hopefully) makes it easier to
5 +deploy, update, and test functions for AWS Lambda.
6 +
7 +There are quite a few steps involved in developing a Lambda function.
8 +You have to:
9 +
10 +* Write the function itself (Javascript only for now)
11 +* Create the IAM roles required by the Lambda function itself (the executing
12 +role) as well as the policy required by whoever is invoking the Lambda
13 +function (the invocation role)
14 +* Zip the function and any dependencies and upload it to AWS Lambda
15 +* Test the function with mock data
16 +* Retrieve the output of the function from CloudWatch Logs
17 +* Add an event source to the function
18 +* View the output of the live function
19 +
20 +Kappa tries to help you with some of this. The IAM roles are created
21 +in a CloudFormation template and kappa takes care of creating, updating, and
22 +deleting the CloudFormation stack. Kappa will also zip up the function and
23 +any dependencies and upload them to AWS Lambda. It also sends test data
24 +to the uploaded function and finds the related CloudWatch log stream and
25 +displays the log events. Finally, it will add the event source to turn
26 +your function on.
27 +
28 +Kappa is a command line tool. The basic command format is:
29 +
30 + kappa --config <path to config file> <command>
31 +
32 +Where ``command`` is one of:
33 +
34 +* deploy - deploy the CloudFormation template containing the IAM roles and zip the function and upload it to AWS Lambda
35 +* test - send test data to the new Lambda function
36 +* tail - display the most recent log events for the function
37 +* add-event-source - hook up an event source to your Lambda function
38 +* delete - delete the CloudFormation stack containing the IAM roles and delete the Lambda function
39 +
40 +The ``config file`` is a YAML format file containing all of the information
41 +about your Lambda function.
42 +
43 +An example project based on a Kinesis stream can be found in
44 +[samples/kinesis](https://github.com/garnaat/kappa/tree/develop/samples/kinesis).
45 +
46 +The basic workflow is:
47 +
48 +* Create your Lambda function
49 +* Create your CloudFormation template with the execution and invocation roles
50 +* Create some sample data
51 +* Create the YAML config file with all of the information
52 +* Run ``kappa --config <path-to-config> deploy`` to create roles and upload function
53 +* Run ``kappa --config <path-to-config> test`` to invoke the function with test data
54 +* Run ``kappa --config <path-to-config> tail`` to view the functions output in CloudWatch logs
55 +* Run ``kappa --config <path-to-config> add-event-source`` to hook your function up to the event source
56 +* Run ``kappa --config <path-to-config> tail`` to see more output
57 +
58 +If you have to make changes in your function or in your IAM roles, simply run
59 +``kappa deploy`` again and the changes will be uploaded as necessary.
...\ No newline at end of file ...\ No newline at end of file
...@@ -64,7 +64,7 @@ def set_debug_logger(logger_names=['kappa'], stream=None): ...@@ -64,7 +64,7 @@ def set_debug_logger(logger_names=['kappa'], stream=None):
64 @click.argument( 64 @click.argument(
65 'command', 65 'command',
66 required=True, 66 required=True,
67 - type=click.Choice(['deploy', 'test', 'tail', 'delete']) 67 + type=click.Choice(['deploy', 'test', 'tail', 'add-event-source', 'delete'])
68 ) 68 )
69 def main(config=None, debug=False, dryrun=False, command=None): 69 def main(config=None, debug=False, dryrun=False, command=None):
70 if debug: 70 if debug:
...@@ -79,6 +79,8 @@ def main(config=None, debug=False, dryrun=False, command=None): ...@@ -79,6 +79,8 @@ def main(config=None, debug=False, dryrun=False, command=None):
79 kappa.tail() 79 kappa.tail()
80 elif command == 'delete': 80 elif command == 'delete':
81 kappa.delete() 81 kappa.delete()
82 + elif command == 'add-event-source':
83 + kappa.add_event_source()
82 84
83 85
84 if __name__ == '__main__': 86 if __name__ == '__main__':
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
14 import logging 14 import logging
15 import os 15 import os
16 import zipfile 16 import zipfile
17 +import time
17 18
18 import botocore.session 19 import botocore.session
19 from botocore.exceptions import ClientError 20 from botocore.exceptions import ClientError
...@@ -23,6 +24,8 @@ LOG = logging.getLogger(__name__) ...@@ -23,6 +24,8 @@ LOG = logging.getLogger(__name__)
23 24
24 class Kappa(object): 25 class Kappa(object):
25 26
27 + completed_states = ('CREATE_COMPLETE', 'UPDATE_COMPLETE')
28 +
26 def __init__(self, config): 29 def __init__(self, config):
27 self.config = config 30 self.config = config
28 self.session = botocore.session.get_session() 31 self.session = botocore.session.get_session()
...@@ -52,6 +55,14 @@ class Kappa(object): ...@@ -52,6 +55,14 @@ class Kappa(object):
52 response = cfn.create_stack( 55 response = cfn.create_stack(
53 StackName=stack_name, TemplateBody=template_body, 56 StackName=stack_name, TemplateBody=template_body,
54 Capabilities=['CAPABILITY_IAM']) 57 Capabilities=['CAPABILITY_IAM'])
58 + done = False
59 + while not done:
60 + response = cfn.describe_stacks(StackName=stack_name)
61 + status = response['Stacks'][0]['StackStatus']
62 + LOG.debug('Stack status is: %s', status)
63 + if status in self.completed_states:
64 + done = True
65 + time.sleep(1)
55 66
56 def get_role_arn(self, role_name): 67 def get_role_arn(self, role_name):
57 role_arn = None 68 role_arn = None
...@@ -157,6 +168,20 @@ class Kappa(object): ...@@ -157,6 +168,20 @@ class Kappa(object):
157 for log_event in response['events']: 168 for log_event in response['events']:
158 print('%s: %s' % (log_event['timestamp'], log_event['message'])) 169 print('%s: %s' % (log_event['timestamp'], log_event['message']))
159 170
171 + def add_event_source(self):
172 + lambda_svc = self.session.create_client('lambda', self.region)
173 + try:
174 + invoke_role = self.get_role_arn(
175 + self.config['cloudformation']['invoke_role'])
176 + response = lambda_svc.add_event_source(
177 + FunctionName=self.config['lambda']['name'],
178 + Role=invoke_role,
179 + EventSource=self.config['lambda']['event_source'],
180 + BatchSize=self.config['lambda'].get('batch_size', 100))
181 + LOG.debug(response)
182 + except Exception:
183 + LOG.exception('Unable to add event source')
184 +
160 def deploy(self): 185 def deploy(self):
161 self.create_update_roles( 186 self.create_update_roles(
162 self.config['cloudformation']['stack_name'], 187 self.config['cloudformation']['stack_name'],
......
1 +console.log('Loading event');
2 +exports.handler = function(event, context) {
3 + console.log(JSON.stringify(event, null, ' '));
4 + for(i = 0; i < event.Records.length; ++i) {
5 + encodedPayload = event.Records[i].kinesis.data;
6 + payload = new Buffer(encodedPayload, 'base64').toString('ascii');
7 + console.log("Decoded payload: " + payload);
8 + }
9 + context.done(null, "Hello World"); // SUCCESS with message
10 +};
1 +---
2 +profile: personal
3 +region: us-east-1
4 +cloudformation:
5 + template: roles.cf
6 + stack_name: TestKinesis
7 + exec_role: ExecRole
8 + invoke_role: InvokeRole
9 +lambda:
10 + name: KinesisSample
11 + zipfile_name: KinesisSample.zip
12 + description: Testing Kinesis Lambda handler
13 + path: ProcessKinesisRecords.js
14 + handler: ProcessKinesisRecords.handler
15 + runtime: nodejs
16 + memory_size: 128
17 + timeout: 3
18 + mode: event
19 + event_source: arn:aws:kinesis:us-east-1:084307701560:stream/lambdastream
20 + test_data: input.json
21 +
...\ No newline at end of file ...\ No newline at end of file
1 +{
2 + "Records": [
3 + {
4 + "kinesis": {
5 + "partitionKey": "partitionKey-3",
6 + "kinesisSchemaVersion": "1.0",
7 + "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=",
8 + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961"
9 + },
10 + "eventSource": "aws:kinesis",
11 + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961",
12 + "invokeIdentityArn": "arn:aws:iam::059493405231:role/testLEBRole",
13 + "eventVersion": "1.0",
14 + "eventName": "aws:kinesis:record",
15 + "eventSourceARN": "arn:aws:kinesis:us-east-1:35667example:stream/examplestream",
16 + "awsRegion": "us-east-1"
17 + }
18 + ]
19 +}
1 +{
2 + "AWSTemplateFormatVersion": "2010-09-09",
3 + "Resources": {
4 + "ExecRole": {
5 + "Type": "AWS::IAM::Role",
6 + "Properties": {
7 + "AssumeRolePolicyDocument": {
8 + "Version" : "2012-10-17",
9 + "Statement": [ {
10 + "Effect": "Allow",
11 + "Principal": {
12 + "Service": [ "lambda.amazonaws.com" ]
13 + },
14 + "Action": [ "sts:AssumeRole" ]
15 + } ]
16 + }
17 + }
18 + },
19 + "ExecRolePolicies": {
20 + "Type": "AWS::IAM::Policy",
21 + "Properties": {
22 + "PolicyName": "ExecRolePolicy",
23 + "PolicyDocument": {
24 + "Version" : "2012-10-17",
25 + "Statement": [ {
26 + "Effect": "Allow",
27 + "Action": [
28 + "logs:*"
29 + ],
30 + "Resource": "arn:aws:logs:*:*:*"
31 + } ]
32 + },
33 + "Roles": [ { "Ref": "ExecRole" } ]
34 + }
35 + },
36 + "InvokeRole": {
37 + "Type": "AWS::IAM::Role",
38 + "Properties": {
39 + "AssumeRolePolicyDocument": {
40 + "Version" : "2012-10-17",
41 + "Statement": [ {
42 + "Effect": "Allow",
43 + "Principal": {
44 + "Service": [ "s3.amazonaws.com" ]
45 + },
46 + "Action": [ "sts:AssumeRole" ],
47 + "Condition": {
48 + "ArnLike": {
49 + "sts:ExternalId": "arn:aws:s3:::*"
50 + }
51 + }
52 + },
53 + {
54 + "Effect": "Allow",
55 + "Principal": {
56 + "Service": "lambda.amazonaws.com"
57 + },
58 + "Action": "sts:AssumeRole"
59 + } ]
60 + }
61 + }
62 + },
63 + "InvokeRolePolicies": {
64 + "Type": "AWS::IAM::Policy",
65 + "Properties": {
66 + "PolicyName": "ExecRolePolicy",
67 + "PolicyDocument": {
68 + "Version" : "2012-10-17",
69 + "Statement": [
70 + {
71 + "Effect":"Allow",
72 + "Action":[
73 + "lambda:InvokeFunction",
74 + "kinesis:GetRecords",
75 + "kinesis:GetShardIterator",
76 + "kinesis:DescribeStream",
77 + "kinesis:ListStreams"
78 + ],
79 + "Resource":[
80 + "*"
81 + ]
82 + }
83 + ]
84 + },
85 + "Roles": [ { "Ref": "InvokeRole" } ]
86 + }
87 + }
88 + }
89 +}