ハウテレビジョンブログ

『外資就活ドットコム』『Liiga』『Mond』を開発している株式会社ハウテレビジョンのブログです。

Cognitoを利用して静的コンテンツへのアクセスを手軽にした話

はじめに

こんにちは、Platform Engineeringに所属している縄司です。

今日は、以前紹介したデータカタログの共有方法をアップデートしましたのでご紹介します。

出力先の現状と課題

https://blog.howtelevision.co.jp/entry/2024/02/16/161754に記載していますが、SchemaspyやDBTで作成したデータカタログをGitHub Action経由でSlackに出力させる方法を採用しています。

これにより、全社員が最新のデータカタログにアクセスすることができるようになりました。少しずつ社内でも活用される場面が増えていることは嬉しい限りです。

ただ利用する中で感じたこととしては、都度ファイルをダウンロードして確認するのが面倒臭いということです。ダウンロードした後ローカルで最新のファイルを探したり、古いバージョンが溜まっていくことに嫌気がさしてきます。

そこでエンジニア・非エンジニア問わず社員限定で閲覧できるという要件を満たし、より手軽にアクセスが可能な状態にできないかと模索しました。

元々Amazon S3にデータを保管しアクセスする案があったのですが、社内限定で公開するためには非エンジニアの方もアカウントを発行する必要があり管理コストが増えるため不採用となっていました。

ユーザー認証ができかつユーザー管理の負担が少ないツールはないかと探している時に見つけたのがAWS Cognitoです。

AWS Cognito

Amazon Cognitoは、AWSが提供する認証およびユーザー管理サービスでGoogleやFacebook、Login with Amazon、Appleなど様々な外部IDプロバイダーにも対応しています。

そのため、Google認証でアクセスできるようにすれば余計なユーザー管理が不要になります。

また50,000 MAU(月間アクティブユーザー)まで無料利用枠があるため、社内利用のみであるならば基本無料で使うことができます。

https://aws.amazon.com/jp/cognito/

これを基に環境を再構築することにしました。

実装の流れ

構想

AWS Cognitoはユーザーの認証部分のみを担う機能なので、実際にS3にアクセスするためにCloudfrontとLambda@Edgeを採用し全体の流れを構成しました。

実装はTerraformで行っていますが、全てのリソースを一度にApplyするとCycle Errorが発生し、うまく構築することができなかったので段階的に実装しています。

1. GCPでOAuth 2.0クライアントIDを作成

Google認証をCognitoで利用するための設定です。

  1. APIとサービス > 認証情報に移動
  2. 「認証情報を作成」ボタンをクリックし、「OAuth クライアントID」を選択
  3. 「同意画面の設定」で、必要な情報を入力
  4. 「アプリケーションの種類」を「ウェブアプリケーション」に設定し、名前を入力

    ※ User Typeを内部に設定することで組織内だけのアクセスに制限することができます。

  5. 「承認されたリダイレクトURI」は、Cognitoの名称とリージョンが決まっていれば{cognito name}.{region}.amazoncognito.comを入力

以上により生成されたクライアントIDとクライアントシークレットを保持しておきます。

2. Cognitoの作成

生成されたクライアントIDとクライアントシークレットを基に実装します。

# cognito.tf
resource "aws_cognito_user_pool" "access_via_google_auth" {
  name = "access-via-google-auth"

  admin_create_user_config {
    allow_admin_create_user_only = false
  }
  auto_verified_attributes = ["email"]
  deletion_protection      = "INACTIVE"
  email_configuration {
    email_sending_account = "COGNITO_DEFAULT"
  }

  mfa_configuration = "OFF"
  user_attribute_update_settings {
    attributes_require_verification_before_update = ["email"]
  }
  username_attributes = ["email"]
  username_configuration {
    case_sensitive = false
  }
  verification_message_template {
    default_email_option = "CONFIRM_WITH_CODE"
  }
}

resource "aws_cognito_user_pool_client" "access_via_google_auth" {
  name                                 = "access-via-google-auth"
  user_pool_id                         = aws_cognito_user_pool.access_via_google_auth.id
  allowed_oauth_flows                  = ["code"]
  allowed_oauth_flows_user_pool_client = true
  allowed_oauth_scopes                 = ["email", "openid", "phone"]
  callback_urls                        = ["**********"] ## リダイレクト先を設定
  supported_identity_providers         = ["Google"]
  enable_token_revocation              = true
  explicit_auth_flows                  = ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_USER_SRP_AUTH"]
  token_validity_units {
    access_token  = "days"
    id_token      = "days"
    refresh_token = "days"
  }
  access_token_validity  = 1
  id_token_validity      = 1
  refresh_token_validity = 7 # 7日間

}

resource "aws_cognito_user_pool_domain" "access_via_google_auth" {
  domain       = "access-via-google-auth"
  user_pool_id = aws_cognito_user_pool.access_via_google_auth.id
}

resource "aws_cognito_identity_provider" "google_provider" {
  user_pool_id  = aws_cognito_user_pool.access_via_google_auth.id
  provider_name = "Google"
  provider_type = "Google"

  provider_details = {
    authorize_scopes = "email"
    client_id        = "***********.apps.googleusercontent.com" ## Google クライアントID
    client_secret    = "**************" ## Google クライアントシークレット
  }

  attribute_mapping = {
    email    = "email"
    username = "sub"
  }
}

この段階で一度terraform applyを実行します。

完了した後はGCP側で先ほどスキップした「承認されたリダイレクトURI」に、cognitoのドメインを入力します。

Cloudfront + Lambda@Edge + S3の作成

S3は、Cloudfront側からのみのアクセス制限をかけ実装しています。

# s3.tf
resource "aws_s3_bucket" "data_catalog" {
  bucket = "data-catalog"
}

resource "aws_s3_bucket_policy" "data_catalog" {
  bucket = aws_s3_bucket.data_catalog.id
  policy = jsonencode(
    {
      Statement = [
        {
          Sid    = "CloudFrontAccessS3"
          Effect = "Allow"
          Principal = {
            "Service" : "cloudfront.amazonaws.com"
          }
          Action   = "s3:GetObject"
          Resource = "arn:aws:s3:::${aws_s3_bucket.data_catalog.bucket}/*"
          Condition = {
            "ArnLike" : {
              "aws:SourceArn" : format("arn:aws:cloudfront::%s:distribution/%s", local.aws_account_id, aws_cloudfront_distribution.data_catalog.id)
            }
          }
        },
      ]
      Version = "2012-10-17"
    }
  )
}

output "aws_s3_bucket_name" {
  value = aws_s3_bucket.data_catalog.bucket
}

Cloudfront側は次のように実装しています。

# cloudfront.tf

resource "aws_cloudfront_cache_policy" "cache_policy" {
  name        = "cloudfront-cache-policy"
  min_ttl     = 1
  max_ttl     = 6000
  default_ttl = 600
  parameters_in_cache_key_and_forwarded_to_origin {
    headers_config {
      header_behavior = "none"
    }
    cookies_config {
      cookie_behavior = "none"
    }
    query_strings_config {
      query_string_behavior = "none"
    }
  }
}

resource "aws_cloudfront_distribution" "data_catalog" {
  origin {
    domain_name = aws_s3_bucket.data_catalog.bucket_regional_domain_name
    origin_id   = aws_s3_bucket.data_catalog.id

    # OACの設定
    origin_access_control_id = aws_cloudfront_origin_access_control.main.id
  }

  enabled = true

  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = aws_s3_bucket.data_catalog.id
    compress         = true
    cache_policy_id  = aws_cloudfront_cache_policy.cache_policy.id
    
    lambda_function_association {
      event_type   = "viewer-request"
      include_body = false
      lambda_arn   = aws_lambda_function.cognito_edge.qualified_arn
    }

    viewer_protocol_policy = "redirect-to-https"
  }

  price_class = "PriceClass_200"

  restrictions {
    geo_restriction {
      restriction_type = "none"
      locations        = []
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

}

resource "aws_cloudfront_origin_access_control" "main" {
  name                              = aws_s3_bucket.data_catalog.bucket
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

Lambdaは、cloudfrontにアクセスした際にcognitoの認証を経由させる目的ですが、cloudfrontではlambda@edgeしか対応していないためus-east-1にリージョンを設定しています。

またlambdaで使用する認証用のindex.jsには、cognitoのユーザープール情報を入力する必要がありますが、Terraformでは環境変数を直接index.jsに渡すことができないため、generate.shを作成し、動的にindex.jsを作成することで柔軟にcognitoの情報を渡せるようにしています。

## generate.sh

#/bin/bash

eval "$(jq -r '@sh "OUTPUT=\(.output) REGION=\(.region) USER_POOL_ID=\(.user_pool_id) USER_POOL_APP_ID=\(.user_pool_app_id) USER_POOL_DOMAIN=\(.user_pool_domain)"')"

cat > "${OUTPUT}" <<EOF
const {Authenticator} = require("cognito-at-edge");

const authenticator = new Authenticator({
    region: "${REGION}",
    userPoolId: "${USER_POOL_ID}",
    userPoolAppId: "${USER_POOL_APP_ID}",
    userPoolDomain: "${USER_POOL_DOMAIN}",
});

exports.handler = async (request) => authenticator.handle(request);
EOF

echo "{}"
## lambda.tf

data "aws_iam_policy_document" "policy_for_lambda" {
  statement {
    effect = "Allow"
    principals {
      type = "Service"
      identifiers = [
        "lambda.amazonaws.com",
        "edgelambda.amazonaws.com"
      ]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "iam_for_lambda" {
  name               = "CloudFrontAccessLambda"
  assume_role_policy = data.aws_iam_policy_document.policy_for_lambda.json
}

resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {  
    role = aws_iam_role.iam_for_lambda.name  
    policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# external data sourceを使用してスクリプトを呼び出す
data "external" "generate_lambda_auth" {
  program = ["/bin/bash", "${path.module}/generate.sh"]

  query = {
    output           = "${path.module}/lambdaEdge/index.js"
    region           = var.cognito_region
    user_pool_id     = var.cognito_user_pool_id
    user_pool_app_id = var.cognito_user_pool_app_id
    user_pool_domain = var.cognito_user_pool_domain
  }
}

data "archive_file" "cognito_lambdaedge" {
  type        = "zip"
  source_dir  = "./lambdaEdge"
  output_path = "./lambda_function_cognito_cloudfront.zip"
}

# Lambda関数の作成
resource "aws_lambda_function" "cognito_edge" {
  provider         = aws.us_east_1
  function_name    = "cognito_edge"
  role             = aws_iam_role.iam_for_lambda.arn
  handler          = "index.handler"
  runtime          = "nodejs20.x"
  filename         = data.archive_file.cognito_lambdaedge.output_path
  source_code_hash = data.archive_file.cognito_lambdaedge.output_base64sha256

  publish = true # Lambda関数のバージョニングを有効にする
}

以上で、terraform applyで無事に作成されれば完了です。

まとめ

Cognito + Cloudfront + Lambda@Edge + S3を利用することで、データカタログを手軽にアクセスできるようになりました。CognitoはコールバックURLを複数指定できるため、1つ作成することで他の静的コンテンツに応用することができます。手軽なアクセスを可能にし、ドキュメントをより充実させていければと思います。

ゼロイチのTerraform実装は初めてでしたが、理解が深まり良いキャッチアップになりました!