diff --git a/README.md b/README.md index 28acdedb..010f05ac 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Name | Description [cloud.aws_ops.enable_cloudtrail_encryption_with_kms](https://github.com/ansible-collections/cloud.aws_ops/blob/main/roles/enable_cloudtrail_encryption_with_kms/README.md)|A role to encrypt an AWS CloudTrail trail using the AWS Key Management Service (AWS KMS) customer managed key you specify. [cloud.aws_ops.manage_vpc_peering](https://github.com/ansible-collections/cloud.aws_ops/blob/main/roles/manage_vpc_peering/README.md)|A role to create, delete and accept existing VPC peering connections. [cloud.aws_ops.moving_objects_between_buckets](https://github.com/ansible-collections/cloud.aws_ops/blob/main/roles/moving_objects_between_buckets/README.md)|A role to move objects from one bucket to another bucket. +[cloud.aws_ops.awsconfig_apigateway_with_lambda_integration](https://github.com/ansible-collections/cloud.aws_ops/blob/main/roles/awsconfig_apigateway_with_lambda_integration/README.md)|A role to create/delete an API gateway with lambda function integration. ### Playbooks diff --git a/changelogs/fragments/awsconfig_apigateway.yml b/changelogs/fragments/awsconfig_apigateway.yml new file mode 100644 index 00000000..34c49bda --- /dev/null +++ b/changelogs/fragments/awsconfig_apigateway.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - awsconfig_apigateway_with_lambda_integration - new role to create API gateway with Lambda integration diff --git a/roles/awsconfig_apigateway_with_lambda_integration/README.md b/roles/awsconfig_apigateway_with_lambda_integration/README.md new file mode 100644 index 00000000..c59c3d66 --- /dev/null +++ b/roles/awsconfig_apigateway_with_lambda_integration/README.md @@ -0,0 +1,49 @@ +# awsconfig_apigateway_with_lambda_integration + +A role to create/delete an API gateway with lambda function integration. +the role produces variables **awsconfig_apigateway_with_lambda_integration\_\_invoke_url** that contains the URL to invoke API gateway and **awsconfig_apigateway_with_lambda_integration\_\_id** that contains the id of the API gateway created. + +## Requirements + +AWS User Account with permission to create API gateway, lambda function and IAM role. + +## Role Variables + +- **awsconfig_apigateway_with_lambda_integration_operation**: Whether to create or delete the API gateway. Choices: 'create', 'delete'. Default: 'create'. +- **awsconfig_apigateway_with_lambda_integration_api_name**: The name of the API gateway to create/delete. +- **awsconfig_apigateway_with_lambda_integration_id**: string identifier of the API gateway to update/delete. +- **awsconfig_apigateway_with_lambda_integration_tags**: collection of tags associated to the API gateway, this is used to ensure unique API gateway is created/deleted while running multiple times. Provided as dictionnary. +- **awsconfig_apigateway_with_lambda_integration_lambda_runtime**: The lambda function runtime. e.g: 'python3.8' +- **awsconfig_apigateway_with_lambda_integration_lambda_function_file**: The path to a valid file containing the code of the lambda function. +- **awsconfig_apigateway_with_lambda_integration_lambda_handler**: The lambda function handler. e.g: 'hello.lambda_handler' +- **awsconfig_apigateway_with_lambda_integration_stage_name**: The name for the Stage resource. Stage names can only contain alphanumeric characters, hyphens, and underscores. Maximum length is 128 characters. + +## Dependencies + +- role: [aws_setup_credentials](../aws_setup_credentials/README.md) + +## Example Playbook + + - hosts: localhost + roles: + - role: cloud.aws_ops.awsconfig_apigateway_with_lambda_integration + aws_access_key: xxxxxxxxxxx + aws_secret_key: xxxxxxxxxxx + aws_region: xxxxxxxx + awsconfig_apigateway_with_lambda_integration_operation: create + awsconfig_apigateway_with_lambda_integration_api_name: hello + awsconfig_apigateway_with_lambda_integration_tags: + automation: ansible + awsconfig_apigateway_with_lambda_integration_lambda_runtime: 'python3.8' + awsconfig_apigateway_with_lambda_integration_lambda_handler: 'hello.lambda_handler' + awsconfig_apigateway_with_lambda_integration_lambda_function_file: hello.py + +## License + +GNU General Public License v3.0 or later + +See [LICENCE](https://github.com/ansible-collections/cloud.aws_ops/blob/main/LICENSE) to see the full text. + +## Author Information + +- Ansible Cloud Content Team diff --git a/roles/awsconfig_apigateway_with_lambda_integration/defaults/main.yml b/roles/awsconfig_apigateway_with_lambda_integration/defaults/main.yml new file mode 100644 index 00000000..1577c570 --- /dev/null +++ b/roles/awsconfig_apigateway_with_lambda_integration/defaults/main.yml @@ -0,0 +1,6 @@ +--- +awsconfig_apigateway_with_lambda_integration_operation: create +awsconfig_apigateway_with_lambda_integration_default_api_name: "ansible-api" +awsconfig_apigateway_with_lambda_integration__iam_role_name: "{{ awsconfig_apigateway_with_lambda_integration_api_name | default(awsconfig_apigateway_with_lambda_integration_default_api_name) }}-role" +awsconfig_apigateway_with_lambda_integration__lambda_name: "{{ awsconfig_apigateway_with_lambda_integration_api_name | default(awsconfig_apigateway_with_lambda_integration_default_api_name) }}-lambda" +awsconfig_apigateway_with_lambda_integration__awsregion: "{{ aws_setup_credentials__output.aws_region }}" diff --git a/roles/awsconfig_apigateway_with_lambda_integration/files/lambda_trust_policy.json b/roles/awsconfig_apigateway_with_lambda_integration/files/lambda_trust_policy.json new file mode 100644 index 00000000..df95399b --- /dev/null +++ b/roles/awsconfig_apigateway_with_lambda_integration/files/lambda_trust_policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Sid": "", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} \ No newline at end of file diff --git a/roles/awsconfig_apigateway_with_lambda_integration/meta/main.yml b/roles/awsconfig_apigateway_with_lambda_integration/meta/main.yml new file mode 100644 index 00000000..e8b3ab42 --- /dev/null +++ b/roles/awsconfig_apigateway_with_lambda_integration/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: cloud.aws_ops.aws_setup_credentials diff --git a/roles/awsconfig_apigateway_with_lambda_integration/tasks/apigateway.yml b/roles/awsconfig_apigateway_with_lambda_integration/tasks/apigateway.yml new file mode 100644 index 00000000..03e145de --- /dev/null +++ b/roles/awsconfig_apigateway_with_lambda_integration/tasks/apigateway.yml @@ -0,0 +1,85 @@ +--- +- name: Create API gateway + when: awsconfig_apigateway_with_lambda_integration_operation == 'create' + block: + - name: Get AWS caller info + amazon.aws.aws_caller_info: + register: awsconfig_apigateway_with_lambda_integration__caller_info + + - name: Create temporary file + ansible.builtin.tempfile: + suffix: .json + register: awsconfig_apigateway_with_lambda_integration__swagger_file + + - name: Generate swagger content + ansible.builtin.template: + src: 'swagger.json.j2' + dest: "{{ awsconfig_apigateway_with_lambda_integration__swagger_file.path }}" + mode: 0755 + + - name: Create API gateway + community.aws.api_gateway: + state: present + api_file: "{{ awsconfig_apigateway_with_lambda_integration__swagger_file.path }}" + stage: "{{ awsconfig_apigateway_with_lambda_integration_stage_name }}" + endpoint_type: REGIONAL + api_id: "{{ awsconfig_apigateway_with_lambda_integration_id | default(omit) }}" + name: "{{ awsconfig_apigateway_with_lambda_integration_api_name | default(awsconfig_apigateway_with_lambda_integration_default_api_name) }}" + tags: "{{ awsconfig_apigateway_with_lambda_integration_tags | default(omit) }}" + lookup: "{{ awsconfig_apigateway_with_lambda_integration_id is defined | ternary('id', 'tag') }}" + register: awsconfig_apigateway_with_lambda_integration__create_apigateway + + - name: Define API gateway id as variable + ansible.builtin.set_fact: + awsconfig_apigateway_with_lambda_integration__id: "{{ awsconfig_apigateway_with_lambda_integration__create_apigateway.api_id }}" + + - name: Give API gateway permission to invoke lambda function + amazon.aws.lambda_policy: + state: present + function_name: "{{ awsconfig_apigateway_with_lambda_integration__lambda_name }}" + statement_id: "AllowExecutionFromAPIGateway" + action: "lambda:InvokeFunction" + principal: "apigateway.amazonaws.com" + source_arn: "arn:aws:execute-api:{{ awsconfig_apigateway_with_lambda_integration__awsregion }}:{{ awsconfig_apigateway_with_lambda_integration__caller_info.account }}:{{ awsconfig_apigateway_with_lambda_integration__id }}/*/*" + + - name: Define API gateway invoke url and API gateway identifier + ansible.builtin.set_fact: + awsconfig_apigateway_with_lambda_integration__invoke_url: "https://{{ awsconfig_apigateway_with_lambda_integration__id }}.execute-api.{{ awsconfig_apigateway_with_lambda_integration__awsregion }}.amazonaws.com/{{ awsconfig_apigateway_with_lambda_integration_stage_name }}" + + always: + - name: Delete temporary file + ansible.builtin.file: + state: absent + path: "{{ awsconfig_apigateway_with_lambda_integration__swagger_file }}" + ignore_errors: true + when: awsconfig_apigateway_with_lambda_integration__swagger_file is defined + +- name: Delete API gateway + when: awsconfig_apigateway_with_lambda_integration_operation == 'delete' + block: + - name: Ensure at least one of API gateway id or tags is provided to delete API gateway + ansible.builtin.fail: + msg: "At least one of API gateway id or tags should be supplied when trying to delete API gateway id" + when: + - awsconfig_apigateway_with_lambda_integration_id is not defined + - awsconfig_apigateway_with_lambda_integration_tags is not defined + + - name: Delete API Gateway using identifier + when: awsconfig_apigateway_with_lambda_integration_id is defined + block: + - name: Get API gateway info + community.aws.api_gateway_info: + register: awsconfig_apigateway_with_lambda_integration__rest_apis + + - name: Delete API gateway using identifier + community.aws.api_gateway: + state: absent + api_id: "{{ awsconfig_apigateway_with_lambda_integration_id }}" + when: awsconfig_apigateway_with_lambda_integration__rest_apis.rest_apis | selectattr('id', 'equalto', awsconfig_apigateway_with_lambda_integration_id) | list | length > 0 + + - name: Delete API gateway using tags + community.aws.api_gateway: + state: absent + name: "{{ awsconfig_apigateway_with_lambda_integration_api_name | default(awsconfig_apigateway_with_lambda_integration_default_api_name) }}" + tags: "{{ awsconfig_apigateway_with_lambda_integration_tags }}" + lookup: "tag" diff --git a/roles/awsconfig_apigateway_with_lambda_integration/tasks/lambda.yml b/roles/awsconfig_apigateway_with_lambda_integration/tasks/lambda.yml new file mode 100644 index 00000000..9a262bb6 --- /dev/null +++ b/roles/awsconfig_apigateway_with_lambda_integration/tasks/lambda.yml @@ -0,0 +1,70 @@ +--- +- name: Create Lambda resources + when: awsconfig_apigateway_with_lambda_integration_operation == 'create' + block: + - name: Create role for lambda function + community.aws.iam_role: + name: "{{ awsconfig_apigateway_with_lambda_integration__iam_role_name }}" + assume_role_policy_document: '{{ lookup("file", "lambda_trust_policy.json") }}' + create_instance_profile: false + managed_policies: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + register: awsconfig_apigateway_with_lambda_integration__iam_role + + - name: Wait for IAM role to be available + ansible.builtin.pause: + seconds: 10 + when: awsconfig_apigateway_with_lambda_integration__iam_role is changed + + - name: Create Lambda function + block: + - name: Create temporary zip file + ansible.builtin.tempfile: + suffix: .zip + register: awsconfig_apigateway_with_lambda_integration__tmp_file + + - name: Create lambda function archive + community.general.archive: + format: zip + path: '{{ awsconfig_apigateway_with_lambda_integration_lambda_function_file }}' + dest: '{{ awsconfig_apigateway_with_lambda_integration__tmp_file.path }}' + mode: 0755 + + - name: Upload lambda function + amazon.aws.lambda: + name: "{{ awsconfig_apigateway_with_lambda_integration__lambda_name }}" + runtime: "{{ awsconfig_apigateway_with_lambda_integration_lambda_runtime | default(omit) }}" + handler: '{{ awsconfig_apigateway_with_lambda_integration_lambda_handler | default(omit) }}' + role: "{{ awsconfig_apigateway_with_lambda_integration__iam_role_name }}" + zip_file: "{{ awsconfig_apigateway_with_lambda_integration__tmp_file.path }}" + register: awsconfig_apigateway_with_lambda_integration__updload_lambda + + - name: Ensure lambda function works + amazon.aws.lambda_execute: + name: "{{ awsconfig_apigateway_with_lambda_integration__lambda_name }}" + payload: + name: simple content to my lambda function + + - name: Save lambda function ARN + ansible.builtin.set_fact: + awsconfig_apigateway_with_lambda_integration__lambda_arn: "{{ awsconfig_apigateway_with_lambda_integration__updload_lambda.configuration.function_arn }}" + + always: + - name: Delete temporary file + ansible.builtin.file: + state: absent + path: "{{ awsconfig_apigateway_with_lambda_integration__tmp_file.path }}" + ignore_errors: true + +- name: Delete Lambda resources + when: awsconfig_apigateway_with_lambda_integration_operation == 'delete' + block: + - name: Delete Lambda function + amazon.aws.lambda: + name: "{{ awsconfig_apigateway_with_lambda_integration__lambda_name }}" + state: absent + + - name: Delete IAM role name + community.aws.iam_role: + name: "{{ awsconfig_apigateway_with_lambda_integration__iam_role_name }}" + state: absent diff --git a/roles/awsconfig_apigateway_with_lambda_integration/tasks/main.yml b/roles/awsconfig_apigateway_with_lambda_integration/tasks/main.yml new file mode 100644 index 00000000..b6ba8568 --- /dev/null +++ b/roles/awsconfig_apigateway_with_lambda_integration/tasks/main.yml @@ -0,0 +1,10 @@ +--- +- name: Create/Delete API gateway with lambda integration + module_defaults: + group/aws: "{{ aws_setup_credentials__output }}" + block: + - name: "Include file {{ item }}" + ansible.builtin.include_tasks: '{{ item }}' + with_items: + - lambda.yml + - apigateway.yml diff --git a/roles/awsconfig_apigateway_with_lambda_integration/templates/swagger.json.j2 b/roles/awsconfig_apigateway_with_lambda_integration/templates/swagger.json.j2 new file mode 100644 index 00000000..282e4a83 --- /dev/null +++ b/roles/awsconfig_apigateway_with_lambda_integration/templates/swagger.json.j2 @@ -0,0 +1,43 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "v1.0", + "title": "{{ awsconfig_apigateway_with_lambda_integration_api_name | default(awsconfig_apigateway_with_lambda_integration_default_api_name) }}" + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "parameters": [ + { + "name": "proxy", + "in": "path", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": {}, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200" + } + }, + "requestTemplates": { + "application/json": "{}" + }, + "uri": "arn:aws:apigateway:{{ awsconfig_apigateway_with_lambda_integration__awsregion }}:lambda:path/2015-03-31/functions/arn:aws:lambda:{{ awsconfig_apigateway_with_lambda_integration__awsregion }}:{{ awsconfig_apigateway_with_lambda_integration__caller_info.account }}:function:{{ awsconfig_apigateway_with_lambda_integration__lambda_name }}/invocations", + "passthroughBehavior": "when_no_match", + "httpMethod": "POST", + "type": "aws_proxy" + } + } + } + }, + "definitions": { + "Empty": { + "type": "object" + } + } +} \ No newline at end of file diff --git a/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/aliases b/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/aliases new file mode 100644 index 00000000..a203fdac --- /dev/null +++ b/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/aliases @@ -0,0 +1,3 @@ +cloud/aws +role/awsconfig_apigateway_with_lambda_integration +time=1m \ No newline at end of file diff --git a/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/defaults/main.yml b/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/defaults/main.yml new file mode 100644 index 00000000..d88ffbc1 --- /dev/null +++ b/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/defaults/main.yml @@ -0,0 +1,8 @@ +--- +aws_security_token: '{{ security_token | default(omit) }}' +awsconfig_apigateway_with_lambda_integration_api_name: "ansible-test-{{ resource_prefix }}" +awsconfig_apigateway_with_lambda_integration_tags: + resource_prefix: "{{ resource_prefix }}" +awsconfig_apigateway_with_lambda_integration_lambda_runtime: "python3.8" +awsconfig_apigateway_with_lambda_integration_lambda_handler: 'server.lambda_handler' +awsconfig_apigateway_with_lambda_integration_stage_name: test diff --git a/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/files/server.py b/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/files/server.py new file mode 100644 index 00000000..9d7bc083 --- /dev/null +++ b/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/files/server.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import json + + +def lambda_handler(event, context): + resource_prefix = event.get("queryStringParameters", {}).get("resource_prefix") + path = event.get("pathParameters", {}).get("proxy") + + message = "" + if path == "ansible-test": + message = "Running ansible-test with Resource prefix {0}".format( + resource_prefix + ) + + return { + "statusCode": 200, + "body": json.dumps(message, indent=2), + "headers": {"Content-Type": "application/json"}, + } diff --git a/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/tasks/main.yml b/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/tasks/main.yml new file mode 100644 index 00000000..e7a97fb5 --- /dev/null +++ b/tests/integration/targets/test_awsconfig_apigateway_with_lambda_integration/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Test Role 'awsconfig_apigateway_with_lambda_integration' + vars: + awsconfig_apigateway_with_lambda_integration_lambda_function_file: "{{ role_path }}/files/server.py" + block: + - name: Create temporary directory for test + ansible.builtin.tempfile: + suffix: .py + state: directory + register: __tempdir + + - name: Copy temporary file + ansible.builtin.copy: + src: server.py + dest: "{{ __tempdir.path }}" + mode: 0755 + + # Test create API gateway + - name: Create API Gateway + ansible.builtin.include_role: + name: cloud.aws_ops.awsconfig_apigateway_with_lambda_integration + vars: + awsconfig_apigateway_with_lambda_integration_lambda_function_file: "{{ __tempdir.path }}/server.py" + + - name: Ensure the role has defined the invoke url as output + ansible.builtin.assert: + that: + - awsconfig_apigateway_with_lambda_integration__invoke_url is defined + + - name: Calling URL using random path + ansible.builtin.uri: + url: "{{ awsconfig_apigateway_with_lambda_integration__invoke_url }}/main?resource_prefix={{ resource_prefix }}" + register: __uri_result + + - name: Ensure server returns empty result + ansible.builtin.assert: + that: + - __uri_result.json == "" + + - name: Calling URL using ansible-test path + ansible.builtin.uri: + url: "{{ awsconfig_apigateway_with_lambda_integration__invoke_url }}/ansible-test?resource_prefix={{ resource_prefix }}" + register: __uri_result + + - name: Ensure result is as expected + ansible.builtin.assert: + that: + - __uri_result.json == "Running ansible-test with Resource prefix {{ resource_prefix }}" + + # Test: Delete API gateway + - name: Delete API Gateway + ansible.builtin.include_role: + name: cloud.aws_ops.awsconfig_apigateway_with_lambda_integration + vars: + awsconfig_apigateway_with_lambda_integration_operation: delete + + - name: Validate that URL is not valid after the API Gateway has been deleted + ansible.builtin.uri: + url: "{{ awsconfig_apigateway_with_lambda_integration__invoke_url }}/ansible-test?resource_prefix={{ resource_prefix }}" + ignore_errors: true + register: __uri_result + failed_when: __uri_result is successful + + always: + - name: Delete API Gateway + ansible.builtin.include_role: + name: cloud.aws_ops.awsconfig_apigateway_with_lambda_integration + vars: + awsconfig_apigateway_with_lambda_integration_operation: delete + awsconfig_apigateway_with_lambda_integration_id: "{{ awsconfig_apigateway_with_lambda_integration__id }}" + when: awsconfig_apigateway_with_lambda_integration__id is defined + + - name: Delete temporary directory + ansible.builtin.file: + path: "{{ __tempdir.path }}" + state: absent + ignore_errors: true