Skip to content

Commit

Permalink
lambda - add support for layers when creating or updating lambda func…
Browse files Browse the repository at this point in the history
…tions (ansible-collections#1118)

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

Depends-On: ansible-collections#1095
SUMMARY

add the ability for module lambda to create/update a lambda function with layers attached

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

lambda
ADDITIONAL INFORMATION



- lambda:
    name: test-func
    runtime: python3.7
    (...)
    layers:
       - layer_version_arn: 'arn:aws:lambda:us-east-1:123456789012:layer:python27-env:7'
       - layer_name: pycustom-library
         version: 1

Reviewed-by: Alina Buzachis <None>
Reviewed-by: Bikouo Aubin <None>
  • Loading branch information
abikouo authored and alinabuzachis committed Apr 27, 2023
1 parent 67bc04d commit 38f2a77
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 0 deletions.
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 @@ -120,6 +120,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.
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 @@ -179,6 +204,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 @@ -329,12 +366,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 @@ -394,6 +455,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 @@ -553,6 +625,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 @@ -595,6 +682,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 @@ -616,6 +704,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 @@ -677,6 +773,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 @@ -761,6 +864,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 @@ -604,9 +643,132 @@
assert:
that:
- result is not failed

# Test creation with layers
- name: Create temporary directory for testing
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

0 comments on commit 38f2a77

Please sign in to comment.