diff --git a/README.md b/README.md index 69a5a01..bb731eb 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,38 @@ # terraform-aws-lambda -terraform module for provisioning a lambda function +This repo contains Terraform modules to manage Lamdbas: -For most scenarios I would reccomend using the "create_empty_function" argument, rather than using terraform to deploy the function code. Once all infrastructure, functions, and permisisons have been provisioned using terraform, you should use your CI/CD tooling to deploy the function code, typically with and **aws lambda update** command. +| Directory | Module Description | +| ------------------ | -------------------------------------------------- | +| lambda_function/ | Lambda Function and IAM, Trigger, and CI resources | +| lambda_layer/ | Lambda Layer and CI resources | +These modules are primarily designed to deploy Lambda functions and layers with _placeholder_ code and then use an +external CI/CD process to manage the function and layer code independently of Terraform. It is also possible to point +the CI/CD process to a specific feature branch using the git_branch variable. By default, this value is set to *master*. -# Arguments -Many of the module arguments map directly to the aws_lambda_function resource arguments: +You can optionally provide a GitHub repo containing your function or layer code and the modules will create a simple +CodeBuild job to deploy it. + +## Arguments + +### Common + +| argument | Description | Default | +| ------------------------- | --------------------------------------------------------------------------| ------------ | +| github_url | GitHub URL of function or layer code. Enables CodeBuild. Assumes buildspec.yml at root of repo. Requires github_token_ssm_param | "" | +| codebuild_credential_arn | AWS Codebuild source credential for accessing github | "" | +| build_timeout | Codebuild Timeout in minutes. | "60" | + +### lambda_function +Many of the module arguments map directly to the [aws_lambda_function](https://www.terraform.io/docs/providers/aws/r/lambda_function.html) resource arguments: * function_name +* git_branch * filename * description * runtime * handler * timeout +* layers * memory_size * environment_variables * tags @@ -19,17 +40,49 @@ Many of the module arguments map directly to the aws_lambda_function resource ar * reserved_concurrent_executions * publish -Additional arguments are: -* **create_empty_function** - (Required) (bool) - Create an empty lambda function without the actual code if set to true -* **policies** - (Required) (list) - The module automatically creates a base IAM role for each lambda, This is a list of statement policies to add to that role. The contents are converted to json using the jsonencode() function. -* **permissions** - (Optional) (list) - A list of external resources which can invoke the lambda function such as s3 bucket / sns topic. Properties are: - * statement_id - * action - * principal - * source_arn +Additional arguments: + +| argument | Description | Default | +| ------------------------- | --------------------------------------------------------------------------| ------------ | +| create_empty_function | Create an empty lambda function without the actual code if set to true | True | +| policies | List of statement policies to add to module-manageg Lambda IAM role role. | [] | +| permissions | map of external resources which can invoke the lambda function | { enabled = false } | + +### lambda_layer +Many of the module arguments map directly to the [aws_lambda_layer_version](https://www.terraform.io/docs/providers/aws/r/lambda_layer_version.html) resource arguments: +* layer_name +* filename +* description +* runtime + + +Additional arguments: +| argument | Description | Default | +| ------------------------- | --------------------------------------------------------------------------| ------------ | +| create_empty_layer | Create an empty lambda layer without the actual code if set to true | True | +| codebuild_image | Specify Codebuild's [image](https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html) | "aws/codebuild/standard:1.0" | +| privileged_mode | Run the docker container with [privilege](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities) | False | +| codebuild_can_run_integration_test | Specifies whether or not codebuild job can invoke lambda function and is passed through to the job as an env variable (run_integration_test) | False -# Event trigger arguments +# CodeBuild + +This module will optionally create a CodeBuild job and trigger webhook to deploy your Lambda function or layer from a +GitHub repository. + +To enable creation of a CodeBuild job you must: + * Supply the github_url module argument + * Import a GitHub credential using [awscli](https://docs.aws.amazon.com/cli/latest/reference/codebuild/import-source-credentials.html) + or [Terraform](https://www.terraform.io/docs/providers/aws/r/codebuild_source_credential.html). + This credential must have admin access to your repository to create the webhook. + +--- + > **_NOTE:_** At the time of this writing, [each AWS account is limited to one GitHub CodeBuild credential](https://forums.aws.amazon.com/thread.jspa?threadID=308688&tstart=0). + > + > The module will try to construct the ARN of the CodeBuild credential as arn:aws:codebuild:::token/github. You can optionally override this using the module's codebuild_credential_arn argument. +--- + +# Function Event trigger arguments ## SNS topic trigger * **sns_topic_subscription** (Optional) (map) - The SNS topic ARN which trigger the lambda function` @@ -49,9 +102,11 @@ In addition to the trigger, make sure you * Add sufficient permissions to the lambda role to interact with s3 (E.g s3:GetObject) * Add the source resource has permissions to invoke the lambda (see **permissions** argument) -* **bucket_trigger** - (Optional) (map) - Configures the lambda function to trigger on s3 bucket ObjectCreated events. Has two properties: +* **bucket_trigger** - (Optional) (map) - Configures the lambda function to trigger on s3 bucket ObjectCreated events: * enabled (bool) - true | false * bucket (string) - The bucket name only (Not the full bucket arn!) + * filter_prefix (string) - Only trigger for objects with this prefix (must be "" if no filter) + * filter_suffix (string) - Only trigger for objects with this suffix (must be "" if no filter) ## SQS trigger @@ -66,3 +121,19 @@ Ensure you add the following permissions to the lambda role * event_source_arn (string) - arn of the event source * batch_size (int) - The largest number of records that Lambda will retrieve from your event source at the time of invocation + + +[]: https://www.terraform.io/docs/providers/aws/r/lambda_function.html + +## Codebuild and Integration Testing + +If invoking this module within an environment where Integration testing makes sense as part of CI, by setting the "codebuild_can_run_integration_test" argument to true + * The codebuild job that accompanies lambda ci is now able to invoke the lambda function + * The codebuild job will know if it's appropriate to perform integration testing in the environment it's running in according to env variable "run_integration_test" + +For an example implementation of a lambda-codebuild job setup to conditionally run integration tests see this buildspec.yml excerpt: + + if [ "$run_integration_test" = true ]; then + aws lambda wait function-updated --function-name $lambda_name; + aws lambda invoke --function-name $lambda_name --payload file://tests/testEvent.json response.json | jq -e 'has("FunctionError")|not'; + fi \ No newline at end of file diff --git a/examples/lambda-s3-bucket-event-trigger/main.tf b/examples/lambda-s3-bucket-event-trigger/main.tf index 94e8d5c..a2945c4 100644 --- a/examples/lambda-s3-bucket-event-trigger/main.tf +++ b/examples/lambda-s3-bucket-event-trigger/main.tf @@ -37,6 +37,8 @@ module "lambda_s3_trigger" { bucket_trigger = { enabled = true bucket = "${aws_s3_bucket.example.bucket}" + filter_prefix = "images/" + filter_suffix = "" } permissions = { diff --git a/lambda_function/bucket_trigger.tf b/lambda_function/bucket_trigger.tf index dbbcd79..88de19c 100644 --- a/lambda_function/bucket_trigger.tf +++ b/lambda_function/bucket_trigger.tf @@ -1,9 +1,11 @@ resource "aws_s3_bucket_notification" "bucket_notification" { - count = "${var.bucket_trigger["enabled"] ? 1 : 0}" - bucket = "${var.bucket_trigger["bucket"]}" + count = var.bucket_trigger["enabled"] ? 1 : 0 + bucket = var.bucket_trigger["bucket"] lambda_function { - lambda_function_arn = "${aws_lambda_function.lambda.arn}" + lambda_function_arn = aws_lambda_function.lambda.arn events = ["s3:ObjectCreated:*"] + filter_prefix = var.bucket_trigger["filter_prefix"] + filter_suffix = var.bucket_trigger["filter_suffix"] } } diff --git a/lambda_function/chicken-egg.tf b/lambda_function/chicken-egg.tf index 2d69e61..38db325 100644 --- a/lambda_function/chicken-egg.tf +++ b/lambda_function/chicken-egg.tf @@ -1,5 +1,5 @@ data "archive_file" "lambda_placeholder" { - count = "${var.create_empty_function ? 1 : 0}" + count = var.create_empty_function ? 1 : 0 type = "zip" output_path = "${path.module}/placeholder.zip" diff --git a/lambda_function/ci.tf b/lambda_function/ci.tf new file mode 100644 index 0000000..c2a37a1 --- /dev/null +++ b/lambda_function/ci.tf @@ -0,0 +1,115 @@ +data "aws_region" "current" {} +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "codebuild" { + count = var.github_url == "" ? 0 : 1 + + name = "codebuild_${var.function_name}" + + assume_role_policy = < { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + callback(null, response); +}; diff --git a/lambda_layer/placeholders/nodejs8.10/index.js b/lambda_layer/placeholders/nodejs8.10/index.js new file mode 100644 index 0000000..2af5e7d --- /dev/null +++ b/lambda_layer/placeholders/nodejs8.10/index.js @@ -0,0 +1,7 @@ +exports.handler = async (event) => { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + return response; +}; diff --git a/lambda_layer/placeholders/provided/bin/placeholder.sh b/lambda_layer/placeholders/provided/bin/placeholder.sh new file mode 100644 index 0000000..fdffa2a --- /dev/null +++ b/lambda_layer/placeholders/provided/bin/placeholder.sh @@ -0,0 +1 @@ +# placeholder diff --git a/lambda_layer/placeholders/python2.7/index.py b/lambda_layer/placeholders/python2.7/index.py new file mode 100644 index 0000000..98b644f --- /dev/null +++ b/lambda_layer/placeholders/python2.7/index.py @@ -0,0 +1,9 @@ +import json + + +def lambda_handler(event, context): + print(json.dumps(event)) + return { + 'statusCode': 200, + 'body': event + } diff --git a/lambda_layer/placeholders/python3.6/index.py b/lambda_layer/placeholders/python3.6/index.py new file mode 100644 index 0000000..98b644f --- /dev/null +++ b/lambda_layer/placeholders/python3.6/index.py @@ -0,0 +1,9 @@ +import json + + +def lambda_handler(event, context): + print(json.dumps(event)) + return { + 'statusCode': 200, + 'body': event + } diff --git a/lambda_layer/placeholders/python3.7/index.py b/lambda_layer/placeholders/python3.7/index.py new file mode 100644 index 0000000..98b644f --- /dev/null +++ b/lambda_layer/placeholders/python3.7/index.py @@ -0,0 +1,9 @@ +import json + + +def lambda_handler(event, context): + print(json.dumps(event)) + return { + 'statusCode': 200, + 'body': event + } diff --git a/lambda_layer/placeholders/python3.8/index.py b/lambda_layer/placeholders/python3.8/index.py new file mode 100644 index 0000000..98b644f --- /dev/null +++ b/lambda_layer/placeholders/python3.8/index.py @@ -0,0 +1,9 @@ +import json + + +def lambda_handler(event, context): + print(json.dumps(event)) + return { + 'statusCode': 200, + 'body': event + } diff --git a/lambda_layer/variables.tf b/lambda_layer/variables.tf new file mode 100644 index 0000000..8dd13c3 --- /dev/null +++ b/lambda_layer/variables.tf @@ -0,0 +1,59 @@ +variable "aws_region" { + default = "us-east-1" + description = "The region of AWS" +} + +variable "layer_name" { + type = string +} + +variable "description" { + type = string +} + +variable "runtime" { + type = string +} + +variable "filename" { + type = string + default = "" +} + +variable "create_empty_layer" { + default = true +} + +variable "reserved_concurrent_executions" { + default = "-1" +} + +variable "github_url" { + type = string + default = "" +} + +variable "codebuild_image" { + type = string + default = "aws/codebuild/standard:4.0" +} + +variable "privileged_mode" { + type = string + default = false +} + +variable "codebuild_credential_arn" { + type = string + default = "" +} + +variable "build_timeout" { + type = string + default = "60" +} + +variable "git_branch" { + type = string + default = "master" +} \ No newline at end of file