Serverless FrameworkでもAWS で「APIGateway」→「VPC Lambda」→「EFS」を一発で構築してみる


2023/04/20
【AWS使い方ガイド】手動で「APIGateway」→「VPC Lambda」→「EFS」の繋いだHello Worldをやってみる
蛸壺の技術ブログ|Serverless FrameworkでもAWS で「APIGateway」→「VPC Lambda」→「EFS」を一発で構築してみる

前回はAWS VPC Lambdaの構築方法の基礎を学ぶためにダッシュボードから手動で一つ一つサービスを立ち上げていきました。

合同会社タコスキングダム|蛸壺の技術ブログ
【AWS使い方ガイド】手動で「APIGateway」→「VPC Lambda」→「EFS」の繋いだHello Worldをやってみる

AWSのダッシュボードから手動で「VPC Lambda」の設定手順を一つずつ解説を加えながらハンズオンで構築していきます。

やっていただくと分かるように、ダッシュボードからVPC Lambdaを構築する場合、非常に面倒な手順を正しい順番に設定していく必要がありました。

今回はこの話の続きで、Serverless Frameworkを使ってダッシュボードなしのコマンドから楽に一発構築を目指してみましょう。


通常のSAMをServerlessで構築

復習も兼ねて、最初はもっとも簡単なServerlessの"Hello World"を普通のLambdaで作成してみます。

            
            service: apigw-vpclmb-lab

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs18.x
  stage: dev
  region: ap-northeast-1

package:
  patterns:
    - '!**'
    - handler.mjs

functions:
  hello:
    handler: handler.handler
    events:
      - httpApi:
          path: /
          method: '*'
        
            
            export const handler = async(event) => {
    const response = {
        statusCode: 200,
        body: JSON.stringify('こんにちは!はじめてのVPC Lambda From Serverless!!'),
    };
    return response;
};
        
これをデプロイ後に実行してみると、

            
            $ npx sls deploy --verbose
$ npx sls invoke --function hello --verbose
{
    "statusCode": 200,
    "body": "\"こんにちは!はじめてのVPC Lambda From Serverless!!\""
}
        
とレスポンスが当然ながら返ってきます。

これと言って特別ではない単なる
SAM(Serverless Application Model)のビジネスロジックを使ったHttp APIの基本的な"Hello World"です。

合同会社タコスキングダム|蛸壺の技術ブログ

この場合、Lambdaはリージョンの「どこか」には存在しますが、VPC内にはいないので、「VPC Lambda」ではありません。

ではここから、Serverless FrameworkでVPC Lambdaに仕上げていくように変更していきます。

VPC Lambdaの追加手順としては、

            
            1. serverless.ymlから.envファイルを使えるようにする
2. VPC Lambdaに対応したIAMロールを指定する
3. VPC Lambdaに必要な関数設定を作成する
4. リソースでVPCの設定(セキュリティグループ)を行う
5. リソースでEFSの設定(FS本体・マウントターゲット・アクセスポイント)を行う
        
という点を踏まえた上で、以降で詳しく解説していきます。


VPC LambdaをServerlessでも一括構築する

そのまま先程のserverless.ymlを編集して、通常のLambdaをVPC Lambdaへと"昇格"させてみましょう。

前回の再現になりますが、VPC Lambdaの構成としては以下の図のようになります。

合同会社タコスキングダム|蛸壺の技術ブログ

まずは一気に修正済みの
serverless.ymlを載せます。

            
            service: apigw-vpclmb-lab

frameworkVersion: '3'

#👇解説ポイント①
useDotenv: true

provider:
  name: aws
  runtime: nodejs18.x
  stage: dev
  region: ap-northeast-1
  #👇解説ポイント②
  iam:
    role: !Sub arn:aws:iam::${AWS::AccountId}:role/${env:LAMBDA_CUSTOM_ROLE}

package:
  patterns:
    - '!**'
    - handler.mjs

functions:
  hello:
    handler: handler.handler
    #👇解説ポイント③
    environment:
      EFS_MOUNT_DIR: ${env:EFS_MOUNT_DIR}
    fileSystemConfig:
      localMountPath: ${env:EFS_MOUNT_DIR}
      arn: !GetAtt ["EFSAccessPoint", "Arn"]
    vpc:
      securityGroupIds:
        - !GetAtt ["LambdaSecurityGroup", "GroupId"]
      subnetIds:
        - ${env:LAMBDA_SUBNET_ID}
    dependsOn:
      - EFSMountTargetA
      - EFSMountTargetB
      - EFSMountTargetC
    events:
      - httpApi:
          path: /
          method: '*'

resources:
  Resources:
    #👇解説ポイント④
    LambdaSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        #GroupName: sg-lambda 👈 スタックエラーを起こすので、固定でグループ名を与えてはNG
        GroupDescription: Lambda Access for EFS
        VpcId: ${env:VPC_ID}
        SecurityGroupIngress:
          - IpProtocol: "-1"
        SecurityGroupEgress:
          - CidrIp: 0.0.0.0/0
            IpProtocol: tcp
            FromPort: 2049
            ToPort: 2049
        Tags:
          - Key: Name
            Value: LambdaEFSSecurityGroup

    EFSSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        #GroupName: sg-efs 👈 スタックエラーを起こすので、固定でグループ名を与えてはNG
        GroupDescription: EFS Allowed Ports
        VpcId: ${env:VPC_ID}
        SecurityGroupIngress:
          - IpProtocol: tcp
            FromPort: 2049
            ToPort: 2049
            SourceSecurityGroupId: !GetAtt ["LambdaSecurityGroup", "GroupId"]
            Description: from Lambda
        SecurityGroupEgress:
          - CidrIp: 0.0.0.0/0
            IpProtocol: "-1"
        Tags:
          - Key: Name
            Value: EFSSecurityGroup

    #👇解説ポイント⑤
    EFSFileSystem:
      Type: AWS::EFS::FileSystem
      Properties:
        FileSystemTags:
          - Key: Name
            Value: MyFileSystem
        BackupPolicy:
          Status: ENABLED
        Encrypted: true
        LifecyclePolicies:
          - TransitionToIA: AFTER_30_DAYS
        PerformanceMode: generalPurpose

    #👇解説ポイント⑥
    #MountTargetをアベイラビリティゾーンに全て置く場合には複数を個別に書く
    EFSMountTargetA:
      Type: AWS::EFS::MountTarget
      Properties:
        FileSystemId: !Ref EFSFileSystem
        SecurityGroups:
          - !Ref EFSSecurityGroup
        SubnetId: ${env:LAMBDA_SUBNET_ID_1}
      DependsOn: EFSFileSystem

    EFSMountTargetB:
      Type: AWS::EFS::MountTarget
      Properties:
        FileSystemId: !Ref EFSFileSystem
        SecurityGroups:
          - !Ref EFSSecurityGroup
        SubnetId: ${env:LAMBDA_SUBNET_ID_2}
      DependsOn: EFSFileSystem

    EFSMountTargetC:
      Type: AWS::EFS::MountTarget
      Properties:
        FileSystemId: !Ref EFSFileSystem
        SecurityGroups:
          - !Ref EFSSecurityGroup
        SubnetId: ${env:LAMBDA_SUBNET_ID_3}
      DependsOn: EFSFileSystem

    #👇解説ポイント⑦
    EFSAccessPoint:
      Type: AWS::EFS::AccessPoint
      Properties:
        FileSystemId: !Ref EFSFileSystem
        PosixUser:
          Uid: "1001"
          Gid: "1001"
        RootDirectory:
          Path: ${env:EFS_ROOT_DIR}
          CreationInfo:
            OwnerGid: "1001"
            OwnerUid: "1001"
            Permissions: "750"
        AccessPointTags:
          - Key: Name
            Value: hello-func-ap
      DependsOn: EFSFileSystem
        
はい、VPCにしたいだけなのですが一気に複雑化&難解化しました。

では追記した一つ一つ解説していきます。

解説ポイント① 〜 .envファイルで個人情報の漏洩をガードする

今回の話に限らず、プライベートな趣味レベルのボッチ開発などと違い、第三者とプロジェクトを共有したい場合、serverlessファイルにAWSなどのアカウント情報を直書きしてしまうのは宜しくありません。

ということで、通常はgitから除外した自分だけが参照できる
.envファイルでこれらの情報を管理することになります。

Serverless Frameworkの場合、以下を書き足すと.envファイルをビルド時に読み込んでくれます。

例えば、以下のような
.envファイルがあったとします。

            
            HOGE_SECERT=hogeno_himitu
        
この.envの内容をserverless.ymlで呼び出して利用したいのであれば、

            
            ...
useDotenv: true
...

#👇ENV変数を呼び出す
USER: ${env:HOGE_SECERT}
        
という用法で利用することができます。

今回の実装を試されたい方は、以下のような
.envファイルをあらかじめ用意しておきましょう。

            
            VPC_ID=vpc-<あなたのVPC ID>
AWS_REGION=ap-northeast-1
LAMBDA_SUBNET_ID_1=subnet-<1つ目のアベイラビリティゾーンのサブネットID>
LAMBDA_SUBNET_ID_2=subnet-<2つ目のアベイラビリティゾーンのサブネットID>
LAMBDA_SUBNET_ID_3=subnet-<3つ目のアベイラビリティゾーンのサブネットID>
EFS_MOUNT_DIR=/mnt/efs
EFS_ROOT_DIR=/hello-func
LAMBDA_CUSTOM_ROLE=my-vpclambda-efs-role
        
なお、各ENV変数の諸元は、前回の記事のものと対応した内容にしています。

解説ポイント② 〜 登録済みのIAMロールをアタッチする

次にデプロイするlambdaに必要な実行権限ロールを[provider] > [iam] > [role]で割り当てます。

ロールの割り当て方法がいくつかありますが、前回と同様、既に作成していた
my-vpclambda-efs-roleをスマートに指定します。

            
            ...
provider:
  ...
  iam:
    role: !Sub arn:aws:iam::${AWS::AccountId}:role/${env:LAMBDA_CUSTOM_ROLE}
        
YAML形式で、CloudFormationの組み込み関数(の略記形)である「!Sub」を使えば、${AWS::AccountId}からAWSアカウント名と${env:LAMBDA_CUSTOM_ROLE}のENV変数から値を取得して、ARN値を文字列で返してくれます。

!Sub関数を使うと、一列で簡潔に書けるのでこちらの書式がオススメです。

解説ポイント③ 〜 VPC Lambdaに仕上げる

通常のLambdaをVPC Lambdaにするためには、通常の関数定義に加えて、fileSystemConfigvpcの主に2つの設定が必要最低限の設定になります。

該当する部分を抜粋したものが以下のコードです。

            
            ...
functions:
  hello:
    handler: handler.handler
    #👇Lambdaが使う環境変数を登録
    environment:
      EFS_MOUNT_DIR: ${env:EFS_MOUNT_DIR}
    #👇EFSの指定
    fileSystemConfig:
      localMountPath: ${env:EFS_MOUNT_DIR}
      arn: !GetAtt ["EFSAccessPoint", "Arn"]
    #👇VPC設定
    vpc:
      securityGroupIds:
        - !GetAtt ["LambdaSecurityGroup", "GroupId"]
      subnetIds:
        - ${env:LAMBDA_SUBNET_ID}
    #👇リソースへの依存性
    dependsOn:
      - EFSMountTargetA
      - EFSMountTargetB
      - EFSMountTargetC
      #...
        
前回も解説しましたが、VPC Lambdaを利用するには、「マウントターゲット」「アクセスポイント」を必ず指定しなければなりません。

そこで、
fileSystemConfigにおいて、この2つを指定します。

また、VPC LambdaのVPC設置として、属する
「セキュリティグループ」の設定も必須となるので、vpcの項目に記述します。

VPC Lambdaへ仕上げるためには、ここでは以下のリソースが新たに必要となります。

            
            + LambdaSecurityGroup
+ EFSSecurityGroup
+ EFSFileSystem
+ EFSAccessPoint
+ EFSMountTargetA
+ EFSMountTargetB
+ EFSMountTargetC
        
ちなみにリソースの名前は自由に決定できます。

なお、
dependsOnは基本的に、リソースの生成順序を制御するときに利用します。

ここでは
dependsOnタグで生成するマウントターゲットのリソースを指定していますが、これを指定しておかないと、マウントターゲットの生成よりも前にLambdaの設定が呼び出されてリソースの参照エラーが起こってしまうのを防ぐためのものです。

さて、これらのリソースは、Serverless Frameworkネイティブでは直接生成する機能をサポートされていないため、
[resources] > [Resources]タグに、生のCloudformation的な記述で生成するという作業になります。

解説ポイント④ 〜 VPCのセキュリティグループを作成する

ではまず、デフォルトVPCにセキュリティグループを追加していきましょう。

前回の内容のままセキュリティグループを再現していきます。

            
            ...
resources:
  Resources:
    LambdaSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        #GroupName: sg-lambda 👈 スタックエラーを起こすので、固定でグループ名を与えてはNG
        GroupDescription: Lambda Access for EFS
        VpcId: ${env:VPC_ID}
        SecurityGroupIngress:
          - IpProtocol: "-1"
        SecurityGroupEgress:
          - CidrIp: 0.0.0.0/0
            IpProtocol: tcp
            FromPort: 2049
            ToPort: 2049
        Tags:
          - Key: Name
            Value: LambdaEFSSecurityGroup

    EFSSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        #GroupName: sg-efs 👈 スタックエラーを起こすので、固定でグループ名を与えてはNG
        GroupDescription: EFS Allowed Ports
        VpcId: ${env:VPC_ID}
        SecurityGroupIngress:
          - IpProtocol: tcp
            FromPort: 2049
            ToPort: 2049
            SourceSecurityGroupId: !GetAtt ["LambdaSecurityGroup", "GroupId"]
            Description: from Lambda
        SecurityGroupEgress:
          - CidrIp: 0.0.0.0/0
            IpProtocol: "-1"
        Tags:
          - Key: Name
            Value: EFSSecurityGroup

    #...
        
ここでは、セキュリティグループのリソースをLambdaSecurityGroupEFSSecurityGroupという2つで生成しています。

セキュリティグループの作成方法もまた前回詳しく説明しましたので、ここでは割愛します。

ポイントはセキュリティグループのインバウンドルールは
SecurityGroupIngress、アウトバウンドルールはSecurityGroupEgressに記述することくらいで、書き方はさほど難しくないと思います。

余談でセキュリティグループの名前を
GroupNameを使って固定させてしまうと、Serverless Frameworkから該当のセキュリティグループが識別できなくなって、スタックエラーが発生してしまうようです。

Serverless Frameworkを使う場合、セキュリティグループの名前を自分で付けることは一旦諦めましょう。

解説ポイント⑤ 〜 EFSを準備する

次にEFS本体のリソースをEFSFileSystemという名前で生成します。

            
            ...
resources:
  Resources:
    #...
    EFSFileSystem:
      Type: AWS::EFS::FileSystem
      Properties:
        FileSystemTags:
          - Key: Name
            Value: MyFileSystem
        BackupPolicy:
          Status: ENABLED
        Encrypted: true
        LifecyclePolicies:
          - TransitionToIA: AFTER_30_DAYS
        PerformanceMode: generalPurpose
    #...
        
こちらも前回で行ったEFSの手順をそのまま反映したものなので、難解な記述はないと思います。

解説ポイント⑥ 〜 マウントターゲットを作成する

先程作成したEFSに更に「マウントターゲット」を追加していきます。

マウントターゲットは各アベイラビリティゾーンに一つ設置することができます。

練習用ではどこか1つのアベイラビリティゾーンを使うくらいでよいと思いますが、本番アプリなどを運用する際には、ネットワークの冗長性を考えて、全てのアベイラビリティゾーンにマウントターゲットを設けることも検討しましょう。

またマウントターゲットの生成の前には必ずEFS本体の生成が完了していないといけませんので、
DependsOnタグにEFSFileSystemを指定することで、リソース参照エラーを回避することができます。

            
            ...
resources:
  Resources:
    #...
    EFSMountTargetA:
      Type: AWS::EFS::MountTarget
      Properties:
        FileSystemId: !Ref EFSFileSystem
        SecurityGroups:
          - !Ref EFSSecurityGroup
        SubnetId: ${env:LAMBDA_SUBNET_ID_1}
      DependsOn: EFSFileSystem

    EFSMountTargetB:
      Type: AWS::EFS::MountTarget
      Properties:
        FileSystemId: !Ref EFSFileSystem
        SecurityGroups:
          - !Ref EFSSecurityGroup
        SubnetId: ${env:LAMBDA_SUBNET_ID_2}
      DependsOn: EFSFileSystem

    EFSMountTargetC:
      Type: AWS::EFS::MountTarget
      Properties:
        FileSystemId: !Ref EFSFileSystem
        SecurityGroups:
          - !Ref EFSSecurityGroup
        SubnetId: ${env:LAMBDA_SUBNET_ID_3}
      DependsOn: EFSFileSystem
    #...
        
見てのように、全てのアベイラビリティゾーンにマウントターゲットを設置する場合、マウントターゲットごとに設定を記述しないといけません。

また、マウントターゲットには正しいセキュリティグループが設定されているかも良く確認しておくと良いでしょう。

解説ポイント⑦ 〜 アクセスポイントの追加

最後に、EFSのアクセスポイントのリソースを生成します。

            
            ...
resources:
  Resources:
    #...
    EFSAccessPoint:
      Type: AWS::EFS::AccessPoint
      Properties:
        FileSystemId: !Ref EFSFileSystem
        PosixUser:
          Uid: "1001"
          Gid: "1001"
        RootDirectory:
          Path: ${env:EFS_ROOT_DIR}
          CreationInfo:
            OwnerGid: "1001"
            OwnerUid: "1001"
            Permissions: "750"
        AccessPointTags:
          - Key: Name
            Value: hello-func-ap
      DependsOn: EFSFileSystem
        
こちらも先行してEFSFileSystemが生成完了した後でないと使えないので、DependOnに指定することをお忘れなく。

あとは
前回の記事で解説したように、アクセスポイントの設定を素直に反映した書式となっていますので、すんなりと理解できると思います。


デプロイと実行

VPC Lambdaといえども、Serverless Frameworkでの扱いは通常のLambdaと特に変わりません。

            
            $ npx sls deploy --verbose
Deploying apigw-vpclmb-lab to stage dev (ap-northeast-1)

#...スタックが沢山構成される

✔ Service deployed to stack apigw-vpclmb-lab-dev (113s)

endpoint: ANY - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/
functions:
  hello: apigw-vpclmb-lab-dev-hello (79 kB)

Stack Outputs:
  HelloLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:123456789012:function:apigw-vpclmb-lab-dev-hello:1
  HttpApiId: xxxxxxxxxx
  ServerlessDeploymentBucketName: apigw-vpclmb-lab-dev-serverlessdeploymentbucket-xxxxxxxxxxxx
  HttpApiUrl: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
        
今回はリソースを多く生成しているので、デプロイ完了まで数分程度時間がかかります。

しばらく待ってエラーが出なければ上手くVPC Lambdaがデプロイされたようです。

あとは実行確認で、
sls invokeコマンドを使ったり、

            
            $ npx sls invoke --function hello --verbose
{
    "statusCode": 200,
    "body": "\"こんにちは!はじめてのVPC Lambda From Serverless!!\""
}
        
もしくはcurlコマンドでAPIGatewayのエンドポイントURLを叩いたりしてレスポンスを確認しましょう。

            
            $ curl -XGET https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
{
    "statusCode": 200,
    "body": "\"こんにちは!はじめてのVPC Lambda From Serverless!!\""
}
        
ここまでで、「LambdaはEFSちゃんと使えているの...?どう使うの?」とまっとうなご意見が聞こえて来そうですが、問題なく利用できています。

VPC LambdaとEFSの使い方に関しては、今後
「AWS Datasync」の使い方の解説を一回挟んでから改めて解説していきたいと思います。


まとめ

今回はVPC Lambdaを一発構築するためのServerless Frameworkの設定を考えていきました。

もう一度、手順のポイントをおさらいすると、

            
            1. serverless.ymlから.envファイルを使えるようにする
2. VPC Lambdaに対応したIAMロールを指定する
3. VPC Lambdaに必要な関数設定を作成する
4. リソースでVPCの設定(セキュリティグループ)を行う
5. リソースでEFSの設定(FS本体・マウントターゲット・アクセスポイント)を行う
        
というポイントに注意する必要がありました。

手順の内容のイメージがつかみにくい場合には、一度AWSダッシュボードから手動で動作の確認をしてみるとよいでしょう。

参考サイト

以下の参考記事はランタイムをPythonで紹介されている方の構築手順になります。

普段nodejsを使わない方は参考になるかも知れません。

参考|Serverless FrameworkでEFS for AWS Lambdaをデプロイする