diff --git a/README.md b/README.md index cfcbfcc..cd48b51 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ function name unique per region, for example by setting | attach\_dead\_letter\_config | Set this to true if using the dead_letter_config variable | string | `"false"` | no | | attach\_policy | Set this to true if using the policy variable | string | `"false"` | no | | attach\_vpc\_config | Set this to true if using the vpc_config variable | string | `"false"` | no | +| build\_script | The path to a custom build script for creating the Lambda package zip file (defaults to the included build.py) | string | `""` | no | | dead\_letter\_config | Dead letter configuration for the Lambda function | map | `` | no | | description | Description of what your Lambda function does | string | `"Managed by Terraform"` | no | | enable\_cloudwatch\_logs | Set this to false to disable logging your Lambda output to CloudWatch Logs | string | `"true"` | no | diff --git a/archive.tf b/archive.tf index 071a0b3..03973bc 100644 --- a/archive.tf +++ b/archive.tf @@ -4,8 +4,9 @@ data "external" "archive" { program = ["${path.module}/hash.py"] query = { - runtime = "${var.runtime}" - source_path = "${var.source_path}" + runtime = "${var.runtime}" + source_path = "${var.source_path}" + build_script = "${coalesce(var.build_script, "${path.module}/build.py")}" } } diff --git a/hash.py b/hash.py index 8e2beb4..1e9def7 100755 --- a/hash.py +++ b/hash.py @@ -120,22 +120,24 @@ def update_hash(hash_obj, file_root, file_path): query = { 'runtime': 'python3.6', 'source_path': os.path.join(current_dir, 'tests', 'python3-pip', 'lambda'), + 'build_script': os.path.join(current_dir, 'build.py'), } else: query = json.load(sys.stdin) runtime = query['runtime'] source_path = query['source_path'] +build_script = query['build_script'] # Validate the query. if not source_path: abort('source_path must be set.') # Generate a hash based on file names and content. Also use the -# runtime value and content of build.py because they can have an +# runtime value and content of the build script because they can have an # effect on the resulting archive. content_hash = generate_content_hash(source_path) content_hash.update(runtime.encode()) -with open(os.path.join(current_dir, 'build.py'), 'rb') as build_script_file: +with open(build_script, 'rb') as build_script_file: content_hash.update(build_script_file.read()) # Generate a unique filename based on the hash. @@ -146,7 +148,7 @@ def update_hash(hash_obj, file_root, file_path): # Determine the command to run if Terraform wants to build a new archive. build_command = "{build_script} {build_data}".format( - build_script=os.path.join(current_dir, 'build.py'), + build_script=build_script, build_data=bytes.decode(base64.b64encode(str.encode( json.dumps({ 'filename': filename, diff --git a/tests/build-script/lambda/build.sh b/tests/build-script/lambda/build.sh new file mode 100755 index 0000000..5152065 --- /dev/null +++ b/tests/build-script/lambda/build.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Compiles a Python package into a zip deployable on AWS Lambda. +# +# - Builds Python dependencies into the package, using a Docker image to +# correctly build native extensions +# - Able to be used with the terraform-aws-lambda module +# +# Dependencies: +# +# - Docker +# - base64 +# - jq +# +# Usage: +# +# $ ./build.sh "$(echo { \"filename\": \"out.zip\", \"runtime\": \"python3.7\", \"source_path\": \".\" \} | jq . | base64)" +# +# - Arguments to be provided as JSON, then Base64 encoded + +set -euo pipefail + +# Extract variables from the JSON-formatted, Base64 encoded input argument. +# This is to conform to arguments as passed in by hash.py +eval "$(echo $1 | base64 --decode | jq -r '@sh "FILENAME=\(.filename) RUNTIME=\(.runtime) SOURCE_PATH=\(.source_path)"')" + +# Convert to absolute paths +SOURCE_DIR=$(cd "$SOURCE_PATH" && pwd) +ZIP_DIR=$(cd "$(dirname "$FILENAME")" && pwd) +ZIP_NAME=$(basename "$FILENAME") + +# Install dependencies, using a Docker image to correctly build native extensions +docker run --rm -t -v "$SOURCE_DIR:/src" -v "$ZIP_DIR:/out" lambci/lambda:build-$RUNTIME sh -c " + cp -r /src /build && + cd /build && + pip install -r requirements.txt -t . && + chmod -R 755 . && + zip -r /out/$ZIP_NAME * && + chown \$(stat -c '%u:%g' /out) /out/$ZIP_NAME +" + +echo "Created $FILENAME from $SOURCE_PATH" diff --git a/tests/build-script/lambda/src/main.py b/tests/build-script/lambda/src/main.py new file mode 100644 index 0000000..de9967e --- /dev/null +++ b/tests/build-script/lambda/src/main.py @@ -0,0 +1,6 @@ +def lambda_handler(event, context): + print('importing numpy package') + import numpy as np + print('checking numpy works correctly') + assert np.array_equal(np.array([1, 2]) + 3, np.array([4, 5])) + return 'test passed' diff --git a/tests/build-script/lambda/src/requirements.txt b/tests/build-script/lambda/src/requirements.txt new file mode 100644 index 0000000..d9c3d91 --- /dev/null +++ b/tests/build-script/lambda/src/requirements.txt @@ -0,0 +1,3 @@ +# numpy has native extensions, needs a custom build script to +# install correctly if your host OS differs to Lambda OS +numpy diff --git a/tests/build-script/main.tf b/tests/build-script/main.tf new file mode 100644 index 0000000..7ce5ee8 --- /dev/null +++ b/tests/build-script/main.tf @@ -0,0 +1,21 @@ +terraform { + backend "local" { + path = "terraform.tfstate" + } +} + +provider "aws" { + region = "eu-west-1" +} + +module "lambda" { + source = "../../" + + function_name = "terraform-aws-lambda-test-build-script" + description = "Test custom build script functionality in terraform-aws-lambda" + handler = "main.lambda_handler" + runtime = "python3.7" + + source_path = "${path.module}/lambda/src" + build_script = "${path.module}/lambda/build.sh" +} diff --git a/variables.tf b/variables.tf index 8124e5a..6ddb9d0 100644 --- a/variables.tf +++ b/variables.tf @@ -36,6 +36,12 @@ variable "source_path" { type = "string" } +variable "build_script" { + description = "The path to a custom build script for creating the Lambda package zip file (defaults to the included build.py)" + type = "string" + default = "" +} + variable "description" { description = "Description of what your Lambda function does" type = "string"