Skip to content

Commit 5964d80

Browse files
committed
merge w/ upstream master + claranet#36
2 parents 34f9d9d + 5968898 commit 5964d80

File tree

15 files changed

+171
-82
lines changed

15 files changed

+171
-82
lines changed

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ module "lambda" {
5858
// Trigger from a Cloudwatch Events rule.
5959
attach_cloudwatch_rule_config = true
6060
cloudwatch_rule_config {
61-
name = "scheduled-run"
62-
description = "Run my lambda every day at 8pm"
61+
name = "scheduled-run"
62+
enabled = true // set this to false if you want to have the trigger declared but disabled
63+
description = "Run my lambda every day at 8pm"
6364
schedule_expression = "cron(0 20 * * ? *)"
6465
}
6566
}
@@ -76,9 +77,13 @@ function name unique per region, for example by setting
7677

7778
| Name | Description | Type | Default | Required |
7879
|------|-------------|:----:|:-----:|:-----:|
80+
| attach\_cloudwatch\_rule\_config | Set this to true if using the cloudwatch_rule_config variable | string | `false` | no |
7981
| attach\_dead\_letter\_config | Set this to true if using the dead_letter_config variable | string | `"false"` | no |
8082
| attach\_policy | Set this to true if using the policy variable | string | `"false"` | no |
8183
| attach\_vpc\_config | Set this to true if using the vpc_config variable | string | `"false"` | no |
84+
| build\_command | The command that creates the Lambda package zip file | string | `"python build.py '$filename' '$runtime' '$source'"` | no |
85+
| build\_paths | The files or directories used by the build command, to trigger new Lambda package builds whenever build scripts change | list | `<list>` | no |
86+
| cloudwatch\_rule\_config | Cloudwatch Rule for the Lambda function | map | `<map>` | no |
8287
| dead\_letter\_config | Dead letter configuration for the Lambda function | map | `<map>` | no |
8388
| description | Description of what your Lambda function does | string | `"Managed by Terraform"` | no |
8489
| enable\_cloudwatch\_logs | Set this to false to disable logging your Lambda output to CloudWatch Logs | string | `"true"` | no |
@@ -101,8 +106,8 @@ function name unique per region, for example by setting
101106

102107
| Name | Description |
103108
|------|-------------|
109+
| cloudwatch\_rule\_arn | The ARN of the Cloudwatch rule |
104110
| function\_arn | The ARN of the Lambda function |
105111
| function\_name | The name of the Lambda function |
106112
| role\_arn | The ARN of the IAM role created for the Lambda function |
107113
| role\_name | The name of the IAM role created for the Lambda function |
108-
| cloudwatch\_rule\_arn | The ARN of the Cloudwatch rule |

archive.tf

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1+
locals {
2+
module_relpath = "${substr(path.module, length(path.cwd) + 1, -1)}"
3+
}
4+
15
# Generates a filename for the zip archive based on the contents of the files
26
# in source_path. The filename will change when the source code changes.
37
data "external" "archive" {
48
count = "${var.source_from_s3 ? 0 : 1}"
59
program = ["python", "${path.module}/hash.py"]
610

711
query = {
8-
runtime = "${var.runtime}"
9-
source_path = "${var.source_path}"
12+
build_command = "${var.build_command}"
13+
build_paths = "${jsonencode(var.build_paths)}"
14+
module_relpath = "${local.module_relpath}"
15+
runtime = "${var.runtime}"
16+
source_path = "${var.source_path}"
1017
}
1118
}
1219

@@ -18,7 +25,8 @@ resource "null_resource" "archive" {
1825
}
1926

2027
provisioner "local-exec" {
21-
command = "${lookup(data.external.archive.result, "build_command")}"
28+
command = "${lookup(data.external.archive.result, "build_command")}"
29+
working_dir = "${path.module}"
2230
}
2331
}
2432

@@ -32,8 +40,9 @@ data "external" "built" {
3240
program = ["python", "${path.module}/built.py"]
3341

3442
query = {
35-
build_command = "${lookup(data.external.archive.result, "build_command")}"
36-
filename_old = "${lookup(null_resource.archive.triggers, "filename")}"
37-
filename_new = "${lookup(data.external.archive.result, "filename")}"
43+
build_command = "${lookup(data.external.archive.result, "build_command")}"
44+
filename_old = "${lookup(null_resource.archive.triggers, "filename")}"
45+
filename_new = "${lookup(data.external.archive.result, "filename")}"
46+
module_relpath = "${local.module_relpath}"
3847
}
3948
}

build.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Builds a zip file from the source_dir or source_file.
22
# Installs dependencies with pip automatically.
33

4-
import base64
5-
import json
64
import os
75
import shutil
86
import subprocess
@@ -105,11 +103,10 @@ def create_zip_file(source_dir, target_file):
105103
root_dir=source_dir,
106104
)
107105

108-
json_payload = bytes.decode(base64.b64decode(sys.argv[1]))
109-
query = json.loads(json_payload)
110-
filename = query['filename']
111-
runtime = query['runtime']
112-
source_path = query['source_path']
106+
107+
filename = sys.argv[1]
108+
runtime = sys.argv[2]
109+
source_path = sys.argv[3]
113110

114111
absolute_filename = os.path.abspath(filename)
115112

builds/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.zip

built.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
build_command = query['build_command']
1313
filename_old = query['filename_old']
1414
filename_new = query['filename_new']
15+
module_relpath = query['module_relpath']
1516

1617
# If the old filename (from the Terraform state) matches the new filename
1718
# (from hash.py) then the source code has not changed and thus the zip file
@@ -29,10 +30,10 @@
2930
# console) then it is possible that Terraform will try to upload
3031
# the missing file. I don't know how to tell if Terraform is going
3132
# to try to upload the file or not, so always ensure the file exists.
32-
subprocess.check_output(build_command, shell=True)
33+
subprocess.check_output(build_command, shell=True, cwd=module_relpath)
3334

3435
# Output the filename to Terraform.
3536
json.dump({
36-
'filename': filename_new,
37+
'filename': module_relpath + '/' + filename_new,
3738
}, sys.stdout, indent=2)
3839
sys.stdout.write('\n')

cloudwatch.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
resource "aws_lambda_permission" "cloudwatch_trigger" {
2+
count = "${var.attach_cloudwatch_rule_config ? 1 : 0}"
3+
statement_id = "AllowExecutionFromCloudWatch"
4+
action = "${lookup(var.cloudwatch_rule_config, "enabled", true) ? "lambda:InvokeFunction" : "lambda:DisableInvokeFunction"}"
5+
function_name = "${element(concat(aws_lambda_function.lambda.*.function_name, aws_lambda_function.lambda_s3.*.function_name, aws_lambda_function.lambda_with_dl.*.function_name, aws_lambda_function.lambda_with_vpc.*.function_name, aws_lambda_function.lambda_with_dl_and_vpc.*.function_name), 0)}"
6+
principal = "events.amazonaws.com"
7+
source_arn = "${aws_cloudwatch_event_rule.rule.arn}"
8+
}
19
resource "aws_cloudwatch_event_rule" "rule" {
210
count = "${var.attach_cloudwatch_rule_config ? 1 : 0}"
311
name = "${var.cloudwatch_rule_config["name"]}"

hash.py

Lines changed: 46 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,14 @@
33
#
44
# Outputs a filename and a command to run if the archive needs to be built.
55

6-
import base64
76
import datetime
87
import errno
98
import hashlib
109
import json
1110
import os
12-
import re
1311
import sys
1412

1513

16-
FILENAME_PREFIX = 'terraform-aws-lambda-'
17-
FILENAME_PATTERN = re.compile(r'^' + FILENAME_PREFIX + r'[0-9a-f]{64}\.zip$')
18-
19-
2014
def abort(message):
2115
"""
2216
Exits with an error message.
@@ -36,24 +30,21 @@ def delete_old_archives():
3630
now = datetime.datetime.now()
3731
delete_older_than = now - datetime.timedelta(days=7)
3832

39-
top = '.terraform'
40-
if os.path.isdir(top):
41-
for name in os.listdir(top):
42-
if FILENAME_PATTERN.match(name):
43-
path = os.path.join(top, name)
44-
try:
45-
file_modified = datetime.datetime.fromtimestamp(
46-
os.path.getmtime(path)
47-
)
48-
if file_modified < delete_older_than:
49-
os.remove(path)
50-
except OSError as error:
51-
if error.errno == errno.ENOENT:
52-
# Ignore "not found" errors as they are probably race
53-
# conditions between multiple usages of this module.
54-
pass
55-
else:
56-
raise
33+
for name in os.listdir('builds'):
34+
if name.endswith('.zip'):
35+
try:
36+
file_modified = datetime.datetime.fromtimestamp(
37+
os.path.getmtime(name)
38+
)
39+
if file_modified < delete_older_than:
40+
os.remove(name)
41+
except OSError as error:
42+
if error.errno == errno.ENOENT:
43+
# Ignore "not found" errors as they are probably race
44+
# conditions between multiple usages of this module.
45+
pass
46+
else:
47+
raise
5748

5849

5950
def list_files(top_path):
@@ -72,22 +63,23 @@ def list_files(top_path):
7263
return results
7364

7465

75-
def generate_content_hash(source_path):
66+
def generate_content_hash(source_paths):
7667
"""
77-
Generate a content hash of the source path.
68+
Generate a content hash of the source paths.
7869
7970
"""
8071

8172
sha256 = hashlib.sha256()
8273

83-
if os.path.isdir(source_path):
84-
source_dir = source_path
85-
for source_file in list_files(source_dir):
74+
for source_path in source_paths:
75+
if os.path.isdir(source_path):
76+
source_dir = source_path
77+
for source_file in list_files(source_dir):
78+
update_hash(sha256, source_dir, source_file)
79+
else:
80+
source_dir = os.path.dirname(source_path)
81+
source_file = source_path
8682
update_hash(sha256, source_dir, source_file)
87-
else:
88-
source_dir = os.path.dirname(source_path)
89-
source_file = source_path
90-
update_hash(sha256, source_dir, source_file)
9183

9284
return sha256
9385

@@ -109,51 +101,42 @@ def update_hash(hash_obj, file_root, file_path):
109101
hash_obj.update(data)
110102

111103

112-
113-
current_dir = os.path.dirname(__file__)
114-
115104
# Parse the query.
116-
if len(sys.argv) > 1 and sys.argv[1] == '--test':
117-
query = {
118-
'runtime': 'python3.6',
119-
'source_path': os.path.join(current_dir, 'tests', 'python3-pip', 'lambda'),
120-
}
121-
else:
122-
query = json.load(sys.stdin)
105+
query = json.load(sys.stdin)
106+
build_command = query['build_command']
107+
build_paths = json.loads(query['build_paths'])
108+
module_relpath = query['module_relpath']
123109
runtime = query['runtime']
124110
source_path = query['source_path']
125111

126112
# Validate the query.
127113
if not source_path:
128114
abort('source_path must be set.')
129115

116+
# Change working directory to the module path
117+
# so references to build.py will work.
118+
os.chdir(module_relpath)
119+
130120
# Generate a hash based on file names and content. Also use the
131-
# runtime value and content of build.py because they can have an
132-
# effect on the resulting archive.
133-
content_hash = generate_content_hash(source_path)
121+
# runtime value, build command, and content of the build paths
122+
# because they can have an effect on the resulting archive.
123+
content_hash = generate_content_hash([source_path] + build_paths)
134124
content_hash.update(runtime.encode())
135-
with open(os.path.join(current_dir, 'build.py'), 'rb') as build_script_file:
136-
content_hash.update(build_script_file.read())
125+
content_hash.update(build_command.encode())
137126

138127
# Generate a unique filename based on the hash.
139-
filename = '.terraform/{prefix}{content_hash}.zip'.format(
140-
prefix=FILENAME_PREFIX,
128+
filename = 'builds/{content_hash}.zip'.format(
141129
content_hash=content_hash.hexdigest(),
142130
)
143131

144-
# Determine the command to run if Terraform wants to build a new archive.
145-
build_command = "python {build_script} {build_data}".format(
146-
build_script=os.path.join(current_dir, 'build.py'),
147-
build_data=bytes.decode(base64.b64encode(str.encode(
148-
json.dumps({
149-
'filename': filename,
150-
'source_path': source_path,
151-
'runtime': runtime,
152-
})
153-
)
154-
),
155-
)
156-
)
132+
# Replace variables in the build command with calculated values.
133+
replacements = {
134+
'$filename': filename,
135+
'$runtime': runtime,
136+
'$source': source_path,
137+
}
138+
for old, new in replacements.items():
139+
build_command = build_command.replace(old, new)
157140

158141
# Delete previous archives.
159142
delete_old_archives()

outputs.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ output "role_name" {
2020

2121
output "cloudwatch_rule_arn" {
2222
description = "The ARN of the Cloudwatch rule"
23-
value = ["${compact(concat(aws_cloudwatch_event_rule.rule.*.arn))}"]
23+
value = "${element(concat(aws_cloudwatch_event_rule.rule.*.arn), 0)}"
2424
}

tests/.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
terraform 0.11.11

tests/build-command/lambda/build.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
#
3+
# Compiles a Python package into a zip deployable on AWS Lambda.
4+
#
5+
# - Builds Python dependencies into the package, using a Docker image to
6+
# correctly build native extensions
7+
# - Able to be used with the terraform-aws-lambda module
8+
#
9+
# Dependencies:
10+
#
11+
# - Docker
12+
#
13+
# Usage:
14+
#
15+
# $ ./build.sh <output-zip-filename> <runtime> <source-path>
16+
17+
set -euo pipefail
18+
19+
# Read variables from command line arguments
20+
FILENAME=$1
21+
RUNTIME=$2
22+
SOURCE_PATH=$3
23+
24+
# Convert to absolute paths
25+
SOURCE_DIR=$(cd "$SOURCE_PATH" && pwd)
26+
ZIP_DIR=$(cd "$(dirname "$FILENAME")" && pwd)
27+
ZIP_NAME=$(basename "$FILENAME")
28+
29+
# Install dependencies, using a Docker image to correctly build native extensions
30+
docker run --rm -t -v "$SOURCE_DIR:/src" -v "$ZIP_DIR:/out" lambci/lambda:build-$RUNTIME sh -c "
31+
cp -r /src /build &&
32+
cd /build &&
33+
pip install --progress-bar off -r requirements.txt -t . &&
34+
chmod -R 755 . &&
35+
zip -r /out/$ZIP_NAME * &&
36+
chown \$(stat -c '%u:%g' /out) /out/$ZIP_NAME
37+
"
38+
39+
echo "Created $FILENAME from $SOURCE_PATH"

0 commit comments

Comments
 (0)