Skip to content

Commit 033b787

Browse files
authored
fix(local invoke): ensure later Lambda layers overwrite earlier layers in Docker image (#8688)
* fix: ensure later Lambda layers overwrite earlier layers in Docker image Docker's ADD/COPY with directories may merge rather than overwrite files in BuildKit, breaking AWS Lambda's layer precedence where later layers should override earlier ones. Fix by staging each layer in a temp directory and using 'cp -rf' to explicitly overwrite files in /opt, matching real Lambda behavior. Also add cleanup_samcli_images() call to tearDownClass for proper cleanup between parameterized test classes. * fix: ensure later Lambda layers overwrite earlier layers in Docker image Docker's ADD with directories may not overwrite existing files in all build backends (e.g. BuildKit). This breaks AWS Lambda's layer precedence where later layers should override earlier ones. Fix: the first layer still uses ADD directly to /opt. Subsequent layers are staged in a temp directory and copied with 'cp -rf' to guarantee overwrite semantics. Also adds test_two_independent_layers integration test that verifies two layers providing different modules both work in a single function, and adds cleanup_samcli_images() to tearDownClass. * fix: pass LayerAArn/LayerBArn through nested template parents The nested template parents were not forwarding the new layer parameters, causing SAM CLI to use the default fake ARNs and fail with lambda:GetLayerVersion permission errors.
1 parent f6c6c1d commit 033b787

8 files changed

Lines changed: 88 additions & 5 deletions

File tree

samcli/local/docker/lambda_image.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,8 @@ def _generate_dockerfile(base_image, layers, architecture):
498498
ADD aws-lambda-rie /var/rapid
499499
500500
ADD layer1 /opt
501-
ADD layer2 /opt
501+
ADD layer2 /tmp/layer1
502+
RUN cp -rf /tmp/layer1/. /opt/ && rm -rf /tmp/layer1
502503
503504
Parameters
504505
----------
@@ -521,8 +522,16 @@ def _generate_dockerfile(base_image, layers, architecture):
521522
+ f"ADD {rie_name} {rie_path}\n"
522523
+ f"RUN mv {rie_path}{rie_name} {rie_path}aws-lambda-rie && chmod +x {rie_path}aws-lambda-rie\n"
523524
)
524-
for layer in layers:
525-
dockerfile_content = dockerfile_content + f"ADD {layer.name} {LambdaImage._LAYERS_DIR}\n"
525+
for idx, layer in enumerate(layers):
526+
if idx == 0:
527+
# First layer can be added directly — no existing files to conflict with
528+
dockerfile_content += f"ADD {layer.name} {LambdaImage._LAYERS_DIR}\n"
529+
else:
530+
# Subsequent layers: stage in temp dir, then cp -rf to ensure overwrite.
531+
# Docker ADD/COPY may merge directories without overwriting in BuildKit.
532+
stage_dir = f"/tmp/layer{idx}"
533+
dockerfile_content += f"ADD {layer.name} {stage_dir}\n"
534+
dockerfile_content += f"RUN cp -rf {stage_dir}/. {LambdaImage._LAYERS_DIR}/ && rm -rf {stage_dir}\n"
526535
return dockerfile_content
527536

528537
def _remove_rapid_images(self, repo: str) -> None:

tests/integration/local/invoke/test_integrations_cli.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,11 +834,14 @@ def tearDown(self):
834834
def setUpClass(cls):
835835
cls.layer_utils.upsert_layer(LayerUtils.generate_layer_name(), "LayerOneArn", "layer1.zip")
836836
cls.layer_utils.upsert_layer(LayerUtils.generate_layer_name(), "LayerTwoArn", "layer2.zip")
837+
cls.layer_utils.upsert_layer(LayerUtils.generate_layer_name(), "LayerAArn", "layer_a.zip")
838+
cls.layer_utils.upsert_layer(LayerUtils.generate_layer_name(), "LayerBArn", "layer_b.zip")
837839
super(TestLayerVersionBase, cls).setUpClass()
838840

839841
@classmethod
840842
def tearDownClass(cls):
841843
cls.layer_utils.delete_layers()
844+
cleanup_samcli_images()
842845
integ_layer_cache_dir = Path().home().joinpath("integ_layer_cache")
843846
if integ_layer_cache_dir.exists():
844847
shutil.rmtree(str(integ_layer_cache_dir))
@@ -1008,6 +1011,31 @@ def test_download_two_layers(self, function_logical_id):
10081011

10091012
self.assertEqual(process_stdout, expected_output)
10101013

1014+
def test_two_independent_layers(self):
1015+
"""Test that two layers providing different modules both work in a single function."""
1016+
command_list = InvokeIntegBase.get_command_list(
1017+
"TwoIndependentLayersFunction",
1018+
template_path=self.template_path,
1019+
no_event=True,
1020+
region=self.region,
1021+
layer_cache=str(self.layer_cache),
1022+
parameter_overrides=self.layer_utils.parameters_overrides,
1023+
)
1024+
1025+
process = Popen(command_list, stdout=PIPE)
1026+
try:
1027+
stdout, _ = process.communicate(timeout=TIMEOUT)
1028+
except TimeoutExpired:
1029+
process.kill()
1030+
raise
1031+
1032+
process_stdout = stdout.decode("utf-8").strip().split(os.linesep)[-1]
1033+
import json
1034+
1035+
result = json.loads(process_stdout)
1036+
self.assertEqual(result["a"], "hello from layer A")
1037+
self.assertEqual(result["b"], "goodbye from layer B")
1038+
10111039
def test_caching_two_layers(self):
10121040
command_list = InvokeIntegBase.get_command_list(
10131041
"TwoLayerVersionServerlessFunction",
545 Bytes
Binary file not shown.
547 Bytes
Binary file not shown.

tests/integration/testdata/invoke/layers/layer-main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@ def one_layer_hanlder(event, context):
1919
from simple_python_module.simple_python import which_layer
2020

2121
return which_layer()
22+
23+
24+
def two_independent_layers_handler(event, context):
25+
from module_a.util import greeting
26+
from module_b.util import farewell
27+
28+
return {"a": greeting(), "b": farewell()}

tests/integration/testdata/invoke/layers/layer-template.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ Parameters:
1919
Default: arn:aws:lambda:us-west-2:111111111111:layer:non_existent_layer:1
2020
Type: String
2121

22+
LayerAArn:
23+
Default: arn:aws:lambda:us-west-2:111111111111:layer:layer_a:1
24+
Type: String
25+
26+
LayerBArn:
27+
Default: arn:aws:lambda:us-west-2:111111111111:layer:layer_b:1
28+
Type: String
29+
2230
Resources:
2331
# AWS::Serverless::Function
2432
OneLayerVersionServerlessFunction:
@@ -145,6 +153,17 @@ Resources:
145153
Layers:
146154
- arn:aws:lambda:us-west-2:111111111101:layer:layerDoesNotExist:1
147155

156+
TwoIndependentLayersFunction:
157+
Type: AWS::Serverless::Function
158+
Properties:
159+
Handler: layer-main.two_independent_layers_handler
160+
Runtime: python3.9
161+
CodeUri: .
162+
Timeout: 20
163+
Layers:
164+
- Ref: LayerAArn
165+
- Ref: LayerBArn
166+
148167
# AWS::Lambda::LayerVersion
149168
MyCustomLambdaLayer:
150169
Type: AWS::Lambda::LayerVersion

tests/integration/testdata/invoke/layers/some-dir/layer-template-parent.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ Parameters:
1919
Default: arn:aws:lambda:us-west-2:111111111111:layer:non_existent_layer:1
2020
Type: String
2121

22+
LayerAArn:
23+
Default: arn:aws:lambda:us-west-2:111111111111:layer:layer_a:1
24+
Type: String
25+
26+
LayerBArn:
27+
Default: arn:aws:lambda:us-west-2:111111111111:layer:layer_b:1
28+
Type: String
29+
2230
Resources:
2331
SubApp:
2432
Type: AWS::Serverless::Application
@@ -28,4 +36,6 @@ Resources:
2836
LayerOneArn: !Ref LayerOneArn
2937
LayerTwoArn: !Ref LayerTwoArn
3038
ChangedLayerArn: !Ref ChangedLayerArn
31-
NonExistentLayerArn: !Ref NonExistentLayerArn
39+
NonExistentLayerArn: !Ref NonExistentLayerArn
40+
LayerAArn: !Ref LayerAArn
41+
LayerBArn: !Ref LayerBArn

tests/integration/testdata/invoke/nested-templates/layer-template-parent.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ Parameters:
1919
Default: arn:aws:lambda:us-west-2:111111111111:layer:non_existent_layer:1
2020
Type: String
2121

22+
LayerAArn:
23+
Default: arn:aws:lambda:us-west-2:111111111111:layer:layer_a:1
24+
Type: String
25+
26+
LayerBArn:
27+
Default: arn:aws:lambda:us-west-2:111111111111:layer:layer_b:1
28+
Type: String
29+
2230
Resources:
2331
SubApp:
2432
Type: AWS::Serverless::Application
@@ -28,4 +36,6 @@ Resources:
2836
LayerOneArn: !Ref LayerOneArn
2937
LayerTwoArn: !Ref LayerTwoArn
3038
ChangedLayerArn: !Ref ChangedLayerArn
31-
NonExistentLayerArn: !Ref NonExistentLayerArn
39+
NonExistentLayerArn: !Ref NonExistentLayerArn
40+
LayerAArn: !Ref LayerAArn
41+
LayerBArn: !Ref LayerBArn

0 commit comments

Comments
 (0)