Serverless FrameworkでAurora Postgres + VPC Lambda + RDS Proxyをデプロイする

Serverless Frameworkを用いて、RDS Aurora Postgres、VPC Lambda、RDS Proxyをまとめてデプロイする方法をご紹介します。
2020.09.23

こんにちは、クラスメソッドの岡です。

7月にRDS ProxyがGAとなったことで、サーバーレスでのRDS実用化待ったなし!!ということで今回はServerless FrameworkでRDS Aurora Postgres、VPC Lambda、RDS Proxyをまとめてデプロイしてみたので、テンプレートの中身を紹介していこうと思います。

動作確認環境

  • Python3.8.5
  • Serverless Framework
    • Framework Core: 2.1.1
    • Plugin: 4.0.4
    • SDK: 2.3.2
    • Components: 3.1.3

構成

まず、LambdaとRDSを同一のVPC内に所属させます。マルチAZ構成にするためにサブネット(プライベート)を2つ作成します。
今回はLambdaとRDSを同じサブネット内に配置しています。
セキュリティグループはLambda用とRDS用の2つ作成して、RDS用のインバウンドのルールは同一VPC内からのアクセスのみを許可します。

テンプレート

リソースを追加する前にテンプレートのベースを用意しておきます。

service:
  name: sample-aurora-postgres

plugins:
  - serverless-pseudo-parameters

provider:
  name: aws
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}
  profile: ${self:custom.profiles.${opt:stage, self:custom.defaultStage}}
  runtime: python3.8
  stackName: sample-aurora-postgres
  apiName: sample-aurora-postgres
  logRetentionInDays: 7
  versionFunctions: false
  iamRoleStatements:
    - Effect: Allow
      Action:
        - secretsmanager:GetSecretValue
        - rds-data:*
        - ec2:CreateNetworkInterface
        - ec2:DescribeNetworkInterfaces
        - ec2:DeleteNetworkInterface
      Resource: "*"
  environment:
    ENV: ${self:custom.environments.ENV}
    DB_PORT: 3306
    DB_HOST: !GetAtt RDSProxy.Endpoint

custom:
  defaultStage: itg
  profiles:
    itg: sls-itg
    stg: sls-stg
    prd: sls-prd
  environments: ${file(./config/config.${opt:stage, self:custom.defaultStage}.yml)}
  secret: ${file(./config/secret/.secret.${opt:stage, self:custom.defaultStage}.yml)}

プラグイン

serverless-pseudo-parameters はテンプレート内でアカウントIDを参照するために追加しています。
このプラグインを使うと、#{AWS::AccountId}という記述で参照できます。

ステージとプロファイル

AWSアカウントは環境毎に分かれるケースが多いかと思います。 デプロイコマンド実行時のstageオプションで下記のように切り替えるためにテンプレートに ${opt:stage, self:custom.defaultStage} と変数を仕込んでおきます。

$ sls deploy                 →開発アカウント
$ sls deploy --stage=stg     →検証アカウント
$ sls deploy --stage=prd     →本番アカウント

stageと一緒にprofileも切り替えられるよう、custom.profilesにそれぞれのステージに相対するAWS CLIのプロファイル名を設定しておきます。

認証情報の読み込み

RDSのクラスター作成時とSecrets Managerのシークレット作成時に同一の認証情報を指定するために ./config/secret/.secret.${env}.yml という隠しファイルを作っておきます。

USER_NAME: postgres
PASSWORD: postgres

Serverless Frameworkではテンプレート内で ${file(file_name)}と指定することで、外部ファイルのプロパティを読み込めます。
今回はローカルからデプロイするため、この様な構成にしてますが作成したシークレット情報はGitに含めないよう注意してください。

DBのポートとエンドポイント

ちなみにこちらのドキュメントにある通り、Aurora Postgres(EngineMode: provisioned)のDBクラスターのポートを指定しなかった場合はPostgresでもデフォルトで3306ポートとなります。
今回はポート指定せずに作成するので、3306ポートを共通の環境変数として設定しておきます。 DB_HOSTには後述するRDSProxyのエンドポイントを参照できるようにしておきます。

VPC関連のリソース

Serverless Frameworkと言いつつ、作成するリソースは実質ほぼcfnの記述になります。

resources:
  Resources:
    ## VPC Resource
    VPC:
      Type: AWS::EC2::VPC
      Properties:
        CidrBlock: 10.0.0.0/24
        Tags:
          - { Key: Name, Value: Sample VPC }
    PrivateSubnetA:
      Type: AWS::EC2::Subnet
      Properties:
        VpcId: !Ref VPC
        CidrBlock: 10.0.0.0/25
        AvailabilityZone: ap-northeast-1a
        Tags:
          - { Key: Name, Value: Sample Private A }
    PrivateSubnetC:
      Type: AWS::EC2::Subnet
      Properties:
        VpcId: !Ref VPC
        CidrBlock: 10.0.0.128/25
        AvailabilityZone: ap-northeast-1c
        Tags:
          - { Key: Name, Value: Sample Private C }
    LambdaSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        GroupDescription: SecurityGroup for Lambda Functions
        VpcId: !Ref VPC
        Tags:
          - Key: "Name"
            Value: "LambdaSecurityGroup"
    AuroraSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        GroupDescription: SecurityGroup for Aurora
        VpcId: !Ref VPC
        SecurityGroupIngress:
          - IpProtocol: tcp
            FromPort: 3306
            ToPort: 3306
            CidrIp: 10.0.0.0/24
        Tags:
          - Key: "Name"
            Value: "AuroraSecurityGroup"
      DependsOn: VPC

まずはVPC関連のリソース群です。
セキュリティグループはVPC内のIPアドレス範囲の3306ポートをインバウンドで開けておきます。

RDS関連のリソース

    ## RDS Resource
    DBSubnetGroup:
      Type: AWS::RDS::DBSubnetGroup
      Properties:
        DBSubnetGroupDescription: "SampleDB subnet group"
        DBSubnetGroupName: sampledb-subnet-group
        SubnetIds:
          - !Ref PrivateSubnetA
          - !Ref PrivateSubnetC
    DBCluster:
      Type: AWS::RDS::DBCluster
      Properties:
        DatabaseName: SampleDB
        Engine: aurora-postgresql
        # EngineMode: serverless
        EngineVersion: "11.6"
        MasterUsername: ${self:custom.secret.USER_NAME}
        MasterUserPassword: ${self:custom.secret.PASSWORD}
        DBClusterParameterGroupName: !Ref DBClusterParameterGroup
        DBSubnetGroupName: !Ref DBSubnetGroup
        VpcSecurityGroupIds:
          - !Ref AuroraSecurityGroup
      DependsOn: DBSubnetGroup
    DBClusterParameterGroup:
      Type: AWS::RDS::DBClusterParameterGroup
      Properties:
        Description: A parameter group for aurora
        Family: aurora-postgresql11
        Parameters:
          client_encoding: UTF8
    DBInstance1:
      Type: AWS::RDS::DBInstance
      Properties:
        DBClusterIdentifier: !Ref DBCluster
        DBSubnetGroupName: !Ref DBSubnetGroup
        Engine: aurora-postgresql
        EngineVersion: "11.6"
        DBInstanceClass: db.t3.medium
      DependsOn: DBCluster

今回はプライマリインスタンス1台(DBInstance1)のみ作成します。ちなみにDBClusterのEngineModeをserverlessにすると、Aurora Serverlessでの起動となります。
サブネットを2つ以上指定する場合は、DBSubnetGroupを作成する必要があります。

Secrets ManagerとRDS Proxy周りのリソース

    AuroraSecret:
      Type: AWS::SecretsManager::Secret
      Properties:
        Name: Sample/aurora
        SecretString: '{"username":"${self:custom.secret.USER_NAME}", "password":"${self:custom.secret.PASSWORD}"}'
    SecretTargetAttachment:
      Type: AWS::SecretsManager::SecretTargetAttachment
      Properties:
        SecretId: !Ref AuroraSecret
        TargetId: !Ref DBCluster
        TargetType: "AWS::RDS::DBCluster"
      DependsOn: DBCluster
    ProxyRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: sample-proxy-role
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - "rds.amazonaws.com"
              Action:
                - "sts:AssumeRole"
        Path: /
        Policies:
          - PolicyName: RdsProxyPolicy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: Allow
                  Action:
                    - "secretsmanager:GetResourcePolicy"
                    - "secretsmanager:GetSecretValue"
                    - "secretsmanager:DescribeSecret"
                    - "secretsmanager:ListSecretVersionIds"
                  Resource:
                    - !Ref AuroraSecret
                - Effect: Allow
                  Action:
                    - "kms:Decrypt"
                  Resource: "arn:aws:kms:${self:provider.region}:#{AWS::AccountId}:key/*"
                  Condition:
                    StringEquals:
                      kms:ViaService: "secretsmanager.${self:provider.region}.amazonaws.com"
      DependsOn: AuroraSecret
    RDSProxy:
      Type: AWS::RDS::DBProxy
      Properties:
        DBProxyName: SampleAuroraProxy
        Auth:
          - SecretArn: !Ref AuroraSecret
        VpcSecurityGroupIds:
          - !Ref AuroraSecurityGroup
        VpcSubnetIds:
          - !Ref PrivateSubnetA
          - !Ref PrivateSubnetC
        EngineFamily: POSTGRESQL
        RoleArn: !GetAtt ProxyRole.Arn
      DependsOn: AuroraSecret
    DBProxyTargetGroup:
      Type: AWS::RDS::DBProxyTargetGroup
      Properties:
        TargetGroupName: default
        DBProxyName: !Ref RDSProxy
        DBClusterIdentifiers:
          - !Ref DBCluster
      DependsOn: RDSProxy

ProxyRoleはRDS Proxyに設定するIAMロールです。
今回はSecrets ManagerにRDS用の認証情報を格納しているので、Secrets Managerの権限と取得後にKMSでの復号化権限をアタッチしておきます。

RDS Proxyの認証情報をSecrets Managerに保存する場合、 SecretTargetAttachment を作成する必要があります。

また、注意点として公式ドキュメントにもある通り、DBProxyTargetGroupのTargetGroupNameはdefaultの固定値に設定する必要があります。

VPC Lambdaの定義

functions:
  testFunc:
    name: test_func
    handler: src/handlers/test_func.handler
    description: "接続確認用"
    vpc:
      securityGroupIds:
        - !Ref LambdaSecurityGroup
      subnetIds:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetC
    events:
      - http:
          path: /test
          method: post
          cors: true

VPCをLambdaに設定します。上で作成したセキュリティグループとサブネットを指定します。

デプロイ

下記コマンドで開発環境にデプロイします。

$ sls deploy

RDSのクラスターとインスタンスの生成に結構時間がかかります。