Serverless Framework でスタックを分割してデプロイする

Serverless Framework でスタックを分割してデプロイする

こんにちは、エンジニアの牧田です。最近「AWS Certified Developer – Associate」に合格しました。今回はServerless Frameworkを使用して様々なリソースをデプロイするときに、スタックを分割することのメリットや方法を紹介します。

Serverless Frameworkとスタックについて

MMMでは、サーバーレスアプリケーションを作成する際に、Serverless Frameworkを用いることが多いです。Serverless Frameworkはデプロイ時に裏側ではCloud Formationが動いており、serverless.ymlにCloud Formationの記述を追加することで、API Gateway・Lambdaの他にも、様々なリソースをデプロイすることができます。

今回は以下のリソースをデプロイすることを考えてみます。

・VPCなど
・DB
・API Gateway / Lambda(VPC内)

スタックを分割することのメリット

Serverless Frameworkでは、1つのserverless.ymlファイルがCloud Formationの1つのスタックとなりますが、これらの全てのリソースを1つのスタックでデプロイすると、以下のような点が気になります。

  • デプロイ時にスタックに何らかの問題が発生してしまい、スタックを一度作り直したいと思った時、同じスタックに存在するDBのデータも影響を受けてしまう。
  • Lambda関数は頻繁に更新してデプロイするので、同じスタックにあるDBなどに影響を与えるリスクをなくしたい

このような問題を避けるために、スタックを分割することを考えます。スタックを分割する場合は、リソースのまとまりごとや、ライフサイクル(どれくらいの頻度で更新されるかなど)を元に分けるといいでしょう。今回の場合、VPC関連やDBは一度デプロイしたらそうそう変わることはないのに対し、API・Lambdaは頻繁に変更・追加がされるものです。なので、この2つに分けてスタックを作成していきます。

また、Cloud Formationには1スタックあたりのリソースは200個までという制限があります。スタックを分割することで、この制限に達することの防止にもなります。

構成

ディレクトリ構成は以下のようにします。(具体的な関数等のファイルは省略しています)
db.yml・vpc.ymlは、リソースを別のファイルとして書いています。長くなる部分は分割することでserverless.ymlがスッキリしてメンテナンスもしやすくなります。また、(今回の部分には関係ないですが)言語はGoを使用しています。

.
├── serverless
│       ├── api
│       │       └── serverless.yml
│       └── vpc-db
│                   ├── db.yml
│                   ├── serverless.yml
│                   └── vpc.yml
└── .env

コード例

  • vpc-db/serverless.yml
service: sample-vpc

custom:
  dotenv:
    path: ../../
  stage: ${env:STAGE}
  name: sample-vpc-${env:STAGE}

plugins:
  - serverless-dotenv-plugin

provider:
  name: aws
  runtime: go1.x
  stage: ${self:custom.stage}
  region: ap-northeast-1
  stackName: ${self:custom.name}-stack

resources:
  # 別ファイルからリソースを参照
  - ${file(./vpc.yml)}
  - ${file(./db.yml)}

  # 別スタックで必要な値を出力
  - Outputs:
      LambdaSecurityGroup:
        Value: !Ref LambdaSecurityGroup
        Export:
          Name: LambdaSecurityGroup-${self:custom.stage}
      PrivateSubnetA:
        Value: !Ref PrivateSubnetA
        Export:
          Name: PrivateSubnetA-${self:custom.stage}
      PrivateSubnetC:
        Value: !Ref PrivateSubnetC
        Export:
          Name: PrivateSubnetC-${self:custom.stage}
  • vpc-db/vpc.yml
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: sample-vpc
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: ap-northeast-1a
      Tags:
        - Key: Name
          Value: sample-subnet-a
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: ap-northeast-1c
      Tags:
        - Key: Name
          Value: sample-subnet-c
  # 他のリソースは省略
  • api/serverless.yml
service: sample-api

custom:
  dotenv:
    path: ../../
  stage: ${env:STAGE}
  name: sample-api-${env:STAGE}

plugins:
  - serverless-dotenv-plugin

provider:
  name: aws
  runtime: go1.x
  stage: ${self:custom.stage}
  region: ap-northeast-1
  apiName: ${self:custom.name}-api
  stackName: ${self:custom.name}-cfs-api
  vpc:
    # 別スタックからExportされた値を読み込む
    securityGroupIds:
      - !ImportValue LambdaSecurityGroup-${self:custom.stage}
    subnetIds:
      - !ImportValue PrivateSubnetA-${self:custom.stage}
      - !ImportValue PrivateSubnetC-${self:custom.stage}
  iam:
    role:
      # (省略)

functions:
  GetSamples:
    events:
    - http:
        method: get
        path: /samples
    handler: handlers/api/main
# などの関数(実際は別ファイルで書いています)

使用しているプラグインに関してですが、serverless-dotenv-pluginを使っています。これは、.envファイルに書いた環境変数をserverless.ymlに読み込むためのものです。${env:STAGE}のように読み込むことができます。ただし注意点として、.envとserverless.ymlの場所が異なる場合は、以下の部分のようにserverless.ymlから見た.envのパスを指定してあげる必要があります。

custom:
  dotenv:
    path: ../../

別スタックの値を参照する方法

今回の場合、LambdaがVPC内にあるということで、VPCに関する情報をvpc-dbスタックからapiスタックに渡す必要があります。Cloud FormationのOutputsという機能を使用してこれを実現します。

まず、vpc-db側のserverless.ymlで、以下の部分で必要な項目をexportします。

- Outputs:
      LambdaSecurityGroup:
        Value: !Ref LambdaSecurityGroup
        Export:
          Name: LambdaSecurityGroup-${self:custom.stage}
      PrivateSubnetA:
        Value: !Ref PrivateSubnetA
        Export:
          Name: PrivateSubnetA-${self:custom.stage}
      PrivateSubnetC:
        Value: !Ref PrivateSubnetC
        Export:
          Name: PrivateSubnetC-${self:custom.stage}

そして、api側のserverless.ymlでは、以下の部分でexportした値をimportしています。注意点として、serverless.ymlのcustom:の部分ではImportValue関数は使えません。

vpc:
    # 別スタックからExportされた値を読み込む
    securityGroupIds:
      - !ImportValue LambdaSecurityGroup-${self:custom.stage}
    subnetIds:
      - !ImportValue PrivateSubnetA-${self:custom.stage}
      - !ImportValue PrivateSubnetC-${self:custom.stage}

*Cloud Formationのスタック名を指定して、別スタックのoutputした値を読み込むやり方もあります。
(参考)https://qiita.com/hamanakamakoto/items/a14fab3c7b9ddbd80fee

デプロイする際の注意点

別のスタックが出力した値を参照する場合は、出力する側のスタックを先にデプロイする必要があります。そうでないと、まだデプロイされていないスタックの値を参照することはできません。

まとめ

今回は、Serverless Framework でスタックを分割してデプロイする方法を紹介しました。Cloud Formationの機能を活用することで、スタックを分割した場合でも、値の参照を問題なく行うことができました。Serverless Frameworkはとても便利なので、これからも様々な機能を学んで活用してきたいところです。