Lambda Function URLをCloudFormationやCDKで作る

# はじめに
Lambdaを、API Gatewayを用いずに、URLで実行できる機能があります。

https://aws.amazon.com/jp/blogs/aws/announcing-aws-lambda-function-urls-built-in-https-endpoints-for-single-function-microservices/

これをCloudFormation(とSAM)、CDKで作るサンプルをまとめてみました。
認証タイプは２つありますが、今回は「NONE」の例だけです。

# 参考

https://awstut.com/2022/04/24/lambda-function-url-by-cloudformation-auth-type-none/

# CloudFormation
昔書いた以下のテンプレートを用います。

https://qiita.com/a_b_/items/12e3002998d22dfb6385

最後に以下を加えただけです。
- AWS::Lambda::Url
- AWS::Lambda::Permission
- 出力
    - スタックからURLがわかるようにするためだけ
    - Lambda関数のプロパティから確認可能

```yaml:
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  LambdaFunctionName:
    Type: String
    Default: function-https

Resources:
  FunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaFunctionName}"
      RetentionInDays: 3653   # 未指定時は「失効しない」

  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "for-lambdafunction-${LambdaFunctionName}"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: '/service-role/'
      Policies:
        # CloudWatch
        - PolicyName: write-cloudwatchlogs
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunctionName}:*"    

  TargetFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Ref LambdaFunctionName
      Role: !GetAtt FunctionRole.Arn
      Runtime: python3.9
      Handler: index.lambda_handler
      Code:
        ZipFile: !Sub |
          def lambda_handler(event, context):
            return 'hello, from function 1.'

  ##################################################
  # 以下、URLと許可
  ##################################################
  TargetFunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      TargetFunctionArn: !GetAtt TargetFunction.Arn

  TargetFunctionUrlPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunctionUrl
      FunctionName: !GetAtt TargetFunction.Arn
      FunctionUrlAuthType: NONE
      Principal: "*"

Outputs:
  FunctionUrl:
    Value: !GetAtt TargetFunctionUrl.FunctionUrl
    Export:
      Name: TargetFunctionUrl
```

# SAM
こちらも、昔のコードを流用します。

https://qiita.com/a_b_/items/0d645e50750e3fc2b9b9

先ほどと同じものを追加するだけです。

```yaml:template..yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: SAM Template for Lambda Function

Parameters:
  LambdaFunctionName:
    Type: String
    Default: "testfunction001"

Resources:
  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "for-lambdafunction-${LambdaFunctionName}"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: '/service-role/'
      Policies:
        # CloudWatch
        - PolicyName: write-cloudwatchlogs
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunctionName}:*"

  FunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${LambdaFunctionName}
      RetentionInDays: 3653

  TargetFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref LambdaFunctionName
      Role: !GetAtt FunctionRole.Arn
      CodeUri: functions/
      Handler: app.lambda_handler
      Runtime: python3.9
      
  ##################################################
  # 以下、URLと許可
  ##################################################
  TargetFunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      TargetFunctionArn: !GetAtt TargetFunction.Arn

  TargetFunctionUrlPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunctionUrl
      FunctionName: !GetAtt TargetFunction.Arn
      FunctionUrlAuthType: NONE
      Principal: "*"

Outputs:
  FunctionUrl:
    Value: !GetAtt TargetFunctionUrl.FunctionUrl
    Export:
      Name: TargetFunctionUrl
```


# CDK
CDKで記述すると以下のようになります。CFnテンプレートと比べてとてもシンプルです。

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as path from 'path';

export class UsecdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const TargetFunction = new lambda.Function(this, 'TargetFunction',{
      runtime: lambda.Runtime.PYTHON_3_9,
      handler: 'app.lambda_handler',
      code: lambda.Code.fromAsset(path.join(__dirname,'../functions/'))
    });
    
    // Function URL
    const fnUrl = TargetFunction.addFunctionUrl({
      authType: lambda.FunctionUrlAuthType.NONE,
    });
    
    // URLをアウトプット
    new cdk.CfnOutput(this,'TargetFunctionUrl',{
      value: fnUrl.url,
    });
  }
}
```
:::note warn
先に記載したCFnテンプレートとSAMとは以下の点が異なりますが、Function URLの例には影響ないので除きました。
- Lambda用のロールは作成しない
    - 自動で`AWSLambdaBasicExecutionRole`がアタッチされます
- Lambda用のロググループは作成しない
    - CFnテンプレート変換時に以下のプロパティで上書きされてしまったので定義しませんでした
        - 保持期間:1年
        - 削除ポリシー: RETAIN
:::

使用するLambda関数は`functions`フォルダ内に置きます。
```python:functions/app.py
def lambda_handler(event, context):

    print('sample function')
    return {
        'statusCode': 200,
        'body': 'Hello from Lambda!'
    }
```

あとはコンパイルして変換してデプロイになります。方法は昔まとめた記事がありますのでそちらをご確認ください。

https://qiita.com/a_b_/items/15a35108ad4a8a38346f#typescript%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB

# おわりに
今回、ちょっと使ってみたくて触った内容を記事にしました。
どなたかの助けになれば幸いです。