AWSは、IaC
(Infrastructure as Code)で手軽にアプリケーションを構築するためのオープンソースのフレームワークを複数提供しています。
-
SAM
- サーバーレスアプリケーション構築用のオープンソースフレームワーク
-
CloudFormation
より短い定義でいい感じに定義できる - Lambda関数のバンドル処理やAWSへのアップロードもコマンドで手軽にできる
- ローカルでLambda関数+αを起動させてデバッグができるツールも提供している
-
CDK
- 特定のプログラミング言語でクラウドインフラストラクチャをコード化+
CloudFormation
を通じてデプロイするためのオープンソースフレームワーク - AWSにより最適化されたコンストラクト(通称:
L2 Layer
?)が提供されているため、複雑なコーディングなしでリソースを定義+構築することができる(コーディングの負荷軽減)
- 特定のプログラミング言語でクラウドインフラストラクチャをコード化+
昨今ではAWSリソースをCDK
で管理していくのが主流になりつつありますが、SAM
はサーバーレスのアプリケーションをローカルで手軽にデバッグできるツールとして優秀ですので、どちらもうまく活用していきたいところです。
そこで今回の記事では、その使い分け方法として、以下のことをやっていきます。(タイトル回収)
【主な手順】
-
CDK
でAWSリソースを定義する - 1.の
CDK
のコード結果をCloudFormation
のテンプレートを出力する - 2.で出力したテンプレートを利用して
SAMCLI Local
でデバッグする - 3.の結果が問題なければ
CDK
のコマンドでデプロイする
なお、今回は公式の手順を参考に、aws-cdk-lib.aws_samを利用しないバージョンを試していきます。
CDK
でコーディングするAWSリソース
API Gateway
×Lambda
のAWSリソースを構築します。
CDK
のコーディングを行う前に、比較用としてSAM
テンプレートで今回構築するリソースを表現しておきます。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.8
Timeout: 3
MemorySize: 128
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
動作環境
本記事を実施した環境は以下の通りです。
$ uname -a
Linux MikMatsuPC 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ python3 --version
Python 3.8.10
$ node -v
v19.6.0
$ cdk --version
2.65.0 (build 5862f7a)
$ sam --version
SAM CLI, version 1.74.0
$ docker --version
Docker version 23.0.0, build e92dd87
実行手順
CDKプロジェクト作成
馴染みのコマンドで作成します。
$ cdk init app --language python
Applying project template app for python
# Welcome to your CDK Python project!
{中略}
Enjoy!
Initializing a new git repository...
Please run 'python3 -m venv .venv'!
Executing Creating virtualenv...
✅ All done!
Lambda関数のコード作成
単純なメッセージが返却されるプログラムを用意します。
$ mkdir hello_world
$ touch hello_world\app.py
プログラムの中身は以下となります。
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message": "hello world from SAM and the CDK!",
}),
}
スタック作成
API Gateway
とLambda
を定義します。
API Gateway
は手抜きで、Lambda
でトリガー指定した際に自動生成されるものを利用します。
SAM
とほぼコード量は変わらないように見えます。
from aws_cdk import (
Stack,
Duration,
aws_lambda as lambda_,
aws_lambda_event_sources as lambda_event_sources,
)
from constructs import Construct
function_timeout = 3
function_memory_size = 128
class SamWithCdkStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_function = lambda_.Function(self, "HelloWorldFunction",
code=lambda_.Code.from_asset("hello_world"),
handler="app.lambda_handler",
runtime=lambda_.Runtime.PYTHON_3_8,
timeout=Duration.minutes(function_timeout),
memory_size=function_memory_size,
)
lambda_function.add_event_source(lambda_event_sources.ApiEventSource(
method="get",
path="/hello",
)
)
cdk synth
でCloudFormation
のテンプレート出力
コマンド実行時には、--no-staging
を付与して実行します。(SAM CLIでローカルデバッグするためのオプション)
$ cdk synth --no-staging
実行結果で出力されたyamlは一応載せておきます。(長いので折りたたんでいます)
CDK
の恩恵を受けていますので、AWS::IAM::Role
やAWS::Lambda::Permission
などもしっかり自動付与されています。
コマンド出力結果(yaml)
Resources:
HelloWorldFunctionServiceRole8E0BD458:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Metadata:
aws:cdk:path: SamWithCdkStack/HelloWorldFunction/ServiceRole/Resource
HelloWorldFunctionB2AB6E79:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket:
Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}
S3Key: 44b520949ecc9f814580b3164083d23132e7feb8125f24d991a81d197bc5b22c.zip
Role:
Fn::GetAtt:
- HelloWorldFunctionServiceRole8E0BD458
- Arn
Handler: app.lambda_handler
MemorySize: 128
Runtime: python3.8
Timeout: 180
DependsOn:
- HelloWorldFunctionServiceRole8E0BD458
Metadata:
aws:cdk:path: SamWithCdkStack/HelloWorldFunction/Resource
aws:asset:path: /home/mikmatsu/cdk/sam-with-cdk/hello_world
aws:asset:is-bundled: false
aws:asset:property: Code
SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE:
Type: AWS::ApiGateway::RestApi
Properties:
Name: SamWithCdkStackHelloWorldFunction7124C723:ApiEventSourceA7A86A4F
Metadata:
aws:cdk:path: SamWithCdkStack/SamWithCdkStackHelloWorldFunction7124C723:ApiEventSourceA7A86A4F/Resource
SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FDeployment1B0E4B9D858995340bef49c95618c03485e457d9:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId:
Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE
Description: Automatically created by the RestApi construct
DependsOn:
- SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4Fhelloget39C449D6
- SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4Fhello00548847
Metadata:
aws:cdk:path: SamWithCdkStack/SamWithCdkStackHelloWorldFunction7124C723:ApiEventSourceA7A86A4F/Deployment/Resource
SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FDeploymentStageprod407F57F9:
Type: AWS::ApiGateway::Stage
Properties:
RestApiId:
Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE
DeploymentId:
Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FDeployment1B0E4B9D858995340bef49c95618c03485e457d9
StageName: prod
Metadata:
aws:cdk:path: SamWithCdkStack/SamWithCdkStackHelloWorldFunction7124C723:ApiEventSourceA7A86A4F/DeploymentStage.prod/Resource
SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4Fhello00548847:
Type: AWS::ApiGateway::Resource
Properties:
ParentId:
Fn::GetAtt:
- SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE
- RootResourceId
PathPart: hello
RestApiId:
Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE
Metadata:
aws:cdk:path: SamWithCdkStack/SamWithCdkStackHelloWorldFunction7124C723:ApiEventSourceA7A86A4F/Default/hello/Resource
SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FhellogetApiPermissionSamWithCdkStackSamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F869387A4GEThelloD632A017:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::GetAtt:
- HelloWorldFunctionB2AB6E79
- Arn
Principal: apigateway.amazonaws.com
SourceArn:
Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- ":execute-api:"
- Ref: AWS::Region
- ":"
- Ref: AWS::AccountId
- ":"
- Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE
- /
- Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FDeploymentStageprod407F57F9
- /GET/hello
Metadata:
aws:cdk:path: SamWithCdkStack/SamWithCdkStackHelloWorldFunction7124C723:ApiEventSourceA7A86A4F/Default/hello/get/ApiPermission.SamWithCdkStackSamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F869387A4.GET..hello
SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FhellogetApiPermissionTestSamWithCdkStackSamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F869387A4GEThello83E18F88:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::GetAtt:
- HelloWorldFunctionB2AB6E79
- Arn
Principal: apigateway.amazonaws.com
SourceArn:
Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- ":execute-api:"
- Ref: AWS::Region
- ":"
- Ref: AWS::AccountId
- ":"
- Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE
- /test-invoke-stage/GET/hello
Metadata:
aws:cdk:path: SamWithCdkStack/SamWithCdkStackHelloWorldFunction7124C723:ApiEventSourceA7A86A4F/Default/hello/get/ApiPermission.Test.SamWithCdkStackSamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F869387A4.GET..hello
SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4Fhelloget39C449D6:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: GET
ResourceId:
Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4Fhello00548847
RestApiId:
Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri:
Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- ":apigateway:"
- Ref: AWS::Region
- :lambda:path/2015-03-31/functions/
- Fn::GetAtt:
- HelloWorldFunctionB2AB6E79
- Arn
- /invocations
Metadata:
aws:cdk:path: SamWithCdkStack/SamWithCdkStackHelloWorldFunction7124C723:ApiEventSourceA7A86A4F/Default/hello/get/Resource
CDKMetadata:
Type: AWS::CDK::Metadata
Properties:
Analytics: v2:deflate64:H4sIAAAAAAAA/1VP207DMAz9lr2nZjCBeN2GeENM4wMmLzXFtEmq2tFUVf13knSI8XQuOT52HuDpEdYrvEhl67bq+AzTh6JtTbJOU4fuXCNMr9Fb5eDN/tPf8gMNjkWSmg2jg+kYOsoPGWcjmxOKkApsMyQNu2hb0h0KGey5QaULjmmORLc9l9ErfaG+C6Mjr9m9Uem+pixZSMqHOFgqnb/iWrTwN9KvUGdrYfP8lyuH5SL2TU68R+2j/htPfB98zVq+eRhThb/bwDPcr1ffwlwN0Ss7guOCP5DsvIpUAQAA
Metadata:
aws:cdk:path: SamWithCdkStack/CDKMetadata/Default
Condition: CDKMetadataAvailable
Outputs:
SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FEndpointFD4B5AB4:
Value:
Fn::Join:
- ""
- - https://
- Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4F567FE4AE
- .execute-api.
- Ref: AWS::Region
- "."
- Ref: AWS::URLSuffix
- /
- Ref: SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FDeploymentStageprod407F57F9
- /
Conditions:
CDKMetadataAvailable:
Fn::Or:
- Fn::Or:
- Fn::Equals:
- Ref: AWS::Region
- af-south-1
- Fn::Equals:
- Ref: AWS::Region
- ap-east-1
- Fn::Equals:
- Ref: AWS::Region
- ap-northeast-1
- Fn::Equals:
- Ref: AWS::Region
- ap-northeast-2
- Fn::Equals:
- Ref: AWS::Region
- ap-south-1
- Fn::Equals:
- Ref: AWS::Region
- ap-southeast-1
- Fn::Equals:
- Ref: AWS::Region
- ap-southeast-2
- Fn::Equals:
- Ref: AWS::Region
- ca-central-1
- Fn::Equals:
- Ref: AWS::Region
- cn-north-1
- Fn::Equals:
- Ref: AWS::Region
- cn-northwest-1
- Fn::Or:
- Fn::Equals:
- Ref: AWS::Region
- eu-central-1
- Fn::Equals:
- Ref: AWS::Region
- eu-north-1
- Fn::Equals:
- Ref: AWS::Region
- eu-south-1
- Fn::Equals:
- Ref: AWS::Region
- eu-west-1
- Fn::Equals:
- Ref: AWS::Region
- eu-west-2
- Fn::Equals:
- Ref: AWS::Region
- eu-west-3
- Fn::Equals:
- Ref: AWS::Region
- me-south-1
- Fn::Equals:
- Ref: AWS::Region
- sa-east-1
- Fn::Equals:
- Ref: AWS::Region
- us-east-1
- Fn::Equals:
- Ref: AWS::Region
- us-east-2
- Fn::Or:
- Fn::Equals:
- Ref: AWS::Region
- us-west-1
- Fn::Equals:
- Ref: AWS::Region
- us-west-2
Parameters:
BootstrapVersion:
Type: AWS::SSM::Parameter::Value<String>
Default: /cdk-bootstrap/hnb659fds/version
Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]
Rules:
CheckBootstrapVersion:
Assertions:
- Assert:
Fn::Not:
- Fn::Contains:
- - "1"
- "2"
- "3"
- "4"
- "5"
- Ref: BootstrapVersion
AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.
SAMCLI Local
でローカル実行
では、出力したテンプレートを利用して今回作成したAPI Gateway
×Lambda
をローカルで動かしてみます。
なお、SAMCLI Local
はDocker
が必要になりますので、Docker
が起動していることは事前に確認してください。
今回は二つのコマンドをそれぞれ実行します。
-
sam local invoke
: Lambda 関数を直接呼び出す
--no-event
のオプション(Lambdaのeventsに何も含めない)を付与し、cdk sysnth
で出力したテンプレートを利用するようにパスを指定して実行します。$ sam local invoke HelloWorldFunction --no-event -t ./cdk.out/SamWithCdkStack.template.json Invoking app.lambda_handler (python3.8) Local image is up-to-date Using local image: public.ecr.aws/lambda/python:3.8-rapid-x86_64. Mounting /home/mikmatsu/cdk/sam-with-cdk/hello_world as /var/task:ro,delegated inside runtime container START RequestId: 3dfd4c52-d2c2-422b-9c75-b9ace38493ad Version: $LATEST END RequestId: 3dfd4c52-d2c2-422b-9c75-b9ace38493ad REPORT RequestId: 3dfd4c52-d2c2-422b-9c75-b9ace38493ad Init Duration: 0.03 ms Duration: 45.05 ms Billed Duration: 46 ms Memory Size: 128 MB Max Memory Used: 128 MB {"statusCode": 200, "body": "{\"message\": \"hello world from SAM and the CDK!\"}"}m
-
sam local start-api
: API をローカルでホストする
本コマンドを実行するとDockerが
起動し、API経由でLambda関数を実行することができます。Docker
したターミナルではcurl
コマンドが実行できないため、別のターミナルを起動して、Docker
のログに表示されたローカルのエンドポイント(http://127.0.0.1:3000/hello
)に対してアクセスします。APIのDocker起動$ sam local start-api -t ./cdk.out/SamWithCdkStack.template.json Mounting LambdaFunctionBF21E41F at http://127.0.0.1:3000/hello [GET] You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. If you used sam build before running local commands, you will need to re-run sam build for the changes to be picked up. You only need to restart SAM CLI if you update your AWS SAM template 2023-02-26 22:40:38 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
別のコマンドプロンプト経由で実行$ curl http://127.0.0.1:3000/hello {"message": "hello world from SAM and the CDK!"}
どちらのコマンドでもLambda関数が正常に動いていることが確認できました。
CDK
を利用してAWSにデプロイ
ローカルでの動作検証が完了しましたので、いよいよAWSにデプロイします。
$ cdk deploy
✨ Synthesis time: 3.42s
SamWithCdkStack: building assets...
[0%] start: Building 44b520949ecc9f814580b3164083d23132e7feb8125f24d991a81d197bc5b22c:current_account-current_region
[0%] start: Building f019d22a0c8a1892a25302dc4918de1c5be4b7803f438a50de068bac70348ba0:current_account-current_region
[50%] success: Built 44b520949ecc9f814580b3164083d23132e7feb8125f24d991a81d197bc5b22c:current_account-current_region
[100%] success: Built f019d22a0c8a1892a25302dc4918de1c5be4b7803f438a50de068bac70348ba0:current_account-current_region
SamWithCdkStack: assets built
{中略}
SamWithCdkStack: creating CloudFormation changeset...
✅ SamWithCdkStack
✨ Deployment time: 81.34s
Outputs:
SamWithCdkStack.SamWithCdkStackHelloWorldFunction7124C723ApiEventSourceA7A86A4FEndpointFD4B5AB4 = https://vcwi5z4vzk.execute-api.ap-northeast-1.amazonaws.com/prod/
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:0123456789AB:stack/SamWithCdkStack/9a7b2a00-b5e3-11ed-9379-0e63d8c9e43b
✨ Total time: 84.77s
デプロイが完了したので、AWSにデプロイされたアプリケーションを呼び出してみます。
Outputs
に記載されているAPI Gateway
のURLに対してcurl
を叩きます。
$ curl https://vcwi5z4vzk.execute-api.ap-northeast-1.amazonaws.com/prod/hello
{"message": "hello world from SAM and the CDK!"}
想定通りの結果が返ってきました!
これでやりたかったことがすべて完了です。
【おまけ】aws-cdk-lib.aws_sam
を利用した場合の注意点
SAM
テンプレートのままコード化したい!という方もいらっしゃると思います。
その場合はaws-cdk-lib.aws_samを利用すればほぼそのまま書くことができます。
from aws_cdk import (
Stack,
aws_lambda as lambda_,
aws_sam as sam, # samライブラリ追加
)
from constructs import Construct
function_timeout = 3
function_memory_size = 128
class SamWithCdkStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
#sam書き換え
lambda_function = sam.CfnFunction(self, "HelloWorldFunction",
code_uri="hello_world/",
handler="app.lambda_handler",
runtime=lambda_.Runtime.PYTHON_3_8.to_string(),
events={
"HelloWorld": sam.CfnFunction.EventSourceProperty(
type="Api",
properties=sam.CfnFunction.ApiEventProperty(
path="/hello",
method="get",
),
)
},
timeout=function_timeout,
memory_size=function_memory_size,
)
ただし、先ほどと同じcdk synth --no-staging
実行後にSAMCLI Local
を行うと失敗する可能性があります。(私は上手くいきませんでした)
$ cdk synth --no-staging
{実行結果省略}
$ sam local invoke HelloWorldFunction --no-event -t ./cdk.out/SamWithCdkStack.template.json
Invoking app.lambda_handler (python3.8)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.8-rapid-x86_64.
Mounting /home/mikmatsu/cdk/sam-with-cdk/cdk.out/hello_world as /var/task:ro,delegated inside runtime container
START RequestId: a05c8624-768e-4da7-ad9f-8668f2649c62 Version: $LATEST
Traceback (most recent call last): Unable to import module 'app': No module named 'app'
END RequestId: a05c8624-768e-4da7-ad9f-8668f2649c62
REPORT RequestId: a05c8624-768e-4da7-ad9f-8668f2649c62 Init Duration: 0.88 ms Duration: 41.41 ms Billed Duration: 42 ms Memory Size: 128 MB Max Memory Used: 128 MB
{"errorMessage": "Unable to import module 'app': No module named 'app'", "errorType": "Runtime.ImportModuleError", "stackTrace": []}m
なお、以下のようにcdk synth
の出力結果(yaml)をファイル化してそちらを見るようにしたところ上手く実行することができました。(なぜうまく行くのかは不明です。jsonとyamlで差分があるようには見えないのですが…)
もしaws-cdk-lib.aws_sam
を利用している方でSAMCLI Local
が上手くいかなかった方は試してみてください。
$ cdk synth --no-staging > template.yml
$ sam local invoke HelloWorldFunction --no-event -t template.yml
Invoking app.lambda_handler (python3.8)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.8-rapid-x86_64.
Mounting /home/mikmatsu/cdk/sam-with-cdk/hello_world as /var/task:ro,delegated inside runtime container
START RequestId: 2bc492a1-e9ac-486b-a35d-3acbef7ce22d Version: $LATEST
{"statusCode": 200, "body": "{\"message\": \"hello world from SAM and the CDK!\"}"}END RequestId: 2bc492a1-e9ac-486b-a35d-3acbef7ce22d
REPORT RequestId: 2bc492a1-e9ac-486b-a35d-3acbef7ce22d Init Duration: 0.15 ms Duration: 41.35 ms Billed Duration: 42 ms Memory Size: 128 MB Max Memory Used: 128 MB
また、aws-cdk-lib.aws_sam
を利用した場合、Lambda関数のバンドル処理をどのように行えば良いのかわからず、AWSへのデプロイを試せませんでした。
今回メインの内容ではないので調査は割愛しますが、必要に迫られたら再調査するかもしれません。
最後に
今回の検証で、CDK
とSAM
が意外と相性良く、どちらもスムーズに動かすことができるということがわかりました。
今までは使い分けを意識することがなく、どちらか一方のフレームワークを選択し利用していたので、今後は二つのフレームワークを上手く使い分けて開発効率を上げていきたいなと思います。
こちらからは以上です。
参考URL