Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lambda - add support for layers when creating or updating lambda functions #1118

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/fragments/lambda-add-support-for-layers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
minor_changes:
- lambda - add support for function layers when creating or updating lambda function.
107 changes: 107 additions & 0 deletions plugins/modules/lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,31 @@
choices: ['x86_64', 'arm64']
aliases: ['architectures']
version_added: 5.0.0
layers:
description:
- A list of function layers to add to the function's execution environment.
- Specify each layer by its ARN, including the version.
suboptions:
layer_version_arn:
description:
- The ARN of the layer version.
- Mutually exclusive with I(layer_version_arn).
type: str
layer_name:
description:
- The name or Amazon Resource Name (ARN) of the layer.
- Mutually exclusive with I(layer_version_arn).
type: str
aliases: ['layer_arn']
version:
description:
- The version number.
- Required when I(layer_name) is provided, ignored if not.
abikouo marked this conversation as resolved.
Show resolved Hide resolved
type: int
aliases: ['layer_version']
type: list
elements: dict
version_added: 5.1.0
author:
- 'Steyn Huizinga (@steynovich)'
extends_documentation_fragment:
Expand Down Expand Up @@ -178,6 +203,18 @@
loop:
- HelloWorld
- ByeBye

# Create Lambda functions with function layers
- name: looped creation
amazon.aws.lambda:
name: 'HelloWorld'
state: present
zip_file: 'hello-code.zip'
runtime: 'python2.7'
role: 'arn:aws:iam::123456789012:role/lambda_basic_execution'
handler: 'hello_python.my_handler'
layers:
- layer_version_arn: 'arn:aws:lambda:us-east-1:123456789012:layer:python27-env:7'
'''

RETURN = r'''
Expand Down Expand Up @@ -328,12 +365,36 @@
'subnet_ids': [],
'vpc_id': '123'
}
layers:
description: The function's layers.
returned: on success
version_added: 5.1.0
type: complex
contains:
arn:
description: The Amazon Resource Name (ARN) of the function layer.
returned: always
type: str
sample: active
code_size:
description: The size of the layer archive in bytes.
returned: always
type: str
signing_profile_version_arn:
description: The Amazon Resource Name (ARN) for a signing profile version.
returned: always
type: str
signing_job_arn:
description: The Amazon Resource Name (ARN) of a signing job.
returned: always
type: str
'''

import base64
import hashlib
import traceback
import re
from collections import Counter

try:
from botocore.exceptions import ClientError, BotoCoreError, WaiterError
Expand Down Expand Up @@ -393,6 +454,17 @@ def get_current_function(connection, function_name, qualifier=None):
return None


def get_layer_version_arn(module, connection, layer_name, version_number):
try:
layer_versions = connection.list_layer_versions(LayerName=layer_name, aws_retry=True)['LayerVersions']
for v in layer_versions:
if v["Version"] == version_number:
return v["LayerVersionArn"]
module.fail_json(msg='Unable to find version {0} from Lambda layer {1}'.format(version_number, layer_name))
except is_boto3_error_code('ResourceNotFoundException'):
module.fail_json(msg='Lambda layer {0} not found'.format(layer_name))


def sha256sum(filename):
hasher = hashlib.sha256()
with open(filename, 'rb') as f:
Expand Down Expand Up @@ -552,6 +624,21 @@ def main():
architecture=dict(choices=['x86_64', 'arm64'], type='str', aliases=['architectures']),
tags=dict(type='dict', aliases=['resource_tags']),
purge_tags=dict(type='bool', default=True),
layers=dict(
type='list',
elements='dict',
options=dict(
layer_version_arn=dict(type='str'),
layer_name=dict(type='str', aliases=['layer_arn']),
version=dict(type='int', aliases=['layer_version']),
),
required_together=[['layer_name', 'version']],
required_one_of=[['layer_version_arn', 'layer_name']],
mutually_exclusive=[
['layer_name', 'layer_version_arn'],
['version', 'layer_version_arn']
],
),
)

mutually_exclusive = [['zip_file', 's3_key'],
Expand Down Expand Up @@ -594,6 +681,7 @@ def main():
purge_tags = module.params.get('purge_tags')
kms_key_arn = module.params.get('kms_key_arn')
architectures = module.params.get('architecture')
layers = []

check_mode = module.check_mode
changed = False
Expand All @@ -615,6 +703,14 @@ def main():
account_id, partition = get_account_info(module)
role_arn = 'arn:{0}:iam::{1}:role/{2}'.format(partition, account_id, role)

# create list of layer version arn
if module.params.get("layers"):
for layer in module.params.get("layers"):
layer_version_arn = layer.get("layer_version_arn")
if layer_version_arn is None:
layer_version_arn = get_layer_version_arn(module, client, layer.get("layer_name"), layer.get("version"))
layers.append(layer_version_arn)

# Get function configuration if present, False otherwise
current_function = get_current_function(client, name)

Expand Down Expand Up @@ -676,6 +772,13 @@ def main():
if 'VpcConfig' in current_config and current_config['VpcConfig'].get('VpcId'):
func_kwargs.update({'VpcConfig': {'SubnetIds': [], 'SecurityGroupIds': []}})

# Check layers
if layers:
# compare two lists to see if the target layers are equal to the current
current_layers = current_config.get('Layers', [])
if Counter(layers) != Counter((f['Arn'] for f in current_layers)):
func_kwargs.update({'Layers': layers})

# Upload new configuration if configuration has changed
if len(func_kwargs) > 1:
if not check_mode:
Expand Down Expand Up @@ -760,6 +863,10 @@ def main():
func_kwargs.update({'VpcConfig': {'SubnetIds': vpc_subnet_ids,
'SecurityGroupIds': vpc_security_group_ids}})

# Layers
if layers:
func_kwargs.update({'Layers': layers})

# Tag Function
if tags:
func_kwargs.update({'Tags': tags})
Expand Down
5 changes: 5 additions & 0 deletions tests/integration/targets/lambda/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ lambda_role_name: ansible-test-{{ tiny_prefix }}-lambda

lambda_python_runtime: python3.9
lambda_python_handler: mini_lambda.handler
lambda_python_layers_names:
- "{{ tiny_prefix }}-layer-01"
- "{{ tiny_prefix }}-layer-02"
lambda_function_name_with_layer: '{{ tiny_prefix }}-func-with-layer'
lambda_function_name_with_multiple_layer: '{{ tiny_prefix }}-func-with-mutiplelayer'
1 change: 1 addition & 0 deletions tests/integration/targets/lambda/meta/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dependencies:
- role: setup_botocore_pip
vars:
botocore_version: 1.21.51
- role: setup_remote_tmp_dir
162 changes: 162 additions & 0 deletions tests/integration/targets/lambda/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,45 @@
- result.changed == False
- '"parameters are required together" in result.msg'

- name: test state=present with incomplete layers
lambda:
name: '{{ lambda_function_name }}'
runtime: '{{ lambda_python_runtime }}'
role: '{{ lambda_role_name }}'
handler: mini_lambda.handler
zip_file: '{{ zip_res.dest }}'
layers:
- layer_name: test-layer
check_mode: true
register: result
ignore_errors: true
- name: assert lambda fails with proper message
assert:
that:
- result is failed
- result is not changed
- '"parameters are required together: layer_name, version found in layers" in result.msg'

- name: test state=present with incomplete layers
lambda:
name: '{{ lambda_function_name }}'
runtime: '{{ lambda_python_runtime }}'
role: '{{ lambda_role_name }}'
handler: mini_lambda.handler
zip_file: '{{ zip_res.dest }}'
layers:
- layer_version_arn: 'arn:aws:lambda:us-east-2:123456789012:layer:blank-java-lib:7'
version: 9
check_mode: true
register: result
ignore_errors: true
- name: assert lambda fails with proper message
assert:
that:
- result is failed
- result is not changed
- '"parameters are mutually exclusive: version|layer_version_arn found in layers" in result.msg'

# Prepare minimal Lambda
- name: test state=present - upload the lambda (check mode)
lambda:
Expand Down Expand Up @@ -600,9 +639,132 @@
assert:
that:
- result is not failed

# Test creation with layers
- name: Create temporary directory for testing
abikouo marked this conversation as resolved.
Show resolved Hide resolved
tempfile:
suffix: lambda
state: directory
register: test_dir

- name: Create python directory for lambda layer
file:
path: "{{ remote_tmp_dir }}/python"
state: directory

- name: Create lambda layer library
copy:
content: |
def hello():
print("Hello from the ansible amazon.aws lambda layer")
return 1
dest: "{{ remote_tmp_dir }}/python/lambda_layer.py"

- name: Create lambda layer archive
archive:
format: zip
path: "{{ remote_tmp_dir }}"
dest: "{{ remote_tmp_dir }}/lambda_layer.zip"

- name: Create lambda layer
lambda_layer:
name: "{{ lambda_python_layers_names[0] }}"
description: '{{ lambda_python_layers_names[0] }} lambda layer'
content:
zip_file: "{{ remote_tmp_dir }}/lambda_layer.zip"
register: first_layer

- name: Create another lambda layer
lambda_layer:
name: "{{ lambda_python_layers_names[1] }}"
description: '{{ lambda_python_layers_names[1] }} lambda layer'
content:
zip_file: "{{ remote_tmp_dir }}/lambda_layer.zip"
register: second_layer

- name: Create lambda function with layers
lambda:
name: '{{ lambda_function_name_with_layer }}'
runtime: '{{ lambda_python_runtime }}'
handler: '{{ lambda_python_handler }}'
role: '{{ lambda_role_name }}'
zip_file: '{{ zip_res.dest }}'
layers:
- layer_version_arn: "{{ first_layer.layer_versions.0.layer_version_arn }}"
register: result
- name: Validate that lambda function was created with expected property
assert:
that:
- result is changed
- '"layers" in result.configuration'
- result.configuration.layers | length == 1
- result.configuration.layers.0.arn == first_layer.layer_versions.0.layer_version_arn

- name: Create lambda function with layers once again (validate idempotency)
lambda:
name: '{{ lambda_function_name_with_layer }}'
runtime: '{{ lambda_python_runtime }}'
handler: '{{ lambda_python_handler }}'
role: '{{ lambda_role_name }}'
zip_file: '{{ zip_res.dest }}'
layers:
- layer_version_arn: "{{ first_layer.layer_versions.0.layer_version_arn }}"
register: result
- name: Validate that no change were made
assert:
that:
- result is not changed

- name: Create lambda function with mutiple layers
lambda:
name: '{{ lambda_function_name_with_multiple_layer }}'
runtime: '{{ lambda_python_runtime }}'
handler: '{{ lambda_python_handler }}'
role: '{{ lambda_role_name }}'
zip_file: '{{ zip_res.dest }}'
layers:
- layer_version_arn: "{{ first_layer.layer_versions.0.layer_version_arn }}"
- layer_name: "{{ second_layer.layer_versions.0.layer_arn }}"
version: "{{ second_layer.layer_versions.0.version }}"
register: result
- name: Validate that lambda function was created with expected property
assert:
that:
- result is changed
- '"layers" in result.configuration'
- result.configuration.layers | length == 2
- first_layer.layer_versions.0.layer_version_arn in lambda_layer_versions
- second_layer.layer_versions.0.layer_version_arn in lambda_layer_versions
vars:
lambda_layer_versions: "{{ result.configuration.layers | map(attribute='arn') | list }}"

- name: Create lambda function with mutiple layers and changing layers order (idempotency)
lambda:
name: '{{ lambda_function_name_with_multiple_layer }}'
runtime: '{{ lambda_python_runtime }}'
handler: '{{ lambda_python_handler }}'
role: '{{ lambda_role_name }}'
zip_file: '{{ zip_res.dest }}'
layers:
- layer_version_arn: "{{ second_layer.layer_versions.0.layer_version_arn }}"
- layer_name: "{{ first_layer.layer_versions.0.layer_arn }}"
version: "{{ first_layer.layer_versions.0.version }}"
register: result
- name: Validate that lambda function was created with expected property
assert:
that:
- result is not changed

always:

- name: Delete lambda layers
lambda_layer:
name: "{{ item }}"
version: -1
state: absent
ignore_errors: true
with_items: "{{ lambda_python_layers_names }}"

- name: ensure functions are absent at end of test
lambda:
name: '{{ item }}'
Expand Down