サーバーレスなシステムをプラットフォームに依存せず作る 〜 #devboost 登壇に寄せて〜

この記事は Serverless Advent Calendar 2018JX通信社 Advent Calendar 2018の16日目です。

12月15日、Developers Boostにて「なぜサーバーレス『と』Dockerなのか 〜インフラ運用を最小化するサービス開発〜」というタイトルで発表させていただきました。本稿はその補足記事です。

※ 登壇資料中にもありますが、今回は サーバーレス=FaaS(AWS Lambda など)として扱います 。また、記事中の例示はすべて AWS Lambda のものです。

JX通信社の NewsDigest では、サーバーのデプロイ環境として、Docker クラスターやサーバーレスを活用 しています。お互い同列なデプロイ環境として考えて、ケースバイケースで使い分け ているような感じです。

サーバーレスや Docker の本番利用の事例も増えてきていると思いますが、サーバーレスかコンテナのどちらかしか使わないのではなく、どちらもメリット・デメリットある中で「どのように使い分けるのか」という判断が重要です。また、特にサーバーレスに関しては 「チームでシステム開発するには」 という視点を忘れずに持っておくべきだと考えています。

サーバーレスのチーム開発

チームでシステム開発をする際に、だいたい次のものは必要なのではないでしょうか。

  • プログラムを書くこと(ここは当たり前ですね)
  • 簡単にローカル環境が作れること
  • 自動テスト
  • 簡単にデプロイできること
  • CI/CD
  • 監視

サーバーレスを「ただのデプロイ先」として考えると当然、サーバーレスであっても、全く同じものが必要です。

f:id:yamitzky:20181215183218p:plain

サーバーレスに依存しないサーバーレスバッチ

サーバーレスを「ただのデプロイ先」として扱うには、アプリケーションの作り方も特定のプラットフォームに依存せず動くように工夫する必要があります。

まずは、次のようなコードを見てみましょう。次のプログラムは、CloudWatch Events で指定された text を標準出力する Lambda 関数です。

def lambda_handler(event, context):
    # CloudWatch Eventにて、JSONパラメータを設定 {"text": "text to show"}
    print(event['text'])

当然ながら、このシステムは、Lambda 上でしかろくに動きません。このような場合、次のようにリファクタリングをします。

import argparse

def show(text: str):
    print(text)

def lambda_handler(event, context):
    # CloudWatch Eventにて、JSONパラメータを設定 {"text": "text to show"}
    show(event['text'])

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('text')
    args = parser.parse_args()
    show(args.text)

こうすると、このシステムは 「サーバーレス環境でも動くだけの、ただの CLI プログラム」 として抽象化されます! ただの CLI プログラムなので、ただの CLI プログラムとして、テストをしたりすれば良いということになります。手元で検証するときも python main.py hogehoge と実行すれば、ただの CLI プログラムとして適切に実行されます。

サーバーレスに依存しないサーバーレス API

上記の例はバッチのようなシステムの用途の想定ですが、API についても同様です。

awsgiZappa を使うと、普通の Python の API が AWS Lambda にデプロイできる API に早変わり します。次の例は、ただの Flask 製 API としても、サーバーレスな API としてもデプロイできます。

import awsgi
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def index():
    return jsonify(status=200, message='OK')

def lambda_handler(event, context):
    return awsgi.response(app, event, context)

このような形であれば、テストも「ただの Flask API」としてテストすれば良いですし、環境構築等も同様です。

同様の「サーバーレスじゃない API をサーバーレスでも動くようにするライブラリ」は Node(express)Go などでもあるみたいです。

これらのライブラリは、「今あるものをサーバーレスに移行する」というモチベーションが多いような気もしますが、個人的には新規プロジェクトでも積極的に使っています。

デメリットとしては、

  • 余分なライブラリを介するので遅くなるかもしれない
  • 「エンドポイントごとに関数をわける」みたいな設計にはならない

というのがあったりするので一長一短ですが、serverless フレームワークに乗せるにしても「プラットフォームに依存せず動く」という方針はキープしておくと良いように思います。

サーバーレスのテスト

あまり特別なことはしていませんが、サーバーレスではマネージドサービスが頻出するので、dynamodb-locallocalstack のようなモックをよく活用しています。

ただしこのやり方は Twelve-Factor App の開発/本番一致に反していますし、モックサービスは完璧ではありません。「本物のマネージド・サービスを使うべきだ」という意見もあると思います(一長一短ですね)。

また、t_wada さんによる「外部に依存したコードもテストで駆動する」も参考になると思います(Alexaスキルをテスト駆動開発しているものです)。

speakerdeck.com

サーバーレスなプロジェクトのローカル環境例

発表では docker-compose を使うとさらっと述べたのですが、サーバーレスなプロジェクトも、Docker クラスターへデプロイするプロジェクトも、基本的に docker-compose を使って環境構築がさらっとできるようになっています。JX 通信社のアクティブなリポジトリだとほぼ全て docker-compose.yml が用意されています。

例えば、次のように定義します。

version: '3.5'
services:
  app:
    build: .
    command: python -m flask run --host 0.0.0.0
    volumes:
      - .:/usr/src/app
    depends_on:
      - dynamodb

  dynamodb:
    image: amazon/dynamodb-local

こうしておくと、「ただの DynamoDB を使う一般的なプロジェクト」として扱うことができますし、サーバーレス環境にデプロイする際に必要な Liunx 下でのライブラリなども取れて便利です。dynamodb-admin みたいな、GUI のものも入れておくと便利です。

余談1:デプロイ

デプロイについては、

を使ってるケースが多いです。Apexも使ってましたが、最近は使ってないです。今から始めるならserverlessとかも良いと思います(参考:サーバーレスのメリット&本質を、AWS Lambdaを使って理解しよう)。

余談2:始め方

懇親会では「どうやって始めたの?」という質問も聞かれました。

サーバーレスもDockerクラスターも、まずは新規のプロジェクトで試しに本番運用してみて「良さそう」となってから横展開しました。サーバーレスは tech.jxpress.net でも1章割いて紹介したログ基盤が使い始めたプロジェクトです。

まとめ

伝えたかったのは、

  • IaaS/VPS を直接運用している場合は、サーバーレス化/Docker化するとインフラ管理が楽になる
  • 「サーバーレス vs コンテナ」じゃなくて、どっちも併用して使い分けたい
  • サーバーレスであっても「チーム開発」の視点を持ちたい
  • そのために プラットフォームに依存しないように作りましょう (本稿)

といったところです。

今回は貴重な登壇の機会をいただき、デブストオーガナイザーの近藤さん、スタディスト北野さん、ありがとうございました!

また、発表では拙いところもあったと思いますが、何かわからないことがあれば Twitter 上で @yamitzky までメンションを飛ばしていただければと思います。

www.wantedly.com