ファイルが大きくなりがちなSwaggerファイルを分割して管理する方法をまとめます。
また、分割して管理しつつもAWS CodePipelineで自動デプロイがうまく動作するよう設定していきます。
はじめに
なぜSwaggerファイルの分割が必要か
1つのSwaggerファイルでプロジェクトを管理していると様々な課題に直面してきます。
- 機能が多くなるとコード量が膨らみメンテナンスが難しくなる
- 複数人で同じファイルを編集すると競合が発生しやすい
- 競合が発生するとマージ作業が面倒くさい
こういった理由から、Swaggerファイルを分割して管理することで、複数人でも並行して作業が可能になり、効率がよくなるケースがしばしばあります。ただ、一人または少人数開発の場合は、必ずしも分割する必要はないのでケースバイケースになります。
どのように実現するか
やることは2つ。
- Swaggerファイルを分割して作成
- 分割して作成されたSwaggerファイルのパーツを結合
参考にした記事をお手本に json-refs
と gulp
を組み合わせてファイルの結合を実現しています。
Swaggerファイル分割
ファイル構成
.
├── app
│ └── hello_world
│ └── index.py # Lambda関数のプログラムソース
├── swagger
│ ├── components
│ │ └── response_404_not_found.yaml # 共通レスポンスとして分割したYAMLファイル
│ ├── paths
│ │ ├── hello_world.yaml # APIのエンドポイント毎に分割したYAMLファイル
│ │ └── mock
│ │ └── hello_world.yaml # APIのエンドポイント毎に分割したYAMLファイル(MOCK用)
│ ├── _components.yaml # componentsフォルダ配下のファイル管理用のYAMLファイル
│ ├── _paths.yaml # pathsフォルダ配下のファイル管理用のYAMLファイル
│ └── base.yaml # 読み込み開始の起点となるYAMLファイル
├── gulpfile.js # gulpで実行したいJavaScriptのプログラムソース
├── package.json # 利用するモジュール一覧
├── template.yaml # SAMテンプレート
├── buildspec.yml # CodeBuild用のYAMLファイル
├── swagger.yaml # swaggerフォルダ配下のファイルを結合したSwaggerファイル(Git による追跡から除外する)
└── .gitignore # Git による追跡から除外するファイルを記載
今回は簡単なAWS SAMでのWebアプリケーションの実装を想定しています。
プロジェクトの始め方は以下の記事をご参考ください。
次に、各ファイルの中身について記載していきます。
gulp関連ファイル
gulpfile.js
分割して配置したファイルを結合するJavaScriptのプログラムソース
今回の主役。このプログラムを動かして分割したファイルを結合します。
9-11行目でInput/Outputのパスの設定を行っています。
'use strict';
const gulp = require('gulp');
const rename = require('gulp-rename');
const through2 = require('through2');
const yaml = require('js-yaml');
// パス設定
const inputFileName = './swagger/base.yaml';
const outputFileName = 'swagger.yaml';
const outputPath = './';
gulp.task('compile', cb => {
return gulp
.src(`${inputFileName}`)
.pipe(through2.obj((file, enc, cb) => {
if (!file.isBuffer()) throw new Error(`[FAILED]. '${inputFileName}' can not load target file.`);
const root = yaml.safeLoad(file.contents);
const resolve = require('json-refs').resolveRefs;
const options = {
filter : ['relative', 'remote'],
loaderOptions : {
processContent : (res, callback) => {
callback(null, yaml.safeLoad(res.text));
}
}
};
resolve(root, options).then((results) => {
file.contents = Buffer.from(yaml.safeDump(results.resolved));
delete require.cache[require.resolve('json-refs')];
cb(null, file);
}).catch((e) => {
throw new Error(e);
});
})
)
.pipe(rename(outputFileName))
.pipe(gulp.dest(outputPath));
});
gulp.task(
'default',
gulp.series(
'compile'
)
);
package.json
利用するモジュール一覧
{
"name": "swagger",
"version": "1.0.0",
"description": "swagger file join",
"main": "resolve.js",
"scripts": {
"start": "gulp",
"build": "gulp build",
"start2": "node resolve.js ./src/index.yaml -o yaml > ./test.swagger.yaml",
"test": "node resolve.js index.yaml > test/resolved.json && node resolve.js -o yaml index.yaml"
},
"license": "MIT",
"dependencies": {
"commander": "^2.19.0",
"gulp": "^4.0.2",
"gulp-rename": "^2.0.0",
"js-yaml": "^3.12.2",
"json-refs": "^3.0.12",
"multi-file-swagger": "^2.3.0",
"through2": "^4.0.2"
}
}
swagger分割ファイル
swagger/base.yaml
読み込み開始の起点となるYAMLファイル
openapi: "3.0.3"
info:
title: "Sample API"
description: Sample API
version: "1.0.0"
paths:
$ref: ./swagger/_paths.yaml
components:
$ref: ./swagger/_components.yaml
swagger/_components.yaml
componentsフォルダ配下のファイル管理用のYAMLファイル
schemas:
ResponseNotFound:
$ref: ./components/response_404_not_found.yaml
swagger/_paths.yaml
pathsフォルダ配下のファイル管理用のYAMLファイル
/hello:
$ref: ./paths/hello_world.yaml
/mock/hello:
$ref: ./paths/mock/hello_world.yaml
swagger/components/response_404_not_found.yaml
共通レスポンスとして分割したYAMLファイル
description: "404 Not found | 指定リソースがない時のレスポンス"
type: object
properties:
errors:
type: array
items:
type: object
properties:
message:
type: string
example: "コンテンツが存在しません"
swagger/paths/hello_world.yaml
APIのエンドポイント毎に分割したYAMLファイル
今回は、サンプルとしてHello World API
のGETメソッドの定義を記載しています。
APIの呼び出しに対し、バックではLambda関数が動作し、処理結果を返す流れになります。
get:
summary: "Hello World API."
description: "sample api"
responses:
200:
description: "Lambda実行が成功した時"
content:
application/json:
schema:
type: object
properties:
message:
description: "メッセージ"
type: string
example:
- message: "Hello World by Mock."
404:
description: "指定リソースは存在しません"
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseNotFound'
x-amazon-apigateway-integration:
httpMethod: "POST"
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorld.Arn}/invocations
responses:
default:
statusCode: "200"
passthroughBehavior: when_no_match
type: "aws_proxy"
swagger/paths/mock/hello_world.yaml
APIのエンドポイント毎に分割したYAMLファイル(MOCK用)
ウェブアプリケーション開発の場合、フロントエンドとバックエンドの開発が同時で進むことがあります。
その時は、バックエンドの処理結果ではなくMOCK機能を利用してテストデータを返却することでみんながハッピーになります。
(バックエンド開発チームにとってもフロントエンド開発チームからのプレッシャーから解放されるはず)
get:
summary: "Hello World API Mock."
description: "sample api mock"
responses:
200:
description: OK
x-amazon-apigateway-integration:
type: "mock"
requestTemplates:
application/json: |
{
"statusCode": 200
}
responses:
default:
statusCode: "200"
responseTemplates:
application/json: |
{
message: "mock message."
}
その他ファイル
app/hello_world/index.py
Lambda関数のプログラムソース
HelloWorldのサンプルプログラムを使用しています。
import json
import datetime
def handler(event, context):
data = {
'output': 'Hello World!!',
'timestamp': datetime.datetime.utcnow().isoformat()
}
return {'statusCode': 200,
'body': json.dumps(data),
'headers': {'Content-Type': 'application/json'}}
template.yaml
SAMテンプレート
API Gateway、Lambda関数、IAMロールをそれぞれ定義しています。
AWSTemplateFormatVersion: 2010-09-09
Transform:
- AWS::Serverless-2016-10-31
- AWS::CodeStar
Parameters:
ProjectId:
Type: String
Description: CodeStar projectId used to associate new resources to team members
CodeDeployRole:
Type: String
Description: IAM role to allow AWS CodeDeploy to manage deployment of AWS Lambda functions
Stage:
Type: String
Description: The name for a project pipeline stage, such as Staging or Prod, for which resources are provisioned and deployed.
Default: 'Devl'
Globals:
Function:
Runtime: python3.9
Timeout: 5
Resources:
ApiGatewayRestApi:
Type: AWS::Serverless::Api
Properties:
Name: !Sub 'awscodestar-${ProjectId}-api'
StageName: !Ref Stage
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: swagger.yaml
HelloWorld:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub 'awscodestar-${ProjectId}-lambda-HelloWorld'
CodeUri: app/hello_world/
Handler: index.handler
Role:
Fn::GetAtt:
- LambdaExecutionRole
- Arn
Events:
GetEvent:
Type: Api
Properties:
Path: /hello
Method: get
RestApiId: !Ref ApiGatewayRestApi
LambdaExecutionRole:
Description: Creating service role in IAM for AWS Lambda
Type: AWS::IAM::Role
Properties:
RoleName: !Sub 'CodeStar-${ProjectId}-Execution${Stage}'
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [lambda.amazonaws.com]
Action: sts:AssumeRole
Path: /
ManagedPolicyArns:
- !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
buildspec.yml
CodeBuild用のYAMLファイル
CodeBuildで使用するビルド環境に yarn と必要なモジュールをインストールし、分割したswaggerファイルを一つのswaggerファイルに結合します。
version: 0.2
phases:
install:
runtime-versions:
python: 3.9
commands:
- npm install -g yarn
- yarn install
pre_build:
commands:
- yarn start
build:
commands:
# Use AWS SAM to package the application by using AWS CloudFormation
- aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.yml
# Do not remove this statement. This command is required for AWS CodeStar projects.
# Update the AWS Partition, AWS Region, account ID and project ID in the project ARN on template-configuration.json file so AWS CloudFormation can tag project resources.
- sed -i.bak 's/\$PARTITION\$/'${PARTITION}'/g;s/\$AWS_REGION\$/'${AWS_REGION}'/g;s/\$ACCOUNT_ID\$/'${ACCOUNT_ID}'/g;s/\$PROJECT_ID\$/'${PROJECT_ID}'/g' template-configuration.json
artifacts:
files:
- template-export.yml
- template-configuration.json
.gitignore
Git による追跡から除外するファイルを記載
sswaggerファイルのGitへアップロードは競合する可能性が高いので Git による追跡から除外しています。 buildspec.yml により、ビルド環境で結合する設定になっているため、swaggerフォルダ配下の部品だけアップロードしたらOKです。
node_modules, yarn.lockはプロジェクト自体には必要ないので除外に設定しています。
swagger.yaml
node_modules
yarn.lock
Swaggerファイル結合
準備
以下のコマンドを実行して、必要なモジュールをインストールします。
# yarn インストール (既にインストール済みの場合は不要)
$ npm install -g yarn
# 必要モジュールのインストール
$ yarn install
初回の一度だけ実行したらよいです。
実行
yarn start
コマンドを実行すると gulpfile.js
内のプログラムが起動します。
$ yarn start
yarn run v1.22.17
$ gulp
[06:34:17] Using gulpfile ~/environment/nullpo-project/gulpfile.js
[06:34:17] Starting 'default'...
[06:34:17] Starting 'compile'...
[06:34:17] Finished 'compile' after 215 ms
[06:34:17] Finished 'default' after 219 ms
Done in 0.72s.
このように表示されたら処理成功です。
結合結果
swaggerフォルダ配下のファイルが結合されて swagger.yaml
が出力されました。
既に同じ名前のファイルが存在する場合は上書きされます。
openapi: 3.0.3
info:
title: SAMPLE API
Description: SAMPLE API
version: 1.0.0
paths:
/hello:
get:
summary: Hello World API.
description: sample api
responses:
'200':
description: Lambda実行が成功した時
content:
application/json:
schema:
type: object
properties:
message:
description: メッセージ
type: string
example:
- message: Hello World by Mock.
'404':
description: 指定リソースは存在しません
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseNotFound'
x-amazon-apigateway-integration:
httpMethod: POST
uri:
'Fn::Sub': >-
arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorld.Arn}/invocations
responses:
default:
statusCode: '200'
passthroughBehavior: when_no_match
type: aws_proxy
/mock/hello:
get:
summary: Hello World API Mock.
description: sample api mock
responses:
'200':
description: OK
x-amazon-apigateway-integration:
type: mock
requestTemplates:
application/json: |
{
"statusCode": 200
}
responses:
default:
statusCode: '200'
responseTemplates:
application/json: |
{
message: "mock message."
}
components:
schemas:
ResponseNotFound:
description: 404 Not found | 指定リソースがない時のレスポンス
type: object
properties:
errors:
type: array
items:
type: object
properties:
message:
type: string
example: コンテンツが存在しません
CodeCommit(Git) へ登録
今回のファイル一式をCodeCommit(Git)へPushし、CodePipelineで自動デプロイを行います。
その際に swagger.yaml
はビルド環境で結合されます。
.gitignore
ファイルにて、ローカルで生成される swagger.yaml
は追跡対象外にしているので基本的には競合は発生しづらいはずです。それでも競合が発生した場合は手作業で修正してください。
まとめ
Swaggerファイルを分割することでメンテナンスがしやすくなりました。
複数人で開発するプロジェクトでもコンフリクトの恐怖に怯えることはなくなりそうです。
AWS SAMを利用していて、Swaggerファイルのメンテナンスに困っている方の助けになれば幸いです。