LoginSignup
1
0

More than 1 year has passed since last update.

Terraform と Amazon API Gateway でS3バケットにファイルアップロードする

Posted at

はじめに

Amazon API Gateway の AWS サービス統合では、Lambda を通さずに直接サービスにアクセスすることができて、No-Code、Low-Code な感じでサービスを構築することができる(実際には YAML とか VTL とか知らないといけないから、No-Code ではないか)。

今回は、S3バケットにファイルをアップロードする API Gateway を構築してみる。

なお、参考にしたのは以下の公式ドキュメントだ。

Amazon API Gatewayの構築

アップロードする API は API Gateway の prod/uploader に PUT するメソッドを作ることにしよう。
logformat.json はあってもなくても良いが、サービス統合するときには、ログをちゃんと出しておかないと、トラブったときに何もわからなくなるので、出しておくことを推奨する。

API Gateway を構築する HCL ファイルの全体
resource "aws_api_gateway_rest_api" "uploader" {
  name        = local.api_gateway_name
  description = "S3アップロードAPI用API Gateway"

  binary_media_types = [
    "application/octet-stream",
  ]
}

resource "aws_api_gateway_account" "my_api" {
  cloudwatch_role_arn = aws_iam_role.apigateway_putlog.arn
}

resource "aws_api_gateway_stage" "prod" {
  stage_name    = "prod"
  rest_api_id   = aws_api_gateway_rest_api.uploader.id
  deployment_id = aws_api_gateway_deployment.for_prod.id

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.apigateway_accesslog.arn
    format          = file("${path.module}/logformat.json")
  }
}

resource "aws_api_gateway_deployment" "for_prod" {
  rest_api_id = aws_api_gateway_rest_api.uploader.id

  triggers = {
    redeployment = sha1(join(",", list(
      jsonencode(aws_api_gateway_rest_api.uploader),
      jsonencode(aws_api_gateway_integration.uploader_put_s3),
    )))
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_method_settings" "all" {
  depends_on = [
    aws_api_gateway_account.my_api,
  ]

  rest_api_id = aws_api_gateway_rest_api.uploader.id
  stage_name  = aws_api_gateway_stage.prod.stage_name
  method_path = "*/*"

  settings {
    logging_level      = "INFO"
    data_trace_enabled = true
  }
}

resource "aws_api_gateway_request_validator" "uploader" {
  name                        = "Validator"
  rest_api_id                 = aws_api_gateway_rest_api.uploader.id
  validate_request_body       = true
  validate_request_parameters = false
}

resource "aws_api_gateway_resource" "uploader" {
  rest_api_id = aws_api_gateway_rest_api.uploader.id
  parent_id   = aws_api_gateway_rest_api.uploader.root_resource_id
  path_part   = "uploader"
}

resource "aws_api_gateway_method" "uploader_put" {
  rest_api_id   = aws_api_gateway_rest_api.uploader.id
  resource_id   = aws_api_gateway_resource.uploader.id
  http_method   = "PUT"
  authorization = "NONE"
}

resource "aws_api_gateway_method_response" "uploader_put_200" {
  rest_api_id = aws_api_gateway_rest_api.uploader.id
  resource_id = aws_api_gateway_resource.uploader.id
  http_method = aws_api_gateway_method.uploader_put.http_method
  status_code = "200"
}

resource "aws_api_gateway_integration" "uploader_put_s3" {
  rest_api_id = aws_api_gateway_rest_api.uploader.id
  resource_id = aws_api_gateway_resource.uploader.id
  http_method = aws_api_gateway_method.uploader_put.http_method

  type                    = "AWS"
  uri                     = "arn:aws:apigateway:${data.aws_region.current.name}:s3:path/${aws_s3_bucket.uploader.bucket}/test_object"
  integration_http_method = "PUT"

  credentials          = aws_iam_role.apigateway_s3_access.arn
  passthrough_behavior = "WHEN_NO_MATCH"
  content_handling     = "CONVERT_TO_BINARY"
}

アップロード先になる S3 バケットは以下のように作っておく。
これも当然ながら、アクセス制御は API Gateway 側で実施するので、バケットの設定は private で問題ない。

resource "aws_s3_bucket" "uploader" {
  bucket = local.s3_uploader_bucket_name
  acl    = "private"
}

また、IAM は CloudWatchLogs にアクセスできるよう、aws_iam_role.apigateway_putlog.arnAmazonAPIGatewayPushToCloudWatchLogs のマネージドポリシーをアタッチしておこう。

ここで肝になるのが、aws_api_gateway_rest_api.uploader

  binary_media_types = [
    "application/octet-stream",
  ]

の部分と、aws_api_gateway_integration.uploader_put_s3

  passthrough_behavior = "WHEN_NO_MATCH"
  content_handling     = "CONVERT_TO_BINARY"

の部分だ。上記の参考ドキュメントでの方式では、content-type: application/octet-stream のアップロードをしているため、以下の公式ドキュメントの

に書いてあるのに従い、今回は以下の通りのアップロード使用を考えているため、上記の通りの設定を行う。

メソッドリクエストペイロード リクエスト Content-Type ヘッダー binaryMediaTypes contentHandling 統合リクエストペイロード
バイナリデータ バイナリデータ型 一致するメディアタイプで設定 CONVERT_TO_BINARY バイナリデータ

aws_api_gateway_integration.uploader_put_s3 は、API Gateway のサービス統合が S3 バケットにアクセスできるよう、以下の通りのロールを作っておく。

resource "aws_iam_role" "apigateway_s3_access" {
  name               = local.rest_api_s3_access_role_name
  assume_role_policy = data.aws_iam_policy_document.apigateway_s3_access_assume.json
}

data "aws_iam_policy_document" "apigateway_s3_access_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "apigateway.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy_attachment" "apigateway_s3_access" {
  role       = aws_iam_role.apigateway_s3_access.name
  policy_arn = aws_iam_policy.apigateway_s3_access_custom.arn
}

resource "aws_iam_policy" "apigateway_s3_access_custom" {
  name   = local.rest_api_s3_access_policy_name
  policy = data.aws_iam_policy_document.apigateway_s3_access_custom.json
}

data "aws_iam_policy_document" "apigateway_s3_access_custom" {
  statement {
    effect = "Allow"

    actions = [
      "s3:PutObject",
    ]

    resources = [
      "${aws_s3_bucket.uploader.arn}/*",
    ]
  }
}

さて、これで API Gateway は準備完了だ。

ファイルアップロードをする Javascript

シンプルに以下のように作成する。
コンテンツは、Nginx なり S3 の静的Webサイトホスティングなりに入れておけば良いだろう。
${apigateway_invoke_url} の部分は適宜置換を行う。

静的Webサイトホスティングについては以下の記事を参照。

また、上記の API Gateway はかなり雑につくっているので、このようにファイルアップロードする Javascript を作る場合は、当然ながら CORS の設定が必要になる。
CORS も Amazon API Gateway であれば、サクッと対応することができる。詳細は以下の記事を参照。

index.html
<html>
  <head>
    <style>
      [v-cloak] { display: none }
    </style>
    <meta charset="utf-8">
    <title>S3アップローダ</title>
  </head>
  <body>
    <div id="app">
      <h1>S3アップローダ</h1>
      <input v-on:change="selectedFile" type="file" name="file">
      <button v-on:click="upload" type="submit">アップロード</button>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.js"></script>
    <script src="app.js"></script>
  </body>
</html>
app.js
const app = new Vue({
  el: '#app',
  data: {
    uploadFile: null
  },
  methods: {
    selectedFile: function (e) {
      e.preventDefault()
      const files = e.target.files
      this.uploadFile = files[0]
    },
    upload: function () {
      var fileReader = new FileReader()
      fileReader.addEventListener('load', function (e) {
        const config = {
          url: '${apigateway_invoke_url}/uploader',
          method: 'put',
          headers: {
            'content-type': 'application/octet-stream',
            accept: 'application/json'
          },
          data: e.target.result
        }
        axios
          .request(config)
          .then(function (response) {
            console.log(response)
          })
          .catch(function (error) {
            console.log(error)
          })
      })
      fileReader.readAsArrayBuffer(this.uploadFile)
    }
  }
})

app.$mount('#app')

これで、index.html のコンテンツからファイルアップロードすると、S3バケットにそのコンテンツが格納される。
バイナリ格納しているので、画像でも何でも渡せるはずだ!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0