Progateにおけるserverless frameworkの運用知見を紹介します

Progateの小笠原です。普段はSREチームで開発効率化とサービスの安定化に取り組んでいます。

本稿では普段活用しているserverless frameworkに関する運用知見を共有します。

はじめに

弊社ではIaCの取り組みとして永続的なインフラリソースは基本的にすべてコード化しています。リソースの多くはterraformを使って管理しているのですがAWS Lambdaについてはデバッグ環境が整っており取り回しの良いserverless frameworkを用いて管理しています。

今回はserverless frameworkに関してProgateで運用していて踏んだ問題とその対応方法を紹介します。

Progateにおけるserverless frameworkの活用状況

f:id:shotaogasawara:20201223124915p:plain
serverless framework

Progateでは現状、バッチやデプロイに関するスクリプトやChatOpsアプリケーションなど、ユーザには直接見えない部分で主に利用しています。

導入のきっかけは社内で何度かserverlessを使ってサービスを作った実績があったことと、新しく入ったメンバーがserverless推しだったことで、割とカジュアルにLambda管理はこれで行こう!というノリで導入されました。

普段serverless開発に関わるメンバーは2人で、触ることがあるのは多くて4人程度と小規模体制のため効率重視で開発・運用しています。そのためCIは組まずに開発メンバーが手元から必要に応じてデプロイし、構築時や構成変更の際に軽いPR共有のみ実施して基本的には開発者が責任を持ってメンテするという開発体制です。

serverlessなサービスの構築・運用が始まってから管理リソースも増えそれなりに時間も経ってきて運用課題も見えてきたという状況です。

開発・運用で踏んだ課題とその対応

開発・運用していて踏んだ問題とその対応を以降では3つ紹介していきます。

1. Stackが膨らむ問題

f:id:shotaogasawara:20201223123931p:plain
1 serverless.yml = 1 cloudformation stack

serverless frameworkは1つのサービス(一つのserverless.yml)につき1つのCloudFormationスタックのため、リソースが増えると必然デプロイにも時間がかかるようになります。また、デプロイ時間だけでなく影響範囲が広くなると作業のコンフリクトや思わぬデグレの発生につながることがあります。

例えばAさんとBさんが同一サービスを共同で開発する場合、Aさんがfeatureブランチを元にデプロイして動作確認をしている間Bさんは基本的にはデプロイできなくなります。少人数で開発が重なることが少なければある程度コミュニケーションコストをかけて運用するのも無理ではありませんが非効率で無駄が多いのは否めません。

また、弊社では開発担当者がローカルでデプロイを走らせる運用なのですが、誤って古いブランチから切ったブランチからデプロイしてサービスの一部が気づかぬうちにデグレするケースがありました。

対応策

問題によって対応は変わってくるのですが共通して問題なのはスタックが肥大化していることです。スタックを小さく分割することができればデプロイ時間を短縮できるし影響範囲の絞り込みも可能になります。

弊社ではChatOpsのアプリケーションで様々な機能を当初一つのスタックにまとめていたのですが、Chatのインターフェースと呼び出し先の機能を別スタックに分割することで問題を緩和しています。

f:id:shotaogasawara:20201223123934p:plain
Chatアプリケーションのスタックを機能ごとに分割する

分割方法は機能単位の他に、例えばこちらのケースではアプリケーションレイヤとネットワークレイヤで分割している例が紹介されているので参考にできそうです。

(注意)もちろん、分割の仕方がまずいと逆にスタック間にデプロイ順等の依存関係が発生して扱いづらくなることもあり得るので、バランスを見て切る必要がありそうです。

次に作業のコンフリクトとデグレの問題ですが(上述のスタック分割を行った上で)ローカルデプロイを禁止してCIを導入することで問題を回避することが可能です。CI上でのみデプロイさせることで、常にサービスの状態を(mainなどの)特定ブランチの状態と一致させることができますし、デプロイを直列実行させることで並列実行による事故も予防できます。

一方で、ローカルからのデプロイを禁止するとちょっとした変更の動作検証がやりづらくなるため検証環境の準備が別途必要になったり、デプロイがコケた場合のロールバック手順を用意したりCIをメンテする手間が増えるという問題があるため一概にCI導入が良いということでもなさそうです。

これも開発体制とのバランスを見て導入を考える必要がありそうです。実際弊社では開発者が現状少ないためCIは導入せずローカルデプロイ運用を続けていますが特に大きな問題とはなっていません。今後開発者が増えたタイミングでCI導入やデプロイ手順見直しが必要になるのではないかと考えています。

2. IAMのデフォルトロールに関する問題

serverless frameworkはデフォルトの書き方だとグローバルなIAMロールが一つ作成され、それを全Lambdaで使いまわします。

例えば以下の設定を入れるとS3のリスト権限が付いたIAMロールが一つ作成され、全Lambdaにそのロールが紐付けられます。

provider:
  name: aws
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:List*"
      Resource:
        - '*'

一旦動かすためという目的であれば一つのroleに全部乗せで楽なのですが、Lambdaごとに権限を適切に制限したいとなったときに権限の精査が必要となり辛い思いをする可能性があります。

対応策

デフォルトのロールは気を効かせてCloudWatchLogsの権限が自動で付与されており、カスタムロールを利用する場合は自前でその記述が必要となるようです。なお、カスタムロールを関数ごとに用意する際はこちらのpluginを利用することでyml形式で綺麗に記述できるようです。

IAM権限の制限は厳密さを求めると工数が膨らみがちなので各社のセキュリティ要件に応じて必要な分だけ実施すればよいと思います。

(注意)ちなみに分割する際に権限精査が必要と書いたのですがこちらのpluginではLambdaに必要な権限を自動で生成してくれるようなので大した労力をかけなくても機械的に分割できるかもしれません。

3. 開発者ごとに得意言語が異なる問題

共同開発を進めると開発者ごとにLambdaを書く際に使用する言語の好みが分かれることがあります。言語の得意不得意で開発効率は結構変わるのでなるべく開発者が得意な言語を利用できるようにしておくことが重要です。

対応策

serverless frameworkはmulti-runtimeに対応しているので関数ごとに言語選択が可能です。

functions:
  helloGo:
    runtime: go1.x
    handler: go/bin/hello
  helloRuby:
    runtime: ruby2.7
    handler: ruby/src/handler.hello

ただ、ルートディレクトリにGemfileやMakefileなどビルド処理(依存関係解決やコンパイル)に必要なファイルを全部置くと読みづらいのでランタイム別にディレクトリを切ることで綺麗にできます。

以下はRubyとGoを組み合わせる場合の構成例です。

. # ルートにはデプロイに必要なnpmモジュール等を配置
├── README.md
├── go # Goのソースコードとビルドに必要なファイルを配置
│   ├── Makefile
│   ├── bin
│   │   └── hello
│   ├── go.mod
│   ├── go.sum
│   └── src
│       └── hello.go
├── package.json
├── ruby # Rubyのソースコードとビルドに必要なファイルを配置
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── src
│   │   ├── hello.rb
│   │   └── ls_buckets.rb
│   └── vendor
│       └── bundle
├── serverless.yml
└── yarn.lock

まずrubyとgoのディレクトリ内でビルド処理(依存関係解決やコンパイル)が完結できるようにGemfileやMakefileを配置します。

次にルートディレクトリにはserverless.ymlとnpm関連ファイルを用意し、デプロイを行うタスクランナー(今回はnpm-scriptsを利用)を配置します。タスクランナーに以下のようなコマンドを用意すれば yarn run build-allコマンド一つで言語ごとのビルドを行い、yarn run deploy コマンドでその成果物をアップロードできます。

{
  // ...省略
  "scripts": {
    "build:ruby": "cd ruby; rm -rf vendor/bundle; bundle install --with=staging,production --path=vendor/bundle",
    "build:go": "cd go; make build",
    "build-all": "run-s build:*",
    "deploy": "serverless deploy -v"
  }
}

サンプルコードはこちらにありますので参照してください。

とはいえ、むやみに言語を増やすとメンテナンスコストも増大するため弊社ではコンパイル言語ならGo、スクリプトならRuby or node.jsを利用するルールとしています。これもチームメンバーの構成によってバランスを見つつ決めるのが良いと思います。

まとめ

Progateでserverless frameworkを使って開発・運用する際に踏んだ問題とその対応を3つ紹介しました。

ツールとしては高機能で開発も活発に行われている、またデフォルトの状態から要件に応じて拡張させられる柔軟性があるので使いやすい印象です。 現状は運用が粗い部分もあるもののときどきの開発体制に応じて改善を続けながら開発していけたら良いなと考えています。

さて、次回は kzk-maeda さんによる投稿です。お楽しみに!

参考