“Beyond the Twelve-Factor App” & “The Twelve-Factor App”から学びAWSで実践する (2)APIファースト


解説

本ファクターはオリジナルにはなく、Beyondで追加されたものです。最近のアプリケーション開発においてAPIは重要性を増しており、非常に重要なファクターになります。APIファーストの良い点を以下のようにまとめています。

  1. アプリケーションの機能要件はAPIの利用を通じてすべて満たされる。UI部分はWebやモバイルアプリといった様々な形式で自由に提供できる
    あらゆる機能をAPIで実装し、UIはそのAPIを利用して構築するというのはすでに当然の構成です。AndroidやiOSといったモバイル端末、Webブラウザ、PC用のデスクトップアプリケーションと同じアプリケーションを提供するとしても、対象端末ごとに最適なユーザーエクスペリエンスを提供するためには、それぞれに適したUI実装が必要となります。その中で、機能要件は共通なロジックとしてAPIで提供されるのはしかるべき構成になります。
  2. ステークホルダー(内部チーム、顧客、APIを利用する他の組織のチーム)と、APIの仕様をベースに議論することができる
    RESTful APIで設計することは、ドメイン駆動設計におけるエンティティの定義と似たような設計になります。ドメイン駆動設計ではユビキタス言語でステークホルダーたちと会話しドメインの定義をしていくことがベースにあります。これと同様で、API仕様を通して議論することで。内部の設計やアーキテクチャにとらわれることなく本質的な議論が可能となります。
  3. APIを開発するためにはAPI BlueprintやApirayを使用する
    最近だとOpenAPI/Swaggerがこの世界のデファクトになったと思いますのでその利用を推奨します。いまさら独自のフォーマットでAPI仕様書を作成し提供するのはやめましょう。
  4. CIにより仕様を維持していくことが容易になる。各サービスチームはそれぞれのサービス開発に注力できる
    APIをテストするのは非常に簡単で開発コストが低いです。逆にUIからの機能テストはCIでやるにしても非常に開発コストが高くテストコードを保守していくだけでも一苦労です。APIで全機能が実現されているのであれば、全機能のテストがAPIベースで実施でき、容易にCIで自動化できます。これにより、サービスの外部機能仕様の維持ができることになります。外部仕様が維持されれば、各サービスチームは自由に、サービス内の仕様変更や仕様追加ができることになります。
  5. ウォーターフォールによるモノリシックなアプリケーション開発から解放される
    APIファーストによりマイクロサービス化されたアプリケーションは、それぞれがアジャイル的に開発されるべきであることが暗に述べられています。アジャイルで言われている自己組織化された各サービスチームがそれぞれの担当サービスを進化させていくことで各サービスは最良のアーキテクチャで進化していくことになり、それらが集まることで、アプリケーション全体が適切に進化していくことになるということが今のトレンドであるといえます。

実践

ドメイン駆動設計、RESTful APIの設計、マイクロサービス、アジャイル開発とAPIファーストの裏には多くの開発トレンドがひしめいています。本記事では、これらをどう進めるかというよりは、具体的なAPIの実装方法にフォーカスを当てたいと思います。APIファーストでの実装を始めるにあたり、AWSではAPI Gatewayを利用するのが最適です。APIのライフサイクル管理、流量制限、ロギング、認証・認可などAPIに必要な要素がマネージドサービスで提供されている便利なサービスになります。

HTTP API + Lambda統合

API Gatewayにはいくつかの実装形態がありますが、今後主流になるであろうHTTP APIの形式で実装の流れを見ていきたいと思います。また、APIのバックエンドとしてはLambdaを利用します。こちらも最近の主流であるサーバレスアプリケーションの構成になります。APIを実装するアプリケーションの構成としては最初にこの構成で実現できないか考えましょう。サーバレスの構成は圧倒的に低コストでサービスを実現できますし、管理も非常に楽です。24時間常時高負荷で高いトランザクションを処理しないといけない」、「バックエンドがRDB」など、Lambdaと相性が悪い一部の例外を除けばとても有用な構成です。

OpenAPI 3.0によるRESTfull API設計

API GatewayのHTTP APIでは、OpenAPI 3.0で定義したAPIをインポートできます。本記事のテーマであるAPIファーストを実現するためには、APIの設計をOpenAPIで定義された仕様ベースで行い議論することが可能です。細かい仕様に関しては公式のドキュメントから確認できますので、参考にしてください。

それでは早速APIの定義をしていきたいと思います。今回定義するリソースは「本」とします。RESTfull APIで設計すると下記のようになります。多くのリソースの設計は、この検索系+CRUDの5APIになるかと思います。「本」のIDはISBNという国際規格で決まっていますのでその値を使うことにしています。

URL メソッド アクション
/books GET 本の一覧を取得する(検索)
/books POST 新規に本を登録する(C)
/books/{isbn} GET isbnで指定した本の情報を取得する(R)
/books/{isbn} PUT isbnで指定した本の情報を書き換える(U)
/books/{isbn} DELETE isbnで指定した本を削除する(D)

これを実際に定義していきます。ちなみに私はVisual Studio Codeにプラグイン(OpenAPI (Swagger) EditorとYAML)を入れて実装をしています。YAMLで定義を書き、SwaggerUIで表示して確認するというのを簡単に行うことができます。また、YAMLプラグインの以下設定を行うと、入力支援が行われるようになるので多少定義作業が楽になります。/openapi/フォルダ配下の*.yamlファイルに対して有効になる設定なので、ファイルを作る際はそのルールに則り作る必要がある点はご注意ください。

    "yaml.schemas": {
        "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json": [
            "/openapi/*.yaml"
        ]
    },

以下が先ほど定義したメソッド+αを実際に実装したOpenAPI 3.0のYAMLファイルです。以下の定義だけで、すでにAPI Gatewayにインポートすることが可能になっています。

openapi: "3.0.2"
info:
  title: "API First"
  version: "1.0"
servers:
  - url: "https://api.server.test/v1/"
paths:
  /books:
    get:
      description: "本の一覧を取得する(検索する)"
      responses:
        "200":
          description: "本の一覧を取得します"
          content:
            "application/json":
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/book"
    post:
      description: "新規に本を登録する"
      requestBody:
        description: 登録する本のを指定します
        content:
          "application/json":
            schema:
              $ref: "#/components/schemas/book"
      responses:
        "204":
          description: "登録成功"
  /books/{isbn}:
    get:
      description: "ISBNで指定した本の情報を取得する"
      parameters:
        - name: "isbn"
          in: "path"
          description: "ISBN"
          required: true
          schema:
            type: "string"
      responses:
        "200":
          description: "OK"
          content:
            "application/json":
              schema:
                $ref: "#/components/schemas/book"
    put:
      description: "ISBNで指定した本の情報を書き換える"
      parameters:
        - name: "isbn"
          in: "path"
          description: "ISBN"
          required: true
          schema:
            type: "string"
      requestBody:
        description: 登録する本の情報を指定します。isbnは書き換えられません。
        content:
          "appication/json":
            schema:
              $ref: "#/components/schemas/book"
      responses:
        "200":
          description: "OK"
    delete:
      description: "ISBNで指定した本を削除する"
      parameters:
        - name: "isbn"
          in: "path"
          description: "ISBN"
          required: true
          schema:
            type: "string"
      responses:
        "204":
          description: "削除成功"
components:
  schemas:
    book:
      type: "object"
      properties:
        "isbn":
          type: "string"
          description: "ISBN"
        "title":
          type: "string"
          description: "タイトル"
        "price":
          type: "integer"
          description: "税抜き価格"

APIファーストではこれをベースに仕様の議論をし設計・実装を進めましょう。上の例では。メソッドの定義に加えて、本の属性をどうするかという議論を行い、ISBNとタイトルと税抜き価格を持つようにしようと決めて定義しています。また、ISBNは本のIDであるので、PUTでも書き換えられないようにしようといった設計も記載しています。さらに議論を深めるとすると、/booksget時にフィルタ条件(例えば、タイトルの部分一致や税抜き価格の範囲指定)を持たせたいよねといったことや、一覧取得だけどデータが1万とかあった場合どうする?といったことがすぐに考え付きます。このAPIを見ているだけで様々な仕様を決めていくことができますし、同時に機能仕様書が完成していきます。

API Gatewayへのインポート

AWSマネージメントコンソールを使って、API GatewayでAPIを構築していきます。

APIAPIを作成を押下します。

HTTP APIを選択し、インポートを押下します。

OpenAPI3定義ファイルを選択で先ほど作成したYAMLファイルをアップロードし、APIを作成を押下します。

以上でAPIのインポートが完了しました。

開発 > ルートで確認すると、/booksにPOSTとGET、/books/{isbn}にGET、DELETE、PUTのメソッドがYAML定義通り作成されていることがわかります。

Lambdaによるロジックの実装

AWSマネージコンソールでAWS Lamdaを開き、関数 > 関数の作成を選択します。

一から作成を選択し、関数名に任意の値を設定します。また、ランタイムは各自が実装したいものを選択すればよいですが、今回はPython 3.8を選択します。実行ロール基本的なLambdaアクセス権限で新しいロールを作成を選び、関数の作成を押下します。

デフォルトの実装として、APIにステータス200、レスポンスに"Hello from Lambda!"を返却するものができるので、他はいじらず完了します。

API Gatewayへの統合

引き続きAPI GatewayからLambdaを起動する設定を行います。
AWSマネージメントコンソールで、先ほど作成したAPIを開き、開発 > 統合を選択します。Lambdaの統合を設定するAPIを選択します。今回はまず/booksのPOSTを選択しています。その状態で統合を作成してアタッチを押下します。

統合を作成する画面にて、統合ターゲットLambda関数を選択し、先ほど選択したAPIを選び作成を押下します。

すべてのメソッドを順次選択し、先ほど作ったものと同Lambda統合をアタッチしていきます。LambdaはAPI毎に作成することも可能ですが、ある程度まとめて一つのLambdaとするのがハンドリングがしやすいと思います。Lambda関数側で、呼び出されたときのURLのパスやメソッドの種類がわかりますので、どのAPIに対する処理をするべきか判断しロジックを切り替えられます。

最後に作成したAPIをホストして公開します。APIのデプロイ > ステージを選択し、作成を押下します。

ステージの作成画面で名前に任意の値を入力し作成を押下します。

先ほど入力した名前がAPIのルートパスになりステージが作成されます。引き続き、作成したステージにAPIをデプロイするため、デプロイボタンを押下します。

作成したステージを選択し、ステージへデプロイを押下します。これでAPIが公開されます。

以上でAPIの公開開始です。先ほどの画面で表示されたURLをブラウザで開くと、/booksへのGET呼び出しとなり、"Hello from Lambda!"が画面に表示されます。

まとめ

第二のファクターとして、APIファーストによる開発の考えと、AWS API GatewayとLamdaを使った実装方法の話を進めました。今後のアプリケーション開発において、API Gateway+Lamdaでの開発は相当力を発揮すると思いますし、APIファーストを実現するのがとても楽なのでぜひ使ってみてください。

次回は「((3)依存関係管理 / 依存関係 - 依存関係を明示的に宣言し分離する」です。
本シリーズの目次
全シリーズの目次