
OpenAPI ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Redoc で視覚化する構成を AWS CDK で実装してみた
こんにちは、製造ビジネステクノロジー部の若槻です。
前回のブログ では、OpenAPI ドキュメントを 視覚化するツールとして Swagger UI を使いましたが、同じく OpenAPI 仕様から API ドキュメントを生成するツールとして Redoc という OSS があります。開発およびメンテナンスは Redocly という企業が行っています。
下記は Redoc で生成された API ドキュメントの公式サンプルです。パラメーターのスキーマ仕様が表示されるなど非常にリッチな UI となっています。
今回は、OpenAPI ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Redoc で視覚化する構成を 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
Redoc CLI で API ドキュメントを生成
Redoc CLI の build-docs
コマンドを使って、OpenAPI ドキュメントから Redoc の HTML ファイルを生成します。--output
を指定しない場合はカレントディレクトリに redoc-static.html
というファイル名で出力されます。今回は BucketDeployment
で S3 バケットにデプロイするため、docs/api/build
ディレクトリに出力します。
npx @redocly/cli build-docs docs/api/petstore.yaml --output docs/api/build/index.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,
},
],
},
defaultRootObject: "index.html", // 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/build"), // Redoc CLI の出力先ディレクトリを指定
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 のドメイン名をブラウザで開き、ベーシック認証を行うと、Redoc の UI が表示されました!
Redoc CLI には lint コマンドもある
今回 は Redoc CLI の build-docs
コマンドを使いましたが、Redoc CLI には OpenAPI ドキュメントの検証を行う lint
コマンドもあります。
実行すると次のように OpenAPI ドキュメントの検証を行い、問題があればエラーや警告を出力します。ドキュメントの品質を保つために CI に組み込んでも良さそうですね。
$ npx @redocly/cli lint docs/api/petstore.yaml
No configurations were provided -- using built in recommended configuration by default.
validating docs/api/petstore.yaml...
[1] docs/api/petstore.yaml:309:5 at #/paths/~1store~1order/post
Every operation should have security defined on it or on the root level.
307 | - api_key: []
308 | /store/order:
309 | post:
310 | tags:
311 | - store
Error was generated by the security-defined rule.
[2] docs/api/petstore.yaml:335:5 at #/paths/~1store~1order~1{orderId}/get
Every operation should have security defined on it or on the root level.
333 | required: true
334 | "/store/order/{orderId}":
335 | get:
336 | tags:
337 | - store
Error was generated by the security-defined rule.
[3] docs/api/petstore.yaml:367:5 at #/paths/~1store~1order~1{orderId}/delete
Every operation should have security defined on it or on the root level.
365 | "404":
366 | description: Order not found
367 | delete:
368 | tags:
369 | - store
Error was generated by the security-defined rule.
[4] docs/api/petstore.yaml:435:5 at #/paths/~1user~1login/get
Every operation should have security defined on it or on the root level.
433 | $ref: "#/components/requestBodies/UserArray"
434 | /user/login:
435 | get:
436 | tags:
437 | - user
Error was generated by the security-defined rule.
[5] docs/api/petstore.yaml:498:5 at #/paths/~1user~1{username}/get
Every operation should have security defined on it or on the root level.
496 | - api_key: []
497 | "/user/{username}":
498 | get:
499 | tags:
500 | - user
Error was generated by the security-defined rule.
[6] docs/api/petstore.yaml:205:7 at #/paths/~1pet~1{petId}/post/responses
Operation must have at least one `2XX` response.
203 | type: integer
204 | format: int64
205 | responses:
206 | "405":
207 | description: Invalid input
Warning was generated by the operation-2xx-response rule.
[7] docs/api/petstore.yaml:243:7 at #/paths/~1pet~1{petId}/delete/responses
Operation must have at least one `2XX` response.
241 | type: integer
242 | format: int64
243 | responses:
244 | "400":
245 | description: Invalid pet value
Warning was generated by the operation-2xx-response rule.
[8] docs/api/petstore.yaml:265:7 at #/paths/~1pet~1{petId}~1uploadImage/post/responses
Operation must have at least one `4XX` response.
263 | type: integer
264 | format: int64
265 | responses:
266 | "200":
267 | description: successful operation
Warning was generated by the operation-4xx-response rule.
[9] docs/api/petstore.yaml:296:7 at #/paths/~1store~1inventory/get/responses
Operation must have at least one `4XX` response.
294 | description: Returns a map of status codes to quantities
295 | operationId: getInventory
296 | responses:
297 | "200":
298 | description: successful operation
Warning was generated by the operation-4xx-response rule.
[10] docs/api/petstore.yaml:382:7 at #/paths/~1store~1order~1{orderId}/delete/responses
Operation must have at least one `2XX` response.
380 | schema:
381 | type: string
382 | responses:
383 | "400":
384 | description: Invalid ID supplied
Warning was generated by the operation-2xx-response rule.
[11] docs/api/petstore.yaml:394:7 at #/paths/~1user/post/responses
Operation must have at least one `4XX` response.
392 | description: This can only be done by the logged in user.
393 | operationId: createUser
394 | responses:
395 | default:
396 | description: successful operation
Warning was generated by the operation-4xx-response rule.
[12] docs/api/petstore.yaml:413:7 at #/paths/~1user~1createWithArray/post/responses
Operation must have at least one `4XX` response.
411 | description: ""
412 | operationId: createUsersWithArrayInput
413 | responses:
414 | default:
415 | description: successful operation
Warning was generated by the operation-4xx-response rule.
[13] docs/api/petstore.yaml:427:7 at #/paths/~1user~1createWithList/post/responses
Operation must have at least one `4XX` response.
425 | description: ""
426 | operationId: createUsersWithListInput
427 | responses:
428 | default:
429 | description: successful operation
Warning was generated by the operation-4xx-response rule.
[14] docs/api/petstore.yaml:492:7 at #/paths/~1user~1logout/get/responses
Operation must have at least one `4XX` response.
490 | description: ""
491 | operationId: logoutUser
492 | responses:
493 | default:
494 | description: successful operation
Warning was generated by the operation-4xx-response rule.
[15] docs/api/petstore.yaml:538:7 at #/paths/~1user~1{username}/put/responses
Operation must have at least one `2XX` response.
536 | schema:
537 | type: string
538 | responses:
539 | "400":
540 | description: Invalid user supplied
Warning was generated by the operation-2xx-response rule.
[16] docs/api/petstore.yaml:565:7 at #/paths/~1user~1{username}/delete/responses
Operation must have at least one `2XX` response.
563 | schema:
564 | type: string
565 | responses:
566 | "400":
567 | description: Invalid username supplied
Warning was generated by the operation-2xx-response rule.
docs/api/petstore.yaml: validated in 31ms
❌ Validation failed with 5 errors and 11 warnings.
run `redocly lint --generate-ignore-file` to add all problems to the ignore file.
おわりに
OpenAPI ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Redoc で視覚化する構成を AWS CDK で実装してみました。
どなたかの参考になれば幸いです。
以上