🍧

OpenAPIドキュメントをIP制限をかけてS3に配置する

2022/08/15に公開約7,800字

環境

機種 : MacBook Pro 2021(M1 Max)
OS : Monterey(12.2.1)
$ node -v
v18.7.0
$ npm --version
8.15.0

nodeのバージョン18系を利用していますが、

https://nodejs.org/ja/about/releases/

プロダクションアプリケーションでは、アクティブ LTS または メンテナンス LTS リリースのみを使用してください。

とのことで、2022/8/15時点で本番環境で利用するならバージョン16系を利用するべきです。偶数の最新バージョンを使えばええって事ではないらしいです。今更知りました。。。
まぁ今回は本番環境とかではないのでバージョン18系で進めてみました。

openapiドキュメントを記述する

まずはS3に上げるopenapiドキュメントを作成します。

openapi.yaml
openapi: 3.0.0
info:
  description: openapi s3 test
  version: 1.0.0
  title: Openapi S3 Test
host: localhost
servers:
  - url: 'http://localhost'
paths:
  /hello-world:
    get:
      summary: Hello world
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/get_hello_world'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/get_hello_world_500'
      operationId: get-hello-world
      description: hello world endpoint
  /user:
    post:
      summary: User
      operationId: post-user
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/post_user'
      description: user endpoint
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/req_post_user'
        description: POST /user request body
components:
  schemas:
    get_hello_world:
      type: object
      title: get_hello_world_200
      description: GET /hello-world response
      properties:
        id:
          type: integer
          description: ID
          minimum: 1
          example: 1
        message:
          type: string
          description: メッセージ
          example: hello world
        todayDate:
          type: string
          format: date
          description: 本日日付
          example: '2022-08-08'
          nullable: true
      required:
        - id
        - message
    get_hello_world_500:
      type: object
      description: GET /hello-world(status code 500) response
      properties:
        code:
          type: integer
          description: エラーコード
          minimum: 1
          example: 1
        message:
          type: string
          description: エラーメッセージ
          example: internal server error!
      required:
        - code
        - message
    post_user:
      title: post_user
      type: object
      description: POST /user response
    req_post_user:
      title: req_post_user
      type: object
      description: POST /user request body
      properties:
        name:
          type: string
          description: ユーザー名
          example: string
        mailAddress:
          type: string
          format: email
          description: メールアドレス
          example: user@example.com
        nationality:
          type: string
          description: 国籍
          enum:
            - jp
            - us
          example: jp
        birthYear:
          type: integer
          description: 誕生年
          format: int64
          minimum: 1900
          example: 1996
          nullable: true
      required:
        - name
        - mailAddress
        - nationality

yamlからhtmlを生成する

yaml形式のママでもS3に上げれますが、jsファイルなどが必要になってくるので、今回はシンプルにhtmlを上げようと思います。
redocというライブラリのCLIを利用してyamlからhtmlを生成します。

https://github.com/Redocly/redoc

https://redocly.com/docs/redoc/deployment/cli/

redoc-cliをインストールします。

$ npm i -g redoc-cli
$ redoc-cli --version
0.13.17

上記で生成したopenapi.yamlをhtmlに変換します。

$ redoc-cli build openapi.yaml --output openapi.html
$ open openapi.html

以下のようにブラウザで表示されていたら成功です。

IP制限をかけてS3にopenapiドキュメントを配置する

自分だけ見れれば満足するので、IP制限は設けておきます。
次の2記事を参考に進めていきます。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/WebsiteAccessPermissionsReqd.html#bucket-policy-static-site

https://aws.amazon.com/jp/premiumsupport/knowledge-center/block-s3-traffic-vpc-ip/

「パブリックアクセスをすべてブロック」のチェックをはずして、S3バケットを作成します。

作成したS3バケットのバケットポリシーを編集します。

作成したS3バケット名は作成したバケット名を、アクセスを許可したいIPは11.11.11.11/32のように指定して保存します。

1つ目のStatement(SourceIP)で指定IP以外のトラフィックを拒否し、
2つ目のStatement(PublicReadGetObject)で全てのオブジェクトの読み取りを許可しています。

{
    "Version": "2012-10-17",
    "Id": "SourceIP",
    "Statement": [
        {
            "Sid": "SourceIP",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::作成したS3バケット名",
                "arn:aws:s3:::作成したS3バケット名/*"
            ],
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": "アクセスを許可したいIP"
                }
            }
        },
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::作成したS3バケット名/*"
        }
    ]
}

先程作成しておいた、openapi.htmlをS3にアップロードします。そしてオブジェクトのURLを開いて以下のように表示できていたら成功です。

PCのIPを変更するか、スマホでwifiをオフにしたりしてIPを変更した状態にして、URLを踏むと「AccessDenied」となることを確認しておきます。

GithubActionsでS3にhtmlをアップロードする

S3に上げるという目的は達成しましたが、開発でGtihubActionsで変更を反映できると便利なのでやってみます。

IAMユーザーを作成する

GithubActionsで利用するIAMユーザーを作成します。アクセスキーを発行したいので、プログラムによるアクセスの方にチェックを入れます。

細かく設定した方が良いですが、ちょっとめんどくさかったのでS3FullAccessを設定します。

作成後、GithubActionsで使えるようにAWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYをGithubのSecretに設定しておきます。

workflowを記述する

mainブランチにpushされたらS3にアップロードするようにします。[バケット名]は作成したバケット名に置き換えます。

.github/workflows/upload_s3.yml
name: Upload to Amazon S3

on:
  push:
    branches:
      - "main"

jobs:
  build:
    name: Upload
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Upload to Amazon S3
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: aws s3 cp --recursive --region ap-northeast-1 openapi.html s3://[バケット名]/openapi.html

これでGithubActionsを動かしてみると成功するのかと思いきや、失敗、、、

An error occurred (AccessDenied) when calling the PutObject operation: Access Denied

バケットポリシーでIP制限をかけてしまったのが良くなかったぽいです。バケットポリシーを以下のように修正します。
閲覧のみIP制限をかけるために、Denyをs3:*にしていたのをs3:GetObjectのみにし、Allowにs3:PutObjectを追加します。

{
  "Version": "2012-10-17",
  "Id": "SourceIP",
  "Statement": [
    {
      "Sid": "SourceIP",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": [
        "arn:aws:s3:::作成したS3バケット名",
        "arn:aws:s3:::作成したS3バケット名/*"
      ],
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": "アクセスを許可したいIP"
        }
      }
    },
    {
      "Sid": "PublicReadGetPutObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::作成したS3バケット名/*"
    }
  ]
}

これでGithubActionsを再度動かすとuploadが成功しました。
IP制限しかしていないので、もう少しセキュリティは強固にしたほうが良さそうではあります。

2022/8/28追記 セキュリティ強化の方法

もう少しセキュリティを強化する方法として、Cloudfrontを通すことで、Basic認証+IP制限ができそうです。

https://zenn.dev/lightkun/scraps/4253dbca0fbe52

Discussion

ログインするとコメントできます