
OpenAPI ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Swagger UI で視覚化する構成を AWS CDK で実装してみた
こんにちは、製造ビジネステクノロジー部の若槻です。
OpenAPI Specification(OAS)に基づいた Yaml や Json 形式のドキュメントを HTML 形式で視覚化する UI として広く使われているのが Swagger UI です。
さて OAS ドキュメントを Swagger UI で簡単に表示する方法として Swagger Viewer のような VS Code 拡張機能を使う方法がありますが、プロジェクトの内外のメンバーにさらに簡単に API 仕様を共有するために Web 上で Swagger UI で公開したいニーズも多いのではないでしょうか。
今回は、OAS ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Swagger UI で視覚化する構成を AWS CDK で実装してみました。
やってみた
サンプルの OAS ドキュメントを配置
下記で公開されているサンプルの OAS ドキュメント(ペットストア)を、docs/api/petstore.yaml
に配置します。
Petstore OpenAPI ドキュメント
openapi: 3.0.0
servers:
- url: "http://petstore.swagger.io/v2"
info:
description: >-
This is a sample server Petstore server. For this sample, you can use the api key
`special-key` to test the authorization filters.
version: 1.0.0
title: OpenAPI Petstore
license:
name: Apache-2.0
url: "https://www.apache.org/licenses/LICENSE-2.0.html"
tags:
- name: pet
description: Everything about your Pets
- name: store
description: Access to Petstore orders
- name: user
description: Operations about user
paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: ""
operationId: addPet
responses:
"200":
description: successful operation
content:
application/xml:
schema:
$ref: "#/components/schemas/Pet"
application/json:
schema:
$ref: "#/components/schemas/Pet"
"405":
description: Invalid input
security:
- petstore_auth:
- "write:pets"
- "read:pets"
requestBody:
$ref: "#/components/requestBodies/Pet"
put:
tags:
- pet
summary: Update an existing pet
description: ""
operationId: updatePet
externalDocs:
url: "http://petstore.swagger.io/v2/doc/updatePet"
description: "API documentation for the updatePet operation"
responses:
"200":
description: successful operation
content:
application/xml:
schema:
$ref: "#/components/schemas/Pet"
application/json:
schema:
$ref: "#/components/schemas/Pet"
"400":
description: Invalid ID supplied
"404":
description: Pet not found
"405":
description: Validation exception
security:
- petstore_auth:
- "write:pets"
- "read:pets"
requestBody:
$ref: "#/components/requestBodies/Pet"
/pet/findByStatus:
get:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: true
style: form
explode: false
deprecated: true
schema:
type: array
items:
type: string
enum:
- available
- pending
- sold
default: available
responses:
"200":
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
"400":
description: Invalid status value
security:
- petstore_auth:
- "read:pets"
/pet/findByTags:
get:
tags:
- pet
summary: Finds Pets by tags
description: >-
Multiple tags can be provided with comma separated strings. Use tag1,
tag2, tag3 for testing.
operationId: findPetsByTags
parameters:
- name: tags
in: query
description: Tags to filter by
required: true
style: form
explode: false
schema:
type: array
items:
type: string
responses:
"200":
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
"400":
description: Invalid tag value
security:
- petstore_auth:
- "read:pets"
deprecated: true
"/pet/{petId}":
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
responses:
"200":
description: successful operation
content:
application/xml:
schema:
$ref: "#/components/schemas/Pet"
application/json:
schema:
$ref: "#/components/schemas/Pet"
"400":
description: Invalid ID supplied
"404":
description: Pet not found
security:
- api_key: []
post:
tags:
- pet
summary: Updates a pet in the store with form data
description: ""
operationId: updatePetWithForm
parameters:
- name: petId
in: path
description: ID of pet that needs to be updated
required: true
schema:
type: integer
format: int64
responses:
"405":
description: Invalid input
security:
- petstore_auth:
- "write:pets"
- "read:pets"
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
name:
description: Updated name of the pet
type: string
status:
description: Updated status of the pet
type: string
delete:
tags:
- pet
summary: Deletes a pet
description: ""
operationId: deletePet
parameters:
- name: api_key
in: header
required: false
schema:
type: string
- name: petId
in: path
description: Pet id to delete
required: true
schema:
type: integer
format: int64
responses:
"400":
description: Invalid pet value
security:
- petstore_auth:
- "write:pets"
- "read:pets"
"/pet/{petId}/uploadImage":
post:
tags:
- pet
summary: uploads an image
description: ""
operationId: uploadFile
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
schema:
type: integer
format: int64
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/ApiResponse"
security:
- petstore_auth:
- "write:pets"
- "read:pets"
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
additionalMetadata:
description: Additional data to pass to server
type: string
file:
description: file to upload
type: string
format: binary
/store/inventory:
get:
tags:
- store
summary: Returns pet inventories by status
description: Returns a map of status codes to quantities
operationId: getInventory
responses:
"200":
description: successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
format: int32
security:
- api_key: []
/store/order:
post:
tags:
- store
summary: Place an order for a pet
description: ""
operationId: placeOrder
responses:
"200":
description: successful operation
content:
application/xml:
schema:
$ref: "#/components/schemas/Order"
application/json:
schema:
$ref: "#/components/schemas/Order"
"400":
description: Invalid Order
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
description: order placed for purchasing the pet
required: true
"/store/order/{orderId}":
get:
tags:
- store
summary: Find purchase order by ID
description: >-
For valid response try integer IDs with value <= 5 or > 10. Other values
will generate exceptions
operationId: getOrderById
parameters:
- name: orderId
in: path
description: ID of pet that needs to be fetched
required: true
schema:
type: integer
format: int64
minimum: 1
maximum: 5
responses:
"200":
description: successful operation
content:
application/xml:
schema:
$ref: "#/components/schemas/Order"
application/json:
schema:
$ref: "#/components/schemas/Order"
"400":
description: Invalid ID supplied
"404":
description: Order not found
delete:
tags:
- store
summary: Delete purchase order by ID
description: >-
For valid response try integer IDs with value < 1000. Anything above
1000 or nonintegers will generate API errors
operationId: deleteOrder
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
schema:
type: string
responses:
"400":
description: Invalid ID supplied
"404":
description: Order not found
/user:
post:
tags:
- user
summary: Create user
description: This can only be done by the logged in user.
operationId: createUser
responses:
default:
description: successful operation
security:
- api_key: []
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/User"
description: Created user object
required: true
/user/createWithArray:
post:
tags:
- user
summary: Creates list of users with given input array
description: ""
operationId: createUsersWithArrayInput
responses:
default:
description: successful operation
security:
- api_key: []
requestBody:
$ref: "#/components/requestBodies/UserArray"
/user/createWithList:
post:
tags:
- user
summary: Creates list of users with given input array
description: ""
operationId: createUsersWithListInput
responses:
default:
description: successful operation
security:
- api_key: []
requestBody:
$ref: "#/components/requestBodies/UserArray"
/user/login:
get:
tags:
- user
summary: Logs user into the system
description: ""
operationId: loginUser
parameters:
- name: username
in: query
description: The user name for login
required: true
schema:
type: string
pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
- name: password
in: query
description: The password for login in clear text
required: true
schema:
type: string
responses:
"200":
description: successful operation
headers:
Set-Cookie:
description: >-
Cookie authentication key for use with the `api_key`
apiKey authentication.
schema:
type: string
example: AUTH_KEY=abcde12345; Path=/; HttpOnly
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
type: integer
format: int32
X-Expires-After:
description: date in UTC when token expires
schema:
type: string
format: date-time
content:
application/xml:
schema:
type: string
application/json:
schema:
type: string
"400":
description: Invalid username/password supplied
/user/logout:
get:
tags:
- user
summary: Logs out current logged in user session
description: ""
operationId: logoutUser
responses:
default:
description: successful operation
security:
- api_key: []
"/user/{username}":
get:
tags:
- user
summary: Get user by user name
description: ""
operationId: getUserByName
parameters:
- name: username
in: path
description: The name that needs to be fetched. Use user1 for testing.
required: true
schema:
type: string
responses:
"200":
description: successful operation
content:
application/xml:
schema:
$ref: "#/components/schemas/User"
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
description: Invalid username supplied
"404":
description: User not found
put:
tags:
- user
summary: Updated user
description: This can only be done by the logged in user.
operationId: updateUser
parameters:
- name: username
in: path
description: name that need to be deleted
required: true
schema:
type: string
responses:
"400":
description: Invalid user supplied
"404":
description: User not found
security:
- api_key: []
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/User"
description: Updated user object
required: true
delete:
tags:
- user
summary: Delete user
description: This can only be done by the logged in user.
operationId: deleteUser
parameters:
- name: username
in: path
description: The name that needs to be deleted
required: true
schema:
type: string
responses:
"400":
description: Invalid username supplied
"404":
description: User not found
security:
- api_key: []
externalDocs:
description: Find out more about Swagger
url: "http://swagger.io"
components:
requestBodies:
UserArray:
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
description: List of user object
required: true
Pet:
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
application/xml:
schema:
$ref: "#/components/schemas/Pet"
description: Pet object that needs to be added to the store
required: true
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: "http://petstore.swagger.io/api/oauth/dialog"
scopes:
"write:pets": modify pets in your account
"read:pets": read your pets
api_key:
type: apiKey
name: api_key
in: header
schemas:
Order:
title: Pet Order
description: An order for a pets from the pet store
type: object
properties:
id:
type: integer
format: int64
petId:
type: integer
format: int64
quantity:
type: integer
format: int32
shipDate:
type: string
format: date-time
status:
type: string
description: Order Status
enum:
- placed
- approved
- delivered
complete:
type: boolean
default: false
xml:
name: Order
Category:
title: Pet category
description: A category for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
xml:
name: Category
User:
title: a User
description: A User who is purchasing from the pet store
type: object
properties:
id:
type: integer
format: int64
username:
type: string
firstName:
type: string
lastName:
type: string
email:
type: string
password:
type: string
phone:
type: string
userStatus:
type: integer
format: int32
description: User Status
xml:
name: User
Tag:
title: Pet Tag
description: A tag for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: Tag
Pet:
title: a Pet
description: A pet for sale in the pet store
type: object
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
category:
$ref: "#/components/schemas/Category"
name:
type: string
example: doggie
photoUrls:
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
tags:
type: array
xml:
name: tag
wrapped: true
items:
$ref: "#/components/schemas/Tag"
status:
type: string
description: pet status in the store
deprecated: true
enum:
- available
- pending
- sold
xml:
name: Pet
ApiResponse:
title: An uploaded response
description: Describes the result of uploading an image resource
type: object
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
Swagger UI の HTML を配置
下記のドキュメントで Swagger UI のコード HTML に unpkg を使って直接埋め込むサンプルが紹介されているので、これを参考にして docs/api/index.html
を作成します。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="SwaggerUI" />
<title>SwaggerUI</title>
<link
rel="stylesheet"
href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css"
/>
</head>
<body>
<div id="swagger-ui"></div>
<script
src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js"
crossorigin
></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: "./petstore.yaml",
dom_id: "#swagger-ui",
});
};
</script>
</body>
</html>
CDK コード
CloudFront + S3 上でベーシック認証付きでホスティングするための CDK コードです。ベーシック認証はこちらを参考に CloudFront Function と KeyValueStore を使って実装しています。
import * as cdk from "aws-cdk-lib";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as cloudfront_origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3_deployment from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
export class MainStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// S3 バケット
const bucket = new s3.Bucket(this, "Bucket", {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// Basic認証のための KeyValueStore の作成
const keyValueStore = new cloudfront.KeyValueStore(this, "KeyValueStore");
// KeyValueStore の ARN を出力
new cdk.CfnOutput(this, "KeyValueStoreArn", {
value: keyValueStore.keyValueStoreArn,
});
// Basic認証を行う CloudFront Function の作成
const cloudfrontFunction = new cloudfront.Function(this, "Function", {
keyValueStore, // KeyValueStore を指定。
code: cloudfront.FunctionCode.fromFile({
filePath: "./lib/function.js",
}),
});
// CloudFront Distribution
const distribution = new cloudfront.Distribution(this, "Distribution", {
defaultBehavior: {
origin:
cloudfront_origins.S3BucketOrigin.withOriginAccessControl(bucket),
// Basic認証を有効にする
functionAssociations: [
{
function: cloudfrontFunction,
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
},
],
},
// index.html をデフォルトのオブジェクトとして設定
defaultRootObject: "index.html",
});
// Distribution のドメイン名を出力
new cdk.CfnOutput(this, "DistributionDomainName", {
value: distribution.distributionDomainName,
});
// S3 バケットへのコンテンツのデプロイ
new s3_deployment.BucketDeployment(this, "BucketDeploy", {
sources: [
s3_deployment.Source.asset("./docs/api"),
s3_deployment.Source.data("favicon.ico", ""),
],
destinationBucket: bucket,
distribution,
});
}
}
上記を CDK デプロイします。
ベーシック認証のユーザー名とパスワードを KeyValueStore に登録します。
ETAG=$(aws cloudfront-keyvaluestore describe-key-value-store --kvs-arn $KEY_VALUE_STORE_ARN --query 'ETag' --output text)
aws cloudfront-keyvaluestore put-key --key user01 --value hoge --kvs-arn $KEY_VALUE_STORE_ARN --if-match $ETAG
CloudFront のドメイン名をブラウザで開き、ベーシック認証を行うと、Swagger UI が表示されました!
同ページの下半分です。当然ですがブラウザで操作してインタラクティブな API ドキュメントとして利用できます。
Swagger UI にトップバーを表示させる場合
先述の unpkg を使った Swagger UI のドキュメントでは、layout
を StandaloneLayout
に変更することによりトップバーを表示するサンプルも紹介されていたので試してみます。docs/api/index.html
をサンプルを参考に下記のように変更します。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="SwaggerUI" />
<title>SwaggerUI</title>
<link
rel="stylesheet"
href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css"
/>
</head>
<body>
<div id="swagger-ui"></div>
<script
src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js"
crossorigin
></script>
<script
src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js"
crossorigin
></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: "./petstore.yaml",
dom_id: "#swagger-ui",
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
layout: "StandaloneLayout",
validatorUrl: null, // バリデーターバッジを非表示にする
});
};
</script>
</body>
</html>
再度 CDK デプロイを行って同ドメイン名をブラウザで開くと、Explore メニューが付いたトップバーが表示されました。
同ページの下半分です。こちらは先程と同じです。
ちなみに validatorUrl: null
としない場合はページ右下にバリデーターバッジが表示されるのですが、なぜか必ずバリデーションエラーが発生してしまいます。
バッジのリンクを踏むと下記の画面に遷移します。yml ファイルが読み込めないため、バリデーションエラーが発生しているようです。
{"schemaValidationMessages":[{"level":"error","message":"Can't read from file https://d1plcl5stempph.cloudfront.net/petstore.yaml"}]}
ただしブラウザから Swagger UI を利用する上では問題はないため、validatorUrl: null
としてバリデーターバッジを非表示にするのが良いでしょう。
複数の OAC ドキュメントをホストする場合
Swagger UI では複数の OAC ドキュメントに対応した表示も可能です。追加のサンプル OAC ドキュメントを、docs/api/bookstore.json
としてローカルに配置します。
Bookstore API ドキュメント
{
"openapi": "3.0.0",
"info": {
"title": "Book Store API",
"description": "Simple bookstore management API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.bookstore.com/v1",
"description": "Production server"
}
],
"paths": {
"/books": {
"get": {
"summary": "Get all books",
"tags": ["books"],
"parameters": [
{
"name": "category",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "List of books",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Book"
}
}
}
}
}
}
},
"post": {
"summary": "Create a book",
"tags": ["books"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BookInput"
}
}
}
},
"responses": {
"201": {
"description": "Book created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Book"
}
}
}
}
}
}
},
"/books/{id}": {
"get": {
"summary": "Get book by ID",
"tags": ["books"],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Book details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Book"
}
}
}
},
"404": {
"description": "Book not found"
}
}
}
}
},
"components": {
"schemas": {
"Book": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"author": {
"type": "string"
},
"price": {
"type": "number"
},
"category": {
"type": "string"
}
}
},
"BookInput": {
"type": "object",
"required": ["title", "author", "price"],
"properties": {
"title": {
"type": "string"
},
"author": {
"type": "string"
},
"price": {
"type": "number"
},
"category": {
"type": "string"
}
}
}
}
}
}
そして docs/api/index.html
を下記のように変更します。複数の OAC ドキュメントを読み込むために、urls
プロパティを使用しています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="SwaggerUI" />
<title>SwaggerUI</title>
<link
rel="stylesheet"
href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css"
/>
</head>
<body>
<div id="swagger-ui"></div>
<script
src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js"
crossorigin
></script>
<script
src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js"
crossorigin
></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
// 複数の OpenAPI ドキュメントを読み込む
urls: [
{
url: "./echo.yaml",
name: "Echo API",
},
{
url: "./petstore.yaml",
name: "Petstore API",
},
],
dom_id: "#swagger-ui",
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
layout: "StandaloneLayout",
validatorUrl: null, // バリデーションを無効化
});
};
</script>
</body>
</html>
再度 CDK デプロイを行ってドメイン名をブラウザで開くと、トップバーの右上に Select a definition
ドロップダウンが表示されるようになりました。
ドロップダウンリストを操作すると、選択した OAC ドキュメントの API に表示を切り替えることができます。
おわりに
OpenAPI ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Swagger UI で視覚化する構成を AWS CDK で実装してみたので、紹介しました。
今回の構成を使うことで、セキュリティを保ちながら API ドキュメントを公開できます。どなたかの参考になれば幸いです。
参考
以上