Serverless FrameworkとTerraformの使い分けについて

Marketing Solution Division所属のエンジニアの坂本です。

FaaSデプロイの悩み

早速本題ですが、AWS Lambdaのデプロイについて悩んでませんか?

特に検索で飛んで来た方は様々なツールや管理方法を模索しているのではないかと思います。

現行のクラウド管理はIaCを使うことがほぼほぼスタンダードになっており、Terraformなどのツールを使ってIaCで構築しているかと思います。

(IaCについて知りたい方は記事を書いていますのでご参考いただければ。)

確かにインフラ側の目線だけで言えばTerraformでLambda等のFaaSを管理することもできます。

しかし、開発環境をVSCode Dev Containerで整えようとするとめんどくさかったり、インフラ管理であるIaCにLambdaコードが入っていてよいのか考えたり、色々と考慮が必要なため管理するうえで扱いに困るという方が多いのではないでしょうか?

そうした際に役に立つのがServerless Framework(以下、Serverless)などのFaaS用構成管理デプロイツールです。

https://www.serverless.com/

ServerlessはLambdaのインフラ~アプリケーションを一括で管理してくれ、尚且つコンフィグファイルもそこまで面倒な設定は必要ないためアプリコードの中に1ファイル入れておくだけで済みます。

しかし、それゆえ発生する問題がTerraformとの住み分けです。インフラのコードは様々な依存関係が存在します。ネットワーク, それに伴うルーティング, Gateway, logの吐き出し先などなど。

これらはServerlessで完結するのは難しく、対応していないリソースがあったり、そもそも大規模システムであればインフラはTerraform等の専用ツールで管理した方が拡張性や差分管理がしやすいです。

そこで今回は社内で開発したSlackChatBotで使用した構成を例に話していきたいと思います。

本件で例に挙げるChatBotは最近流行のChatGPTSlack経由でアクセスするためのものとなり、AWS上に構築されます。

その際に作成したインフラは下記のようになっています。

中身としてはシンプルにSlack botからのREST通信をAPI-Gatewayで受けて、WAF及び認可Lambdaを通過すれば、Lambdaで内容を処理をしてAzure上のOpen AIに投げるといった構成にしています。

インフラとアプリの責任分界点

結論

最初に結論からいうと今回のServerlessの構成としては下記の図と表のようになっています。

構成図(Severless以外は全てTerraform管理)

リソース対応表

リソース

Terraform

Serverless framework

VPC(subnet, NAT含む)

WAF

Parameter store

△ ※read only

API Gateway(REST API)

API Gateway(リソース)

△ ※初回作成のみ

API Gateway(ステージ)

△ ※初回作成のみ

Lambda

※API Gatewayのリソースとステージは作成必須のため初回のみ構築し、Terraform上では変更を検知しないようにしています。

おそらく大半の方はServerless側にAPI Gatewayを寄せているかと思いますが、何故このような構成にしたかを説明させていただければと思います。

理由

今回寄せなかった理由は一重に、インフラの設計はインフラで一旦管理したかったというのがあります。

Severless側でもAPI Gatewayを作ったり、WAFを作ったりすることは可能です。しかし、VPCなどは作ることがでないため完全にSeverless側のみで構築することは不可能となります。

そうなるとインフラリソースであるWAFなどをSeverless側に寄せると今後の拡張の際にSeverlessTerraformどちらに寄せるかを毎回考えなくてはいけなくなりますし、明確な定義をしないとその時担当した人によって設計がズレてしまう。というのはよくある話かと思います。

そうなった場合の考え方としてたどり着いたのが、アプリ部分以外は完全にTerraformに寄せる今回の作りでした。悩ましかったのはAPI Gatewayの部分ですが、接続先のリソースがないとデプロイできないこと、レスポンスなどアプリ側で決めるべきことが設定内に組み込まれていることからSeverless側に寄せることにしました。

これであればAWSの知識がないアプリエンジニアもSeverlessの設計に取られる時間が最低限で済みますし、アプリのリクエスト・レスポンスにまつわる設定を全てSeverless側にまとめることができました。

Severless側全体をインフラエンジニアはアプリから解放されるので今後アサインされるメンバー選定の際にインフラ、アプリに跨った技術スタックを持つ人物である必要はなくなります。また、アプリとインフラ両方できる人でも新しいリソース(例えばDynamoDBなど)Severlessで管理するかTerraformで管理するか悩まず、Severlessは最低限という設計思想からTerraformに寄せることができます。

実際のコード

それでは実際のseverlessコードを見てみます。

serverless.yml(一部今回の件と関係ない部分はカットしています。

service: arise-slack-llm

frameworkVersion: '3'

provider:
  name: aws
  stage: ${opt:stage} # 各環境ごとの差分吸収のために外部変数をセット
  region: ap-northeast-1
  runtime: python3.9
  apiGateway:
    restApiId: ${ssm:/genai/apigw/chatbot/id}
    restApiRootResourceId: ${ssm:/genai/apigw/chatbot/root_resource_id} 
  tags:
    CreatedBy: Serverless
    System: genai
  stackTags:
    CreatedBy: Serverless
    System: genai
  stackName: genai-${self:provider.stage}-clf-stack-01

package:
  individually: true
  exclude:
    - ./**

functions:
  ariseSlackLlmLambda:
    name: genai-${self:provider.stage}-lambda-chatbot-01
    description: Chatbot Lambda
    role: ${ssm:/genai/iam/chatbot_lambda_invoke/id}
    handler: src/slack_llm_bot/app.handler
    provisionedConcurrency: 1
    memorySize: 512
    timeout: 29
    package:
      include: 
        - ./src/**
      exclude:
        - ./src/authorizer/auth.py
    environment:
      PYTHONPATH: /var/task/src
    events:
      - http:
          path: /{proxy+}
          method: ANY
          authorizer: 
            name: authLambda
            type: request
            identitySource: method.request.header.X-Slack-Signature
    vpc:
      securityGroupIds:
        - ${ssm:/genai/sg/chatbot/root_resource_id}
      subnetIds: ${ssm:/genai/vpc/chatbot/id}
  authLambda:
    name: genai-${self:provider.stage}-lambda-authorizer-01
    description: Authorizer Lambda
    role: ${ssm:/genai/iam/auth_lambda_invoke/id}
    handler: src/authorizer/auth.handler
    memorySize: 256
    timeout: 6 
    package:
      include: 
        - ./src/authorizer/auth.py

途中各所に${ssm:/xxxx}という変数名が入っていますが、こちらはTerraformがSeverless側で必要なリソースを作成した際にIDをParameter storeに格納し、そちらを呼び出しています。
Terraform側のコードは下記のようになります。

main.tf(こちらも説明に不要なリソースは削除しています)

# ------
# api-gateway
# ------
resource "aws_api_gateway_rest_api" "apigw_chatbot_01" {
  name        = "${var.system}-${var.env}-apigw-01"
  description = "GenAI chatbot REST API Gateway"
  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

# ------
# ssm-parameter store(API-GW)
# ------
resource "aws_ssm_parameter" "ssm_parameter_apigw_id_01" {
  name        = "/genai/apigw/chatbot/id"
  description = "API Gateway ID of chatbot"
  type        = "String"
  value       = aws_api_gateway_rest_api.apigw_chatbot_01.id
}

これにより、インフラで作成したリソースidをパラメータストアに格納しており、Severless側が取得することによって依存関係もTerraform→Severlessとなっているため、再帰的な依存や相互依存を防ぐことができ非常にシンプルな管理を実現しています。

メリット

理由の項目にも記載しましたが、上記の構成を実現したことによるメリットとしては

  • IaCとアプリケーション部分が分離させているため、責任分界点がわかりやすい
    ∟Lambdaを含めたアプリケーションのロジック、リクエスト、レスポンスにかかわる部分はSeverless側の変更で完結
    ∟逆にWAFVPCなどといったインフラリソースはTerraform側のみの設定、デプロイが完結
  • 新規のAWSリソースを追加する際にも上記を念頭におけばよいので、人による設計にバラつきがない

といった点が挙げられます。

まとめ

クラウド化が進み、インフラとアプリとの境目が難しくなってきています。

今回挙げた分離方法は、あくまで今回のケース及び、メンバーのスキルによって決定しました。

各ツールでそれぞれのパートの管理しやすさは変わるのでPJのメンバーややりたいことに合わせて、ふさわしい方法で分離いただければと思います。

関連記事