Skip to content

Commit 3d290ae

Browse files
committed
Add Node.js as supported runtime
With this patch, lambda-layer-tool also supports building Lambda layer for Node.js runtimes. It uses NPM to install the specified packages.
1 parent 85e5382 commit 3d290ae

File tree

4 files changed

+114
-33
lines changed

4 files changed

+114
-33
lines changed

.github/workflows/pythonapp.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,19 @@ jobs:
3030
run: |
3131
pip install -r dev-requirements.txt
3232
python3 -m mypy *.py
33-
- name: Build example layer
33+
- name: Build example Python layer
3434
run: |
3535
cd tests/numpy/
3636
../../layer-tool.py --list
3737
../../layer-tool.py --build awesome-numpy
3838
du -h awesome-numpy.zip
39+
- name: Set up Node v10
40+
uses: actions/setup-node@v1
41+
with:
42+
node-version: '10.x'
43+
- name: Build example Node.js layer
44+
run: |
45+
cd tests/aws-sdk/
46+
../../layer-tool.py --list
47+
../../layer-tool.py --build awesome-aws-sdk
48+
du -h awesome-aws-sdk.zip

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ Instead of manually copy & pasting build instructions for a Lambda Layer into yo
88
Given a simple YAML file, it will:
99
* create a new, clean directory for the lambda layer,
1010
* run specified pre-installation commands,
11-
* install python requirements with Pip in a virtual environment,
11+
* *Python*: install requirements with Pip in a virtual environment,
12+
* *Node.js*: install dependencies with NPM,
1213
* strip any binaries and libraries in the lambda directory,
1314
* apply global and layer-specific exclusion patterns,
1415
* bundle the remainder up into a ZIP archive.
@@ -66,6 +67,8 @@ $ ./layer-tool.py --publish awesome-numpy
6667

6768
To learn how to use this tool to reduce the size of your layers, read the [post about creating a minimal boto3 layer](https://blog.cubieserver.de/2020/building-a-minimal-boto3-lambda-layer/).
6869

70+
The [`tests` directory](./tests/) also serve as examples how to build layers for different runtimes (Python, Node.js).
71+
6972
## Lambda Environment
7073

7174
To match the environment of AWS Lambda functions as closely as possible (especially when you use this tool on non-Linux systems), the tool should be run inside a Docker container.
@@ -92,8 +95,8 @@ In addition to the Python dependencies (`requirements.txt`), this tool currently
9295

9396
## Limitations
9497

95-
Currently, this tool only supports building Python layers with Pip.
96-
However, it should be fairly straightforward to extend the functionality to other runtimes, e.g. JavaScript.
98+
Currently, this tool only supports building Python layers with Pip and Node.js layer with NPM.
99+
However, it should be fairly straightforward to extend the functionality to other runtimes and package managers.
97100

98101
## License
99102

layer-tool.py

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def main():
4242
if not file_version:
4343
print("Warning: no version specified in file!")
4444
elif file_version != '0.3':
45-
print("Unsupport file version: ", file_version)
45+
print("Unsupported file version: ", file_version)
4646
return 1
4747

4848
# basic sanity check for YAML file structure
@@ -118,43 +118,82 @@ def build_layer(layername: str, options: Dict[str, Any]) -> int:
118118

119119
# set_paths
120120
venv_dir: str = os.path.join(tmp_dir_path, "venv/")
121+
node_modules_dir: str = os.path.join(tmp_dir_path, "node_modules/")
121122
pip_bin: str = os.path.join(venv_dir, "bin/pip")
122-
lambda_dir: str = os.path.join(tmp_dir_path, "python/")
123123
outfile: str = layername + ".zip"
124124

125+
lambda_dir: str
126+
if runtime.startswith("python"):
127+
lambda_dir = os.path.join(tmp_dir_path, "python")
128+
elif runtime.startswith("node"):
129+
lambda_dir = os.path.join(tmp_dir_path, "nodejs")
130+
125131
# create a new directory
126132
# which only contains files relevant to the lambda layer
127133
os.mkdir(lambda_dir)
128134

129-
# activate virtualenv
130-
venv.create(venv_dir, with_pip=True)
135+
if runtime.startswith('python'):
136+
# activate virtualenv
137+
venv.create(venv_dir, with_pip=True)
131138

132-
# run pre-install steps
133-
for cmd in pre_install_cmds:
134-
try:
135-
subprocess.run(cmd, shell=True, check=True)
136-
except subprocess.CalledProcessError as e:
137-
print(e)
138-
return 1
139+
# run pre-install steps
140+
for cmd in pre_install_cmds:
141+
try:
142+
subprocess.run(cmd, shell=True, check=True)
143+
except subprocess.CalledProcessError as e:
144+
print(e)
145+
return 1
139146

140-
# install requirements with pip in venv
141-
for r in requirements:
142-
try:
143-
subprocess.run([pip_bin, "install", r], check=True)
144-
except subprocess.CalledProcessError as e:
145-
print(e)
146-
return 1
147+
# install requirements with pip in venv
148+
for r in requirements:
149+
try:
150+
subprocess.run([pip_bin, "install", r], check=True)
151+
except subprocess.CalledProcessError as e:
152+
print(e)
153+
return 1
147154

148-
# save venv packages
149-
with open(os.path.join(lambda_dir, 'pip-freeze.txt'), 'w') as outstream:
150-
try:
151-
subprocess.run([pip_bin, "freeze"], stdout=outstream, check=True)
152-
except subprocess.CalledProcessError as e:
153-
print(e)
154-
return 1
155+
# save venv packages
156+
with open(os.path.join(lambda_dir, 'pip-freeze.txt'), 'w') as outstream:
157+
try:
158+
subprocess.run([pip_bin, "freeze"], stdout=outstream, check=True)
159+
except subprocess.CalledProcessError as e:
160+
print(e)
161+
return 1
162+
163+
# move (copy) the installed requirements into the layer path
164+
os.rename(os.path.join(venv_dir, "lib/"), os.path.join(lambda_dir, "lib/"))
165+
166+
elif runtime.startswith('node'):
167+
# this ensures pre-install commands work properly
168+
os.mkdir(node_modules_dir)
155169

156-
# move (copy) the installed requirements into the layer path
157-
os.rename(os.path.join(venv_dir, "lib/"), os.path.join(lambda_dir, "lib/"))
170+
# run pre-install steps
171+
for cmd in pre_install_cmds:
172+
try:
173+
subprocess.run(cmd, shell=True, check=True)
174+
except subprocess.CalledProcessError as e:
175+
print(e)
176+
return 1
177+
178+
# install packages with npm
179+
for r in requirements:
180+
try:
181+
subprocess.run(["npm", "install", r], check=True)
182+
except subprocess.CalledProcessError as e:
183+
print(e)
184+
return 1
185+
186+
# save installed packages
187+
with open(os.path.join(lambda_dir, 'npm-list.txt'), 'w') as outstream:
188+
try:
189+
subprocess.run(["npm", "list"], stdout=outstream, check=True)
190+
except subprocess.CalledProcessError as e:
191+
print(e)
192+
return 1
193+
194+
# move the installed dependencies into the layer path
195+
os.rename(node_modules_dir, os.path.join(lambda_dir, "node_modules/"))
196+
os.rename(os.path.join(tmp_dir_path, "package-lock.json"), os.path.join(lambda_dir, "package-lock.json"))
158197

159198
# put current layer configuration into the folder
160199
with open(os.path.join(lambda_dir, 'layer.yaml'), 'w') as outstream:
@@ -169,7 +208,7 @@ def build_layer(layername: str, options: Dict[str, Any]) -> int:
169208
return 1
170209

171210
# package to zip archive and exclude unnecessary files
172-
zip_cmd: List[str] = ['zip', '-r', '-9', os.path.join(work_dir, outfile), "python/"]
211+
zip_cmd: List[str] = ['zip', '--filesync', '-r', '-9', os.path.join(work_dir, outfile), os.path.basename(lambda_dir)]
173212
for exclude in excludes:
174213
zip_cmd.append('-x')
175214
zip_cmd.append(exclude)
@@ -193,7 +232,17 @@ def build_layer(layername: str, options: Dict[str, Any]) -> int:
193232

194233

195234
def check_runtime(expected_runtime: str) -> bool:
196-
actual_runtime: str = "python{}.{}".format(sys.version_info[0], sys.version_info[1])
235+
actual_runtime: str
236+
if expected_runtime.startswith("python"):
237+
actual_runtime = "python{}.{}".format(sys.version_info[0], sys.version_info[1])
238+
elif expected_runtime.startswith("node"):
239+
node_major_version: str = subprocess.check_output(["node", "--version"]).decode("ascii")
240+
node_major_version = node_major_version[1:].split('.')[0]
241+
actual_runtime = "node{}.x".format(node_major_version)
242+
else:
243+
print("Error: unsupported runtime {}".format(expected_runtime))
244+
return False
245+
197246
if actual_runtime != expected_runtime:
198247
print("Error: specified runtime {} does not match: {}".format(expected_runtime, actual_runtime))
199248
return False

tests/aws-sdk/layers.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
version: '0.3'
3+
default_excludes:
4+
- 'LICENSE'
5+
- 'CHANGELOG'
6+
- 'README.md'
7+
- '.npmignore'
8+
9+
layers:
10+
awesome-aws-sdk:
11+
description: 'AWS SDK JS 2.668'
12+
runtimes: 'node10.x'
13+
pre_installs:
14+
- 'echo "Hello, World!" > node_modules/hello-world.txt'
15+
requirements:
16+
- 'aws-sdk@2.668.0'
17+
excludes:
18+
- '*/aws-sdk/*.d.ts'
19+
- '*/aws-sdk/*.txt'

0 commit comments

Comments
 (0)