OpenAPI ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Redoc で視覚化する構成を AWS CDK で実装してみた

OpenAPI ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Redoc で視覚化する構成を AWS CDK で実装してみた

Clock Icon2025.07.11

こんにちは、製造ビジネステクノロジー部の若槻です。

前回のブログ では、OpenAPI ドキュメントを 視覚化するツールとして Swagger UI を使いましたが、同じく OpenAPI 仕様から API ドキュメントを生成するツールとして Redoc という OSS があります。開発およびメンテナンスは Redocly という企業が行っています。

https://github.com/Redocly/redoc

下記は Redoc で生成された API ドキュメントの公式サンプルです。パラメーターのスキーマ仕様が表示されるなど非常にリッチな UI となっています。

https://redocly.github.io/redoc/

今回は、OpenAPI ドキュメントを CloudFront + S3 上でベーシック認証付きでホスティングして Redoc で視覚化する構成を AWS CDK で実装してみました。

やってみた

サンプルの OAS ドキュメントを配置

下記で公開されているサンプルの OAS ドキュメント(ペットストア)を、docs/api/petstore.yaml に配置します。

https://raw.githubusercontent.com/openapitools/openapi-generator/master/modules/openapi-generator/src/test/resources/3_0/petstore.yaml

Petstore OpenAPI ドキュメント
docs/api/petstore.yaml
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 ディレクトリに出力します。

https://redocly.com/docs/cli/commands/build-docs

npx @redocly/cli build-docs docs/api/petstore.yaml --output docs/api/build/index.html

CDK で実装

CloudFront + S3 上でベーシック認証付きでホスティングするための CDK コードです。ベーシック認証はこちらを参考に CloudFront Function と KeyValueStore を使って実装しています。

lib/main-stack.ts
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 で実装してみました。

どなたかの参考になれば幸いです。

以上

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.