diff --git a/content/articles/99999981-aws-cloud-development-kit.md b/content/articles/99999981-aws-cloud-development-kit.md index 79c1797..c0bc486 100644 --- a/content/articles/99999981-aws-cloud-development-kit.md +++ b/content/articles/99999981-aws-cloud-development-kit.md @@ -144,13 +144,13 @@ class CdkTutorialStack(cdk.Stack): "cdk-tutorial", code=aws_lambda.Code.from_asset("lambda"), handler="index.main", - runtime=aws_lambda.Runtime.PYTHON_3_8, + runtime=aws_lambda.Runtime.PYTHON_3_10, layers=[self.create_dependencies_layer(self.stack_name, "lambda/index")], ) ``` Here we use `code` variable to import our code from `lambda` folder, then define Lambda handler to be `main()` method under `index.py` file by using `handler="index.main"`. -We also define the runtime to be Python 3.8 and a layer that is explained later. +We also define the runtime to be Python 3.10 and a layer that is explained later. ## Define EventBridge Schedules and Lambda access permission Add the below code under where we defined lambda function diff --git a/content/aws/50001000-cdk-fn-create-lambda.md b/content/aws/50001000-cdk-fn-create-lambda.md new file mode 100644 index 0000000..021d5c6 --- /dev/null +++ b/content/aws/50001000-cdk-fn-create-lambda.md @@ -0,0 +1,211 @@ +Title: How to create a lambda function using AWS CDK in Python +Date: 2023-11-05 +Category: AWS Academy +Series: AWS CDK +series_index: 1000 +Tags: aws, cdk, python +Author: Rehan Haider +Summary: A complete guide to creating a lambda function using AWS CDK +Keywords: lambda, cdk + + +The easiest way to create a lambda function using AWS CDK is to use the `Function` construct from the `aws_lambda` module. Let's see how to use it. + +## Create a simple lambda function + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) + +from constructs import Construct + + +class LambdaStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + inline_code = """ +def handler(event, context): + return { + "statusCode": 200, + "body": "Hello from Lambda!" + } + """ + # 👆🏽 formatting & indentation is not a mistake + + my_lambda = _lambda.Function( + self, + id="MyLambda", + runtime=_lambda.Runtime.PYTHON_3_10, + handler="index.handler", + code=_lambda.Code.from_inline(inline_code), + ) + +``` + +You can create the app by modifying the `app.py` file as follows: + +```python +# filename: app.py + +import aws_cdk as cdk +from cdk_app.lambda_stack import LambdaStack + +app = cdk.App() + +lambda_stack = LambdaStack(app, "LambdaStack") + +app.synth() +``` + +To deploy, run `cdk deploy`. + +Once deployed, you can go to the AWS Console and check the lambda function. You should see the following: + +![Lambda inline function]({static}/images/aws/50001000-01-fn-inline-code.png) + +### What's happening here? + +We used what is known as an **inline code** to create the lambda function. Effectively, the code we want to execute is passed as a string to the `code` property of the `Function` construct. + +The `Function` construct takes the following parameters: + +- `scope`: The scope in which the construct is created. In our case, it is the `LambdaStack` class. +- `id`: The ID of the construct. This ID is used within CDK to uniquely identify the construct. It is also used as the logical ID in CloudFormation templates. +- `runtime`: The runtime environment for the lambda function. In our case, it is Python 3.10. Other versions of Python are also supported. You can also use other runtimes such as Node.js, Java, Go, etc. +- `handler`: The name of the handler function. In our case, it is `index.handler`. This means that the `handler` function is defined in the `index.py` file. +- `code`: The code to execute. In our case, it is the `inline_code` variable. + + +But using inline code is not the best way to create a lambda function. It is fine for simple demonstrations, but for anything more complex, you should use a file or a Docker image. Let's see how to do that. + + +## Create a lambda function from a file + +To create a lambda function from a file, you need to use the `Code.from_asset` method. This method takes the path to the file as an argument. + +Now one important point, the lambda code needs to be in its own directory. So, you need to create a directory and put the lambda code in that directory. + +E.g. in our case, we will create a `cdk_app/lambda` directory and create a file named `index.py` in that directory. The contents of the file will be as follows: + +```python +# filename: cdk_app/lambda/index.py + +def handler(event, context): + return { + "statusCode": 200, + "body": "Hello from Lambda!", + } +``` + +Now, we can create the lambda function as follows: + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) + +from constructs import Construct + + +class LambdaStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_lambda = _lambda.Function( + self, + id="MyLambda", + runtime=_lambda.Runtime.PYTHON_3_10, + handler="index.handler", + code=_lambda.Code.from_asset("cdk_app/lambda"), + ) +``` + +Notice that we have used the `Code.from_asset` method to create the lambda function. We have passed the path to the `lambda` directory as an argument to the method. + +Now, when you run `cdk deploy`, you will see that the lambda function is created from the `index.py` file. + +## Set environment variables + +You can set environment variables for the lambda function by using the `environment` property of the `Function` construct. + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) + +from constructs import Construct + + +class LambdaStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_lambda = _lambda.Function( + self, + id="MyLambda", + runtime=_lambda.Runtime.PYTHON_3_10, + handler="index.handler", + code=_lambda.Code.from_asset("cdk_app/lambda"), + # 👇🏽 set environment variables + environment={ + "ENVIRONMENT": "PROD", + }, + ) +``` + +## Set memory size and timeout + +By default lambda provide 128 MB of memory and 3 seconds of timeout. That means if your lambda function takes more than 3 seconds to execute, it will timeout. + +If your lambda function requires more memory or more time, you can set the `memory_size` and `timeout` properties of the `Function` construct. + + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, + Duration, +) + +from constructs import Construct + + +class LambdaStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_lambda = _lambda.Function( + self, + id="MyLambda", + runtime=_lambda.Runtime.PYTHON_3_10, + handler="index.handler", + code=_lambda.Code.from_asset("cdk_app/lambda"), + # 👇🏽 set memory size and timeout + memory_size=256, + timeout=Duration.seconds(10), + ) +``` + + +## Additional resources for lambda functions + +Or learn some advanced methods on how to create lambda functions: + +1. [Import an existing lambda function]({filename}50001010-cdk-fn-import-lambda.md) +2. [Using lambda layers]({filename}50001020-cdk-fn-lambda_layers.md) + + + diff --git a/content/aws/50001010-cdk-fn-import-lambda.md b/content/aws/50001010-cdk-fn-import-lambda.md new file mode 100644 index 0000000..0f80946 --- /dev/null +++ b/content/aws/50001010-cdk-fn-import-lambda.md @@ -0,0 +1,134 @@ +Title: How to import an existing lambda function using AWS CDK in Python +Date: 2023-11-05 +Category: AWS Academy +Series: AWS CDK +series_index: 1010 +Tags: aws, cdk, python +Author: Rehan Haider +Summary: A guide to importing an existing lambda function using AWS CDK in Python and applying changes to it +Keywords: lambda, cdk + + +We learnt [how to create a new lambda function using AWS CDK in Python]({filename}50001000-cdk-fn-create-lambda.md) in the previous post. + +But in a lot of cases, you might already have a lambda function that was created elsewhere and you want to import into your CDK app and apply changes to it. In this post, we will see how to do that. + +There are three ways to import an existing lambda function, can use: + +1. `from_function_arn`: This relies on the ARN of the lambda function. +2. `from_function_name`: You can also import the lambda function using its name. +3. `from_function_attributes`: This is typically used to import a lambda function that was created in a different AWS account or a different region. + + +## Import an existing lambda function using its ARN + +To import an existing lambda function using its ARN, you can use the `from_function_arn` method of the `Function` construct. Let's see how to do that. + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) + +from constructs import Construct + + +class LambdaStack(Stack): + + FN_ARN = "arn:aws:lambda:us-east-1:123456789012:function:my-lambda" + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_lambda = _lambda.Function.from_function_arn( + self, + id="MyLambda", + function_arn=self.FN_ARN, + ) +``` + +The `from_function_arn` method takes only three parameters: + +1. `scope`: The scope of the construct. In this case, it is the `LambdaStack` class. +2. `id`: The ID that will be used within CDK/CloudFormation. In this case, we set it to `MyLambda` but it could be anything. +3. `function_arn`: The ARN of the lambda function that you want to import. + +## Import an existing lambda function using its name + + +To import an existing lambda function using its name, you can use the `from_function_name` method of the `Function` construct. Let's see how to do that. + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) + +from constructs import Construct + + +class LambdaStack(Stack): + + FN_NAME = "my-lambda" + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_lambda = _lambda.Function.from_function_name( + self, + id="MyLambda", + function_name=self.FN_NAME, + ) +``` + +The `from_function_name` method takes only three parameters: + +1. `scope`: The scope of the construct. In this case, it is the `LambdaStack` class. +2. `id`: The ID that will be used within CDK/CloudFormation. In this case, we set it to `MyLambda` but it could be anything. +3. `function_name`: The name of the lambda function that you want to import. + + +## Import an existing lambda function using its attributes + +This is typically used to import a lambda function when you have the Lambda function's ARN and you also need to specify or override other attributes. E.g. for instance, if you need to specify the execution role's ARN because you want to grant permissions or interact with the role in some way within your CDK app, this method would be more suitable. + +To import an existing lambda function using its attributes, you can use the `from_function_attributes` method of the `Function` construct. Let's see how to do that. + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, + aws_iam as iam, +) + +from constructs import Construct + + +class LambdaStack(Stack): + FN_ARN = "arn:aws:lambda:us-east-1:123456789012:function:my-lambda" + ROLE_ARN = "arn:aws:iam::123456789012:role/my-lambda-role" + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_role = iam.Role.from_role_arn( + self, + id="MyRole", + role_arn=self.ROLE_ARN, + ) + + my_lambda = _lambda.Function.from_function_attributes( + self, + id="MyLambda", + function_arn=self.FN_ARN, + role=my_role, + ) +``` + +In the above we provided the `role` parameter to the `from_function_attributes` method. This is because we wanted to override the execution role of the lambda function. To do that, we first imported the role using the `Role.from_role_arn` method. \ No newline at end of file diff --git a/content/aws/50001020-cdk-fn-lambda_layers.md b/content/aws/50001020-cdk-fn-lambda_layers.md new file mode 100644 index 0000000..0eff184 --- /dev/null +++ b/content/aws/50001020-cdk-fn-lambda_layers.md @@ -0,0 +1,254 @@ +Title: Using Lambda Layers with AWS CDK in Python +Date: 2023-11-05 +Category: AWS Academy +Series: AWS CDK +series_index: 1020 +Tags: aws, cdk, python +Author: Rehan Haider +Summary: Using Lambda layers with AWS CDK in Python to handle dependencies and share code between lambda functions +Keywords: lambda, cdk, layers + +While we created some simple lambda functions in a [previous post]({filename}50001000-cdk-fn-create-lambda.md), but in most cases, you will need to use some external libraries or dependencies in your lambda functions. + +E.g., let's modify our `cdk_app/lambda/index.file` to use the `requests` library: + +```python +# filename: cdk_app/lambda/index.py + +import requests + +def handler(event, context): + response = requests.get("https://jsonplaceholder.typicode.com/todos/1") + return { + "statusCode": 200, + "body": response.json() + } +``` +We put a `requirements.txt` file in the same directory as our `index.py` file with the following contents: + +```text +# filename: cdk_app/lambda/requirements.txt +requests +``` + +Then we can create a lambda function using this code as follows: + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) + +from constructs import Construct + + +class LambdaStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_lambda = _lambda.Function( + self, + id="MyLambda", + runtime=_lambda.Runtime.PYTHON_3_10, + handler="index.handler", + code=_lambda.Code.from_asset("cdk_app/lambda"), + ) +``` + +After deploying the stack if you try to invoke the lambda function, you will get an error: + +![lambda requirement import error]({static}/images/aws/50001020-01-fn-lambda-import-error.png) + + +This is because by default, the lambda function does not have the `requests` library installed. To fix this, we need to create a lambda layer that contains the `requests` library and then add that layer to our lambda function. + +## When to use lambda layers + +Lambda layers are useful in the following scenarios: + +1. [**Handle dependencies**](#create-a-lambda-layer-in-aws-cdk-using-python-to-handle-dependencies): If you need to use some external libraries or dependencies in your lambda function, you can put those dependencies in a lambda layer and then add that layer to your lambda function. +2. **Reuse code between lambda functions**: If you have some common code that you want to use in multiple lambda functions, you can put that code in a lambda layer and then add that layer to all the lambda functions that need to use that code. + + + +## Create a lambda layer in AWS CDK using Python to handle dependencies + +Essentially, handling dependencies means we have to download the dependencies and put them in a folder. Then we need to zip that folder and upload it to AWS. + +We will create a function within our stack that will do this work for us, and then import that layer into our lambda function. + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) + +from constructs import Construct + +import os, subprocess # 👈🏽 needed to download dependencies + + +class LambdaStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_lambda = _lambda.Function( + self, + id="MyLambda", + runtime=_lambda.Runtime.PYTHON_3_10, + handler="index.handler", + code=_lambda.Code.from_asset("cdk_app/lambda"), + layers=[self.create_dependencies_layer(self.stack_name, "lambda/index")], + ) + + def create_dependencies_layer(self, project_name, function_name: str) -> _lambda.LayerVersion: + requirements_file = "cdk_app/lambda/requirements.txt" # 👈🏽 point to requirements.txt + output_dir = f".build/app" # 👈🏽 a temporary directory to store the dependencies + + if not os.environ.get("SKIP_PIP"): + # 👇🏽 download the dependencies and store them in the output_dir + subprocess.check_call(f"pip install -r {requirements_file} -t {output_dir}/python".split()) + + layer_id = f"{project_name}-{function_name}-dependencies" # 👈🏽 a unique id for the layer + layer_code = _lambda.Code.from_asset(output_dir) # 👈🏽 import the dependencies / code + + my_layer = _lambda.LayerVersion( + self, + layer_id, + code=layer_code, + ) + + return my_layer +``` + +Let's go through the code above: + +```python +import os, subprocess # 👈🏽 needed to download dependencies +``` + +We imported the `os` and `subprocess` modules. We will use these modules to download the dependencies and store them in a temporary directory. + +```python + my_lambda = _lambda.Function( + self, + id="MyLambda", + runtime=_lambda.Runtime.PYTHON_3_10, + handler="index.handler", + code=_lambda.Code.from_asset("cdk_app/lambda"), + layers=[self.create_dependencies_layer(self.stack_name, "lambda/index")], + ) +``` + +Here, we defined the `layers` which is to be passed as a list. We are calling the `create_dependencies_layer` function to create the layer. + +```python + def create_dependencies_layer(self, project_name, function_name: str) -> _lambda.LayerVersion: + requirements_file = "cdk_app/lambda/requirements.txt" # 👈🏽 point to requirements.txt + output_dir = f".build/app" # 👈🏽 a temporary directory to store the dependencies + + if not os.environ.get("SKIP_PIP"): + # 👇🏽 download the dependencies and store them in the output_dir + subprocess.check_call(f"pip install -r {requirements_file} -t {output_dir}/python".split()) + + layer_id = f"{project_name}-{function_name}-dependencies" # 👈🏽 a unique id for the layer + layer_code = _lambda.Code.from_asset(output_dir) # 👈🏽 import the dependencies / code + + my_layer = _lambda.LayerVersion( + self, + layer_id, + code=layer_code, + ) + + return my_layer +``` + +Here, we are creating the `create_dependencies_layer` function. This functions runs the following steps: + +1. It defines the `requirements_file` variable which points to the `requirements.txt` file. +2. It defines the `output_dir` variable which points to a temporary directory where we will store the dependencies. +3. It uses `subprocess` to run commands to downloads the dependencies and stores them in the `output_dir` directory. +4. It defines the `layer_id` variable which is a unique id for the layer. +5. It defines the `layer_code` variable which points to the `output_dir` directory. +6. It creates the `my_layer` variable which is an instance of the `LayerVersion` construct. +7. It returns the `my_layer` variable. + +Now if you run `cdk deploy`, you will see that the lambda function is created successfully and you can invoke it successfully as well. + +[Succes lambda layer]({static}/images/aws/50001020-02-fn-layer-success-dependency.png) + + +## Reuse code between lambda functions + +Another major use case is when you write some code that you want to reuse between multiple lambda functions. E.g. some utility function or helper function that you want to use in multiple lambda functions. + +Similar to the previous example, we can create a layer by importing the code and then add that layer to our lambda functions. + +Let's create a `helpers.py` file in the `cdk_app/utils` directory with the following contents: + +```python +# filename: cdk_app/utils/helpers.py + +def hello_world(): + return "Hello World" +``` + +Now we can create a layer using this code as follows: + +```python +# filename: cdk_app/lambda_stack.py + +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) + +from constructs import Construct + + +class LambdaStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + my_layer = _lambda.LayerVersion( + self, + "MyLayer", + code=_lambda.Code.from_asset("cdk_app/utils"), + ) + + + my_lambda = _lambda.Function( + self, + id="MyLambda", + runtime=_lambda.Runtime.PYTHON_3_10, + handler="index.handler", + code=_lambda.Code.from_asset("cdk_app/lambda"), + layers=[my_layer], + ) +``` + + +Now we can use the `hello_world` function in our lambda function as follows: + +```python +# filename: cdk_app/lambda/index.py + +from utils.helpers import hello_world # 👈🏽 import the helper function + +def handler(event, context): + + return { + "statusCode": 200, + "body": hello_world() # 👈🏽 use the helper function + } +``` + +This is a very simple example, but you can imagine that you can put a lot of code in the `helpers.py` file and then use that code in multiple lambda functions. + + +## Conclusion +While layers are a very useful feature of lambda functions, they are not a silver bullet. You should use them wisely and only when needed. If you have a lot of code in your lambda function, you should consider using a different approach such as using a container image instead of a lambda function. \ No newline at end of file diff --git a/content/aws/87500000-cdk-api-gateway-route53.md b/content/aws/87500000-cdk-api-gateway-route53.md index cb78784..eb7ffe2 100644 --- a/content/aws/87500000-cdk-api-gateway-route53.md +++ b/content/aws/87500000-cdk-api-gateway-route53.md @@ -185,13 +185,13 @@ B) Now back in the file `api_route53/api_route53_stack.py` we can create a lambd handler = _lambda.Function( self, "ApiHandler", - runtime=_lambda.Runtime.PYTHON_3_8, + runtime=_lambda.Runtime.PYTHON_3_10, handler="lambda_function.lambda_handler", code=_lambda.Code.from_asset("api_route53"), ) ``` -Here we chose Python 3.8 as our runtime, and the handler is the function in the lambda file that will be called when the API Gateway is invoked. The code is loaded from the `api_route53` directory. +Here we chose Python 3.10 as our runtime, and the handler is the function in the lambda file that will be called when the API Gateway is invoked. The code is loaded from the `api_route53` directory. # Create the API Gateway @@ -315,7 +315,7 @@ class ApiRoute53Stack(Stack): handler = _lambda.Function( self, "ApiHandler", - runtime=_lambda.Runtime.PYTHON_3_8, + runtime=_lambda.Runtime.PYTHON_3_10, handler="lambda_function.lambda_handler", code=_lambda.Code.from_asset("api_route53"), ) diff --git a/content/images/aws/50001000-01-fn-inline-code.gif b/content/images/aws/50001000-01-fn-inline-code.gif new file mode 100644 index 0000000..ed71e81 Binary files /dev/null and b/content/images/aws/50001000-01-fn-inline-code.gif differ diff --git a/content/images/aws/50001020-01-fn-lambda-import-error.png b/content/images/aws/50001020-01-fn-lambda-import-error.png new file mode 100644 index 0000000..385bc56 Binary files /dev/null and b/content/images/aws/50001020-01-fn-lambda-import-error.png differ diff --git a/content/images/aws/50001020-02-fn-layer-success-dependency.png b/content/images/aws/50001020-02-fn-layer-success-dependency.png new file mode 100644 index 0000000..3346c01 Binary files /dev/null and b/content/images/aws/50001020-02-fn-layer-success-dependency.png differ