Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Amazon AthenaのPartition Projectionを使ったALB Access Logの実例 (w/ terraform & glue catalog)

こんにちは、グノシー広告技術部、Adnwチームでマネージャーをやっているサンドバーグです!

この記事は Gunosy Advent Calendar 2020 18日目の物となります。

昨日はsyouitさんの変更に強いリスト面とUICollectionViewの話でした。

内容としては弊社のiOSアプリでUITableViewの代わりにUICollectionViewを使っている理由と利点でした。 自分はiOSの開発やアプリ開発自体には関わることがほぼないのですが、わかりやすい説明だったので、まだ読まれていない方は是非読んでください!

はじめに

TL;DR

今回の実装はTerraformを使ってAccess Log向けのバケット・バケットポリシーを作り、KubernetesALB ingress controllerで設定できるAccess Log出力をONにし、格納したログをTerraformのglue catalogで作ったAWS AthenaテーブルでPartition Projectionを使い、クエリを叩けるようにする実装です。

実装の際に使用した機能・ツール・サービスは軽く説明しますが、詳細は添付するドキュメントを直接確認していただけると幸いです!

使ったツール・サービス・機能

Terraform

Terraformとは Infrastructure as Code を体現するためのツールです。 クラウド上などでのサービス管理をコードで定義し、作成・更新・削除の操作から人の手をなくすためのツールとなります。

弊社ではほとんどのインフラ環境でTerraformを導入しており、今回のAccess Log管理においてもAWS上の設定などはTerraform経由で行いました。

今回Terraformで使ったのは...

aws_s3_bucket & aws_s3_bucket_policy Link

Access Logの吐き出し先のs3バケット用意と、ALBへのログの投入権限を用意しました。

AmazonのAthenaとは...

...インタラクティブなクエリサービスで、Amazon S3 内のデータを標準 SQL を使用して簡単に分析できます。 Athena はサーバーレスなので、インフラストラクチャの管理は不要です... Athena は初期状態で AWS Glueデータカタログと統合されており... テーブル定義とパーティション定義のカタログへの入力、スキーマのバージョニング保持が可能です。 - Amazon Athena

今回ALBの標準機能でAccess Logsをs3に吐くため、そのままAthenaを使ってs3のログをクエリすることにしました。

Partition Projection は Amazon Athenaが持つパーティション管理機能で、パーティション値と場所をAWS Glueデータカタログのようなリポジトリ読み込みではなく、設定から計算される機能です。

弊社ではAthenaでのパーティション管理する際にバッチ処理でパーティションを切ることが大半でしたが、今回はそれが不要となるPartition Projectionを採用してみました。

ALB Access Logs はその名の通りロードバランサー(ELB/ALB)に送信されるリクエストの詳細情報を圧縮したログでS3に吐いてくれる機能です。ロードバランサーにデフォルトで入っておりますが、デフォルトでは OFFになっているので、こちらをONにして使います。

実装

Terraform - バケット・バケットポリシーの作成

今回Access Logを吐く際に、Amazonで自動的に生成するバケットではなく、こちらで明示的に用意した物を作りたいため、 Terraformを使いバケットとそのバケットポリシーを用意します。

resource "aws_s3_bucket" "access_logs" {
  bucket = "access-log-${terraform.workspace}"

  lifecycle_rule {
    id      = "alb-api"
    enabled = true
    prefix  = ""

    expiration {
      days = 10
    }
  }
  tags = {
    environment = var.long_env[terraform.workspace]
    Name        = "access-log-${terraform.workspace}"
  }
}

Production/Stagingと環境を分けてバケットとポリシーを作りたいためバケット名、tagsなどではterraform.workspaceを使って定義します。 Expirationを追加する理由としてはAccess Logの場合は調査が目的であり、長期保存をしないようTTLの設定をするためです。

次に、ALBが吐いたログをS3に格納できるよう、バケットポリシーにPubObjectの許可を付与します。

data "aws_iam_policy_document" "access_logs" {
  statement {

    sid = "writable-elb-access-log"

    effect = "Allow"

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${local.elb-account-id}:root"]
    }

    actions = [
      "s3:PutObject",
    ]

    resources = [
      "arn:aws:s3:::${aws_s3_bucket.access_logs.id}/*",
    ]
  }
}

resource "aws_s3_bucket_policy" "access_logs" {
  bucket = aws_s3_bucket.access_logs.id

  policy = data.aws_iam_policy_document.access_logs.json
}

ここの注意点としては principalsidentifiers に定義しているelb-account-idはAmazonのELBが持つ固定のaccount_idであるというところです。 使っているリージョンによってここのIDが変わるためドキュメントの Bucket permissions (バケットのアクセス許可) を参考にしてください。

ALB Access Logの設定

Terraformでバケットとポリシーが作成されたのが確認できれば、次はALBのリクエストログの格納です。 今回ALBのAccess LogがほしいAPIはKubernetesで動いているため、レポジトリで管理しているIngress Controllerの設定でAccess Logの定義をします。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: api-alb
  annotations:
    kubernetes.io/ingress.class: alb
    ...
    alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket=access-log-stg,access_logs.s3.prefix=elb

追加するのはalb.ingress.kubernetes.io/load-balancer-attributesの一行で、enabled=trueを設定した時点でALBのAccess Logsが吐かれるようになります。 そのほか s3.bucket / s3.prefix は今回用意したバケットとその中でのパスを指定します。

ALB Access Logsの設定は以上で完了、設定がALBに反映され次第ログはS3に投入され始めます。

Terraform - テーブル作成とPartition Projectionの設定

この時点でAccess Logsは指定したprefix配下にドキュメント に記載されているパス・ファイル名で吐かれているはずです。

ログが確認できれば、次のステップがTerraformを使ったAthenaのテーブル作成とPartition Projectionの設定となります。

今回のテーブル作成に使うのはTerraformのaws_glue_catalog_tableとなります。 テーブル作成においては、athena_named_queryという、Athenaに対して直接クエリ実行を行うリソースもありますが、見栄え、パラメータ・カラム管理・更新の楽さから aws_glue_catalog_table を使うことにしました。

実際のtfファイルは以下となります。

カラムは一部省略しているので こちらのドキュメント を参考に足してください。

resource "aws_glue_catalog_table" "access_log" {
  database_name = local.database_name[terraform.workspace]
  name          = "access_logs"
  description   = "access log table by terraform"

  table_type = "EXTERNAL_TABLE"
  parameters = {
    "projection.enabled"            = "true"
    "projection.date.type"          = "date"
    "projection.date.range"         = "NOW-1MONTH,NOW"
    "projection.date.format"        = "yyyy/MM/dd"
    "projection.date.interval"      = "1"
    "projection.date.interval.unit" = "DAYS"
    "storage.location.template"     = "${local.access_log_location_base_uri[terraform.workspace]}/AWSLogs/${var.account_id}/elasticloadbalancing/${data.aws_region.current.name}/$${date}"
    "classification"                = "csv"
    "compressionType"               = "gzip"
    "typeOfData"                    = "file"
  }

  storage_descriptor {
    location      = "${local.access_log_location_base_uri[terraform.workspace]}/AWSLogs/${var.account_id}/elasticloadbalancing/${data.aws_region.current.name}"
    input_format  = "org.apache.hadoop.mapred.TextInputFormat"
    output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat"

    ser_de_info {
      name                  = "access_logs"
      serialization_library = "org.apache.hadoop.hive.serde2.RegexSerDe"

      parameters = {
        "serialization.format" = "1"
        "input.regex"          = "([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\\s]+?)\" \"([^\\s]+)\" \"([^ ]*)\" \"([^ ]*)\""
      }
    }

    columns {
      name = "type"
      type = "string"
    }
    columns {
      name = "time"
      type = "string"
    }
    columns {
      name = "elb"
      type = "string"
    }

    ...

    columns {
      name = "classification"
      type = "string"
    }
    columns {
      name = "classification_reason"
      type = "string"
    }
  }
  partition_keys {
    name = "date"
    type = "string"
  }
}

上記のtfファイルでのPartition ProjectionはAthenaのtable properties/parametersで設定します。 Partition Projectionはdateで指定された期間・単位を元にlocation.templateに記載している${date}の先をパーティションとして自動的に判断して動きます。 Access Logsは year/month/day とディレクトリを吐くので、こちらで設定した物が推奨です。

注意点としてはlocation.templateの指定にある${var.account_id}がバケットポリシーで設定したelb_account_idではなく、自分のアカウントでなければいけないというところと、${date}の定義を$${date}にしなければいけないというところです。 $${date}に関しては、Terraformのinterpolation syntaxのため、そのままの状態で展開する際には$を追加しないといけないというものです。Link

個別のPartition Projectionの説明はこちらのPartition ProjectionPartition Projection setupに細かく記載されていますが、よりわかりやすい説明としては以下の記事をおすすめします。

Amazon AthenaのPartition Projectionを使ったALBのアクセスログ解析環境をTerraformで構築する

parameters = {
    "projection.enabled"            = "true"
    "projection.date.type"          = "date"
    "projection.date.range"         = "NOW-1MONTH,NOW"
    "projection.date.format"        = "yyyy/MM/dd"
    "projection.date.interval"      = "1"
    "projection.date.interval.unit" = "DAYS"
    "storage.location.template"     = "${local.access_log_s3_base_uri[terraform.workspace]}/AWSLogs/${var.account_id}/elasticloadbalancing/${data.aws_region.current.name}/$${date}"
    "classification"                = "csv"
    "compressionType"               = "gzip"
    "typeOfData"                    = "file"
  }

テーブルのカラムとser_de propertiesはドキュメントに記載されている物を使います。

  storage_descriptor {
    location      = "${local.access_log_location_base_uri[terraform.workspace]}/AWSLogs/${var.account_id}/elasticloadbalancing/${data.aws_region.current.name}"
    input_format  = "org.apache.hadoop.mapred.TextInputFormat"
    output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat"

    ser_de_info {
      name                  = "access_logs"
      serialization_library = "org.apache.hadoop.hive.serde2.RegexSerDe"

      parameters = {
        "serialization.format" = "1"
        "input.regex"          = "([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\\s]+?)\" \"([^\\s]+)\" \"([^ ]*)\" \"([^ ]*)\""
      }
    }

結果

上記で問題なくテーブル作成が完了すれば、AWSのGlue Catalogでテーブルが作成されていることが確認でき...

Athenaで作成したテーブルにすぐにクエリが投げられるようになっているはずです!

最後に

今回は弊社の一システムでのAccess Logを見やすくするためのAthena Partition Projectionを使った実例の紹介でした。 特に複雑な実装ではないですが、自社で既にterraform/eksなどの実装をされている場合すぐに試せる物になっているので是非参考にしてください。

明日はKubotaさんの記事になります!お楽しみに!