CI/CDツールとS3を活用してREST APIやMQTTの通信仕様をステークホルダーと共有する

はじめに

このブログを公開した数日後にAWSからサーバーレス開発者ポータル提供開始の案内がありました。 ケースバイケースですが、APIのドキュメント公開についてはサーバーレス開発者ポータルの利用も有力な選択肢になりそうです。

API Gateway のポータルサイトを簡単に構築できるサーバーレスアプリを試す

サーバーレス開発部@大阪の岩田です。 サーバーレス開発部の良くある案件パターンとして、Web APIのバックエンド開発やIoTを絡めた案件が挙げられます。 こういった案件では通信仕様の設計からクラメソで対応することが多いのですが、プロジェクトを円滑に進めるためには単に通信仕様を設計するだけでなく、呼び出し側のお客様や他社ベンダーとの認識合わせや仕様の調整が重要になってきます。

現在関わっている案件では、GitリポジトリへのPushをトリガーにS3にドキュメント類をアップロードし、S3の静的Webサイトホスティング機能でドキュメントを自動公開することで開発がスムーズに進むように心がけています。 ドキュメントのイメージはこんな感じです。

本ブログでは、CircleCIを使ってドキュメントをS3の静的Webサイトホスティングで自動公開する手順についてご紹介します。

プロジェクトの概要

簡単にプロジェクトの概要を紹介します。

  • ソース管理はGitHubを使用
  • CI/CDにはCircleCIを活用
  • REST APIの設計にSwaggerを利用
  • MQTTのトピック設計にAsyncAPIを利用
  • シーケンス図等のUML作成にPlantUMLを利用

Gitリポジトリにはソースコードと合わせてdocsというディレクトリを配置しており、ドキュメント類はこのディレクトリ配下に作成しています。 docsディレクトリの構造は下記の通りです。

docs/
├── api
│   ├── dist
│   ├── swagger.yml
│   └── yaml_to_json.py
├── mqtt
│   ├── asyncapi.yml
│   └── dist
└── uml
    ├── create_html.sh
    ├── operationflow
    │   └── 01.xxxxx.puml
    ├── sequence
    │   ├── 00.xxxxx.puml
    └── usecase
        └── 01.xxxxx.puml

利用しているツールの紹介

先ほどのプロジェクト概要と順番が前後しますが、ドキュメント類の作成に使用しているツールをご紹介します。 これらのツールからHTMLファイルを出力し、S3でWebサイトとしてホスティングするのが目標になります。

Swagger

REST APIを記述するための仕様です。周辺ツールまでひっくるめて一括りに「Swagger」ということが多いです。 非常に有名なツールなので、ご存知の方も多いかと思います。

Swaggerの周辺ツールでドキュメントをWeb上で公開するためのSwagger UIというツールがあるので、これを使ってドキュメントを公開します。

AsyncAPI

平たく言うとMQTT版のSwaggerです。 詳細は新井がブログにまとめてくれるそうなので割愛します。 npmにasyncapi-docgenというパッケージがあり、このパッケージを利用する事でドキュメントをHTML形式で出力する事ができます。

PlantUML

UML等の図を作成できるツールです。私はVS Codeのプラグインを利用することが多いです。 http://plantuml.com/

作成した図はPNG等の形式で画像として出力できます。

手順

実際に環境構築の手順を見ていきます。

S3バケットの作成

まずは静的ウェブサイトホスティング用のS3バケットを作成します。 下記のCFnテンプレートから作成します。

AWSTemplateFormatVersion: 2010-09-09
Description: Create Document Hosting Env
Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      WebsiteConfiguration:
        IndexDocument: index.html
  S3BuketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement:
          -
            Action:
            - "s3:GetObject"
            Effect: "Allow"
            Resource:
              Fn::Join:
                - ""
                -
                  - "arn:aws:s3:::"
                  -
                    Ref: "S3Bucket"
                  - "/docs/*"
            Principal: "*"
            Condition:
              IpAddress:
                aws:SourceIp:
                  - "xxx.xxx.xxx.xxx/32"  #アクセスを許可したいアクセス元GIP
Outputs:
  S3BucketName:
    Value: !Ref S3Bucket
  S3BucketArn:
    Value: !GetAtt S3Bucket.Arn

なお、注意点としてS3へのアクセスはHTTPSを強制することができません。 IP制限をかけているとは言え、HTTPでアクセスされると間の経路で盗聴されるリスクが残るので、きちんと構築するならCloudFrontとLambda@EdgeもしくはAWS WAFを組み合わせてHTTPSを強制しつつIP制限をかけるべきです。 このあたりの詳細はこのブログの趣旨から外れるので、今回はS3のみで話を進めます。

CI/CDの設定

実際にCircleCIを使ってジョブを作成していきます。 なお、本ブログで紹介するのはCircelCIですが、手順自体はその他のCI/CDツールにも展開可能です。

まずは設定ファイル全体を貼っておきます。 基本的な思想として、各ツールごとのジョブでドキュメントを作成し、最後に各ジョブの成果物をs3 syncでS3バケットに同期します。

version: 2
jobs:
  create_mqtt_docs:
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run:
          name: create mqtt docs
          command: |
            npm install asyncapi-docgen
            ./node_modules/.bin/adg docs/mqtt/asyncapi.yml -o docs/mqtt/dist/
      - persist_to_workspace:
          root: ./
          paths:
            - docs/mqtt/dist
  create_plant_uml_docs:
    docker:
      - image: circleci/java:8
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run:
          name: create plant uml docs
          command: |
            set -x
            mkdir -p .plant_uml
            sudo apt-get update
            sudo apt-get install graphviz fonts-ipafont
            wget http://sourceforge.net/projects/plantuml/files/plantuml.1.2018.11.jar/download -O ./plantuml.jar
            java -jar ./plantuml.jar -Dfile.encoding=UTF-8 -DPNG=png docs/uml/**/*.puml -o dist
            sh docs/uml/create_html.sh ${S3_WEB_HOSTING_URL} >> docs/uml/index.html
      - persist_to_workspace:
          root: ./
          paths:
            - docs/uml            
  swagger_yaml_to_json:
    docker:
      - image: circleci/python:3.6
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run:
          name: Convert swagger YAML file to JSON
          command: |
            pip install pyyaml --user
            python docs/api/yaml_to_json.py docs/api/swagger.yml
      - persist_to_workspace:
          root: ./
          paths:
            - docs/api
  create_swagger_docs:
    docker:
      - image: swaggerapi/swagger-ui
    steps:
      - checkout
      - run:
          name: install ca-certificates for circleci
          command: |            
            set -x
            apk add --no-cache ca-certificates
      - attach_workspace:
          at: ./
      - run:
          name: create_swagger_doc
          command: |            
            set -x
            sed -i "s|https://petstore.swagger.io/v2/swagger.json|${S3_WEB_HOSTING_URL}docs/api/swagger.json|g" /usr/share/nginx/html/index.html
            cp /usr/share/nginx/html/* docs/api/dist
            cp docs/api/swagger.json docs/api/dist
      - persist_to_workspace:
          root: ./
          paths:
            - docs/api/dist
  deploy_docs:
    docker:
      - image: circleci/python:3.6
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run:
          name: deploy_docs
          command: |
            set -x
            pip install awscli --user
            export PATH=$PATH:/home/circleci/.local/bin/
            aws s3 sync docs/mqtt/dist s3://${S3_BUCKET_FOR_WEB_HOSTING}/docs/mqtt
            aws s3 sync docs/api/dist s3://${S3_BUCKET_FOR_WEB_HOSTING}/docs/api
            aws s3 cp docs/uml/index.html s3://${S3_BUCKET_FOR_WEB_HOSTING}/docs/uml/index.html
            for dir in `find  docs/uml -type d | grep dist`
            do
              aws s3 sync $dir s3://${S3_BUCKET_FOR_WEB_HOSTING}/${dir}
            done
workflows:
  version: 2
  deploy_workflow:
    jobs:
      - create_plant_uml_docs      
      - create_mqtt_docs
      - swagger_yaml_to_json
      - create_swagger_docs:
          requires:
            - swagger_yaml_to_json
      - deploy_docs:
          requires:
            - create_mqtt_docs
            - create_swagger_docs
            - create_plant_uml_docs

実際には依存ライブラリをキャッシュさせたり、YAMLのアンカーを使って記述を共通化したりしていますが、分かりやすさ重視で少し改変しています。

ジョブの実行に必要になるため事前にCircleCIの設定で下記の環境変数を設定しておいて下さい

  • AWS_ACCESS_KEY_ID → AWSアクセスキー
  • AWS_SECRET_ACCESS_KEY → AWS シークレットキー
  • S3_BUCKET_FOR_WEB_HOSTING → ドキュメントを保存するためのS3バケット名
  • S3_WEB_HOSTING_URL → S3の静的Webサイトホスティング機能で公開するURL(https://s3-リージョン.amazonaws.com/S3バケット名>)

ここから個々のジョブの詳細について説明していきます。

Swagger用のジョブ

まずSwaggerのドキュメントをSwagger UIで公開するためのジョブです。 Swagger UIでドキュメントを公開するにはJSON形式の定義ファイルが必要になるため、まず1つ目のジョブでYAMLからJSONに変換します。 ※あまりやらないと思いますが、APIの定義をゴリゴリJSONで書いている場合はこのステップは不要です。

  swagger_yaml_to_json:
    docker:
      - image: circleci/python:3.6
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run:
          name: Convert swagger YAML file to JSON
          command: |
            pip install pyyaml --user
            python docs/api/yaml_to_json.py docs/api/swagger.yml
      - persist_to_workspace:
          root: ./
          paths:
            - docs/api

YAMLをJSONに変換する処理の本体はこちらです。

import decimal
import json
import os
import sys
import yaml


class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            if o % 1 > 0:
                return float(o)
            else:
                return int(o)
        return super(DecimalEncoder, self).default(o)


def main():

    args = sys.argv
    if len(args) != 2:
        print('please specify swagger def file')
        quit()

    with open(args[1]) as file:
        swagger_def = yaml.load(file.read())

    remove_invalid_key(swagger_def.get('paths', {}))

    json_path = os.path.join(os.path.dirname(__file__), "swagger.json")
    with open(json_path, 'w') as file:
        file.write(json.dumps(swagger_def, cls=DecimalEncoder))


def remove_invalid_key(definition: dict):
    # keyにAmazon独自仕様のkeyがあれば削除
    del_keys = list()
    for key in definition:
        if key.startswith('x-amazon'):
            del_keys.append(key)

    for key in del_keys:
        del definition[key]

    # 子供に辞書オブジェクトがあれば再帰呼び出し
    for key in definition.keys():
        if isinstance(definition[key], dict):
            remove_invalid_key(definition[key])


if __name__ == "__main__":
    main()

JSONへの変換ついでに、AWS独自仕様のx-amazonから始まる定義を削除しています。 よく考えたらNode.jsで実装しておけば次のジョブとまとめることができたのですが、このまま進めます。

2つ目のジョブ定義です。

  create_swagger_docs:
    docker:
      - image: swaggerapi/swagger-ui
    steps:
      - checkout
      - run:
          name: install ca-certificates for circleci
          command: |            
            set -x
            apk add --no-cache ca-certificates
      - attach_workspace:
          at: ./
      - run:
          name: create_swagger_doc
          command: |            
            set -x
            sed -i "s|https://petstore.swagger.io/v2/swagger.json|${S3_WEB_HOSTING_URL}docs/api/swagger.json|g" /usr/share/nginx/html/index.html
            cp /usr/share/nginx/html/* docs/api/dist
            cp docs/api/swagger.json docs/api/dist
      - persist_to_workspace:
          root: ./
          paths:
            - docs/api/dist

swaggerapi/swagger-uiのDockerコンテナ内/usr/share/nginx/html/に静的サイトを公開するために必要なHTMLやJSが置かれているので、それらのファイルをpersist_to_workspaceで指定して後続のドキュメントデプロイ用ジョブに引き渡します。 Swagger UIのバージョンアップに追随しないのであれば、いちいちdockerでジョブを動かさずにHTML等のファイルをGitリポジトリにコミットしておいても良いと思います。

ポイントは

sed -i "s|https://petstore.swagger.io/v2/swagger.json|${SWAGGER_JSON_URL}|g" /usr/share/nginx/html/index.html

の部分で、Swagger UIに読み込ませる定義ファイルとして先ほど作成したJSON形式のSwagger定義ファイルを指定しています。

AsyncAPI用のジョブ

次にAsyncAPIのドキュメントを公開するためのファイルを抽出します。

  create_mqtt_docs:
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run:
          name: create mqtt doc
          command: |
            npm install asyncapi-docgen
            ./node_modules/.bin/adg docs/mqtt/asyncapi.yml -o docs/mqtt/dist/
      - persist_to_workspace:
          root: ./
          paths:
            - docs/mqtt/dist

npm installasyncapi-docgenを導入し

./node_modules/.bin/adg <asyncapiの定義ファイル> -o <出力先>

でHTMLやCSSを出力しています。

PlantUML用のジョブ

最後にPlantUML用のジョブです。

  create_plant_uml_docs:
    docker:
      - image: circleci/java:8
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run:
          name: create plant uml docs
          command: |
            set -x
            mkdir -p .plant_uml
            sudo apt-get update
            sudo apt-get install graphviz fonts-ipafont
            wget http://sourceforge.net/projects/plantuml/files/plantuml.1.2018.11.jar/download -O ./plantuml.jar
            java -jar ./plantuml.jar -Dfile.encoding=UTF-8 -DPNG=png docs/uml/**/*.puml -o dist
            sh docs/uml/create_html.sh ${S3_WEB_HOSTING_URL} >> docs/uml/index.html
      - persist_to_workspace:
          root: ./
          paths:
            - docs/uml

必要なライブラリやフォントを導入した後

java -jar ./plantuml.jar -Dfile.encoding=UTF-8 -DPNG=png docs/uml/**/*.puml -o dist

でUMLの画像を生成し、

sh docs/uml/create_html.sh ${S3_WEB_HOSTING_URL} >> docs/uml/index.html

の部分で画像を表示するHTMLファイルを生成しています。 実行しているシェルスクリプトは下記のような内容です。

#!/bin/bash

cat <<EOT
<!DOCTYPE html><head><meta charset="utf-8"></head><body>
EOT

for file in `find  docs/uml -type f -name "*.png"`
do
cat <<EOT
<p><img src="$1$file"/></p>
EOT

done

cat <<EOT
</body></html>
EOT

ひたすらimgタグが並ぶだけのデザインセンス0のHTMLが出力されます。

<html><head><meta charset="utf-8"></head><body>
<p><img src="https://s3-ap-northeast-1.amazonaws.com/xxxxx/docs/uml/usecase/dist/01.usecase.png"></p>
</body></html>

ドキュメントデプロイ用のジョブ

最後にこれまでのジョブで生成されたファイルをS3に流してドキュメントを公開します。

  deploy_docs:
    docker:
      - image: circleci/python:3.6
    steps:
      - checkout
      - attach_workspace:
          at: ./
      - run:
          name: deploy_docs
          command: |
            set -x
            pip install awscli --user
            export PATH=$PATH:/home/circleci/.local/bin/
            aws s3 sync docs/mqtt/dist s3://${S3_BUCKET_FOR_WEB_HOSTING}/docs/mqtt
            aws s3 sync docs/api/dist s3://${S3_BUCKET_FOR_WEB_HOSTING}/docs/api
            aws s3 cp docs/uml/index.html s3://${S3_BUCKET_FOR_WEB_HOSTING}/docs/uml/index.html
            for dir in `find  docs/uml -type d | grep dist`
            do
              aws s3 sync $dir s3://${S3_BUCKET_FOR_WEB_HOSTING}/${dir}
            done

ドキュメントを更新してプッシュしてみる

準備ができたので、適当にドキュメントを更新してGitHubにプッシュしてみます。 すると・・・

WorkFlowが動き出しました! 全てのジョブ完了後にS3でホスティングしている静的Webサイトにアクセスすると、冒頭で紹介したような画像を確認することができます!! これでステークホルダーとの認識合わせもスムーズに行えそうです。

まとめ

CI/CDツールとS3を活用したドキュメントの共有方法をご紹介しました。 最初の環境構築には少し時間がかかりますが、一度環境が構築できてしまえば便利に使うことができます。 こういった手法も活用しながら、効率良く開発を進めていきたいですね。

誰かのお役に立てば幸いです。