Skip to content

Commit db9e514

Browse files
committed
initial commit
1 parent 57e82f9 commit db9e514

File tree

9 files changed

+189
-0
lines changed

9 files changed

+189
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@
77

88
# .tfvars files
99
*.tfvars
10+
11+
# VSCode junk
12+
.vscode/*
13+
14+
# OSX Garbage
15+
.DS_Store

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,58 @@
11
# terraform-aws-lamba-python-archive
22
Package python source and dependencies into Lambda package with stable hash.
3+
4+
See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html
5+
6+
I created this module because using the standard technique of using an
7+
`archive_file` data source has a few shortcomings:
8+
1. This doesn't allow processing a requirements file.
9+
2. Each apply results in a diff in `source_code_hash` becuase the archive includes
10+
metadata and generated files (*.pyc). This module doesn't include file metadata
11+
or .pyc files in the archive so the hash is stable unless the source or dependencies
12+
change.
13+
14+
## Example
15+
```
16+
module "python_lambda_archive" {
17+
source = "rojopolis/lambda-python-archive/aws"
18+
src_dir = "${path.module}/python"
19+
}
20+
21+
resource "aws_iam_role" "iam_for_lambda" {
22+
name = "iam_for_lambda"
23+
24+
assume_role_policy = <<EOF
25+
{
26+
"Version": "2012-10-17",
27+
"Statement": [
28+
{
29+
"Action": "sts:AssumeRole",
30+
"Principal": {
31+
"Service": "lambda.amazonaws.com"
32+
},
33+
"Effect": "Allow",
34+
"Sid": ""
35+
}
36+
]
37+
}
38+
EOF
39+
}
40+
41+
resource "aws_lambda_function" "test_lambda" {
42+
filename = "${module.python_lambda_archive.archive_path}"
43+
function_name = "lambda_function_name"
44+
role = "${aws_iam_role.iam_for_lambda.arn}"
45+
handler = "exports.test"
46+
source_code_hash = "${module.python_lambda_archive.source_code_hash}"
47+
runtime = "python3.6"
48+
49+
environment {
50+
variables = {
51+
foo = "bar"
52+
}
53+
}
54+
}
55+
```
56+
57+
## External Dependencies
58+
1. Python3.4+

examples/lambda_python_archive.tf

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module "python_lambda_archive" {
2+
source = "../"
3+
src_dir = "${path.module}/python"
4+
}
5+
6+
resource "aws_iam_role" "iam_for_lambda" {
7+
name = "iam_for_lambda"
8+
9+
assume_role_policy = <<EOF
10+
{
11+
"Version": "2012-10-17",
12+
"Statement": [
13+
{
14+
"Action": "sts:AssumeRole",
15+
"Principal": {
16+
"Service": "lambda.amazonaws.com"
17+
},
18+
"Effect": "Allow",
19+
"Sid": ""
20+
}
21+
]
22+
}
23+
EOF
24+
}
25+
26+
resource "aws_lambda_function" "test_lambda" {
27+
filename = "${module.python_lambda_archive.archive_path}"
28+
function_name = "lambda_function_name"
29+
role = "${aws_iam_role.iam_for_lambda.arn}"
30+
handler = "exports.test"
31+
source_code_hash = "${module.python_lambda_archive.source_code_hash}"
32+
runtime = "python3.6"
33+
34+
environment {
35+
variables = {
36+
foo = "bar"
37+
}
38+
}
39+
}

examples/python/my_lambda.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def entrypoint():
2+
print("Hello world.")
3+
return 0

examples/python/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pandas
2+
scikit-learn

main.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
data "external" "lambda_archive" {
2+
program = ["python", "${path.module}/scripts/build_lambda.py"]
3+
query = {
4+
src_dir = "${var.src_dir}"
5+
}
6+
}

outputs.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
output "archive_path" {
2+
description = "Path of the archive file."
3+
value = "${data.external.lambda_archive.result.archive}"
4+
}
5+
6+
output "source_code_hash" {
7+
description = "Base64 encoded SHA256 hash of the archive file."
8+
value = "${data.external.lambda_archive.result.base64sha256}"
9+
}

scripts/build_lambda.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
from distutils.dir_util import copy_tree
3+
4+
import base64
5+
import glob
6+
import hashlib
7+
import json
8+
import os
9+
import shutil
10+
import subprocess
11+
import sys
12+
import tempfile
13+
import zipfile
14+
15+
def build(src_dir):
16+
with tempfile.TemporaryDirectory() as build_dir:
17+
copy_tree(src_dir, build_dir)
18+
if os.path.exists(os.path.join(src_dir, 'requirements.txt')):
19+
subprocess.run(
20+
[sys.executable,
21+
'-m',
22+
'pip',
23+
'install',
24+
'--ignore-installed',
25+
'--target', build_dir,
26+
'-r', os.path.join(build_dir, 'requirements.txt')],
27+
check=True,
28+
capture_output=True
29+
)
30+
artifact=tempfile.NamedTemporaryFile(delete=False)
31+
make_archive(build_dir, artifact)
32+
return artifact.name
33+
34+
35+
def make_archive(src_dir, archive_file):
36+
with zipfile.ZipFile(archive_file, 'w') as archive:
37+
for root, dirs, files in os.walk(src_dir):
38+
for file in files:
39+
if file.endswith('.pyc'):
40+
break
41+
metadata = zipfile.ZipInfo(
42+
os.path.join(root, file).replace(src_dir, '')
43+
)
44+
with open(os.path.join(root, file), 'rb') as f:
45+
data = f.read()
46+
archive.writestr(
47+
metadata,
48+
data
49+
)
50+
51+
def get_hash(archive_file):
52+
'''
53+
Return base64 encoded sha256 hash of archive file
54+
'''
55+
with open(archive_file, 'rb') as f:
56+
h = hashlib.sha256()
57+
h.update(f.read())
58+
return base64.standard_b64encode(h.digest()).decode('utf-8', 'strict')
59+
60+
61+
if __name__ == '__main__':
62+
query = json.loads(sys.stdin.read())
63+
archive = build(query['src_dir'])
64+
print(json.dumps({'archive': archive, "base64sha256":get_hash(archive)}))

variables.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
variable "src_dir" {
2+
description = "Path to root of Python source to package."
3+
type = "string"
4+
}

0 commit comments

Comments
 (0)