業界・業務から探す
導入目的・課題から探す
データ・AIについて学ぶ
News
Hakkyについて
ウェビナーコラム
◆トップ【データ基盤】
データハブとは
Ajust
データの保守運用
AI

執筆者:Handbook編集部

dbt docsをs3でホスティングする【Terraformでの実装付】

この記事では、dbt のドキュメントを s3 でホスティングする方法について紹介します。
基本的にdbt を ECS で実行するで紹介している内容に機能追加する形での紹介となります。

構成

この記事で作成する環境です。
ECR・ECS はdbt を ECS で実行するで作成した環境そのままで、 S3・Lambda・Cloud Front を追加で作成します。Lambda は Basic 認証のために配置しています。

Lambda@Edge 作成

まずは Basic 認証用 Lambda@Edge だけ python3.9 ランタイムで手動作成します。Lambda@Edge はバージニアリージョンでのみ作成できる点に注意してください。

Python コードは下記の通りです。
参照: CloudFront Lambda@Edge with Python 3.8 で Basic 認証を実現する

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Basic認証を行う Lambda@Edge
"""

import base64


def authenticate(user, password):
    """ 認証 """
    return user == 'ユーザー名' and password == 'パスワード'


def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']

    error_response = {
        'status': '401',
        'statusDescription': 'Unauthorized',
        'body': 'Authentication Failed',
        'headers': {
            'www-authenticate': [
                {
                    'key': 'WWW-Authenticate',
                    'value': 'Basic realm="Basic Authentication"'
                }
            ]
        }
    }

    if 'authorization' not in headers:
        return error_response

    try:
        auth_values = headers['authorization'][0]['value'].split(" ")
        auth = base64.b64decode(auth_values[1]).decode().split(":")
        (user, password) = (auth[0], auth[1])
        return request if authenticate(user, password) else error_response
    except Exception:
        # フォーマット不正など
        return error_response

Lambda を作成したらバージョンを発行する必要がある点に注意です。

Terraform でのインフラ環境構築

次に、terraform を使用して、環境構築を行います。

ここで構築するリソースは下記の通りです。

  • S3 バケット周り
  • Lambda 用 IAM ロール
  • Cloud Front 周り
resource "aws_s3_bucket" "dbt_on_ecs" {
  provider = aws.s3_region
  bucket   = "${var.project}-docs-static"
}

resource "aws_s3_bucket_policy" "dbt_on_ecs" {
  provider = aws.s3_region
  bucket   = aws_s3_bucket.dbt_on_ecs.id
  policy = jsonencode(
    {
      Id      = "PolicyForCloudFrontPrivateContent"
      Version = "2008-10-17"
      Statement = [
        {
          Action = "s3:GetObject"
          Condition = {
            StringEquals = {
              "AWS:SourceArn" = "${aws_cloudfront_distribution.s3_distribution.arn}"
            }
          }
          Effect = "Allow"
          Principal = {
            Service = "cloudfront.amazonaws.com"
          }
          Resource = "${aws_s3_bucket.dbt_on_ecs.arn}/*"
          Sid      = "AllowCloudFrontServicePrincipal"
        },
      ]
    }
  )
}

resource "aws_s3_bucket_public_access_block" "dbt_on_ecs" {
  bucket = aws_s3_bucket.dbt_on_ecs.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "dbt_on_ecs" {
  provider = aws.s3_region
  bucket   = aws_s3_bucket.dbt_on_ecs.id

  rule {
    bucket_key_enabled = true

    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_website_configuration" "dbt_on_ecs" {
  provider = aws.s3_region
  bucket   = aws_s3_bucket.dbt_on_ecs.id

  index_document {
    suffix = "index.html"
  }
}

data "aws_lambda_function" "dbt_on_ecs" {
  provider      = aws.lambda_region
  function_name = var.project
}

resource "aws_iam_role" "lambda" {
  name = "${var.project}-basic-auth-function"
  path = "/service-role/"
  assume_role_policy = jsonencode(
    {
      Statement = [
        {
          Action = "sts:AssumeRole"
          Effect = "Allow"
          Principal = {
            Service = [
              "lambda.amazonaws.com",
              "edgelambda.amazonaws.com",
            ]
          }
        },
      ]
      Version = "2012-10-17"
    }
  )
}

resource "aws_iam_policy" "lambda" {
  name = "${var.project}-AWSLambdaEdgeExecution"

  policy = <<EOF
{
  "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        }
    ]
}
EOF
}

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

resource "aws_cloudfront_distribution" "s3_distribution" {
  default_root_object = "index.html"
  enabled             = true
  wait_for_deployment = true

  default_cache_behavior {
    allowed_methods = [
      "GET",
      "HEAD",
    ]
    cached_methods = [
      "GET",
      "HEAD",
    ]
    compress               = true
    default_ttl            = 0
    max_ttl                = 0
    min_ttl                = 0
    smooth_streaming       = false
    target_origin_id       = aws_s3_bucket.dbt_on_ecs.bucket_regional_domain_name
    viewer_protocol_policy = "allow-all"

    lambda_function_association {
      event_type   = "viewer-request"
      include_body = false
      lambda_arn   = data.aws_lambda_function.dbt_on_ecs.qualified_arn
    }
    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }
  }

  origin {
    connection_attempts      = 3
    connection_timeout       = 10
    domain_name              = aws_s3_bucket.dbt_on_ecs.bucket_regional_domain_name
    origin_id                = aws_s3_bucket.dbt_on_ecs.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.main.id
  }

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

  viewer_certificate {
    cloudfront_default_certificate = true
    minimum_protocol_version       = "TLSv1"
  }
}

resource "aws_cloudfront_origin_access_control" "main" {
  name                              = "dbt-on-ecs-terraform"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

terraform apply して環境構築します。 初回 apply 後、Lambda に Lambda 用 IAM ロールをアタッチする必要があります。

docker-entrypoint.sh 修正

次に、dbt docs を s3 へ配置するため、docker-entrypoint.shを修正します。

#!/bin/bash
dbt run --profiles-dir /usr/app/.dbt

dbt docs generate --profiles-dir /usr/app/.dbt

aws s3 cp target/manifest.json s3://<s3バケット名>/
aws s3 cp target/run_results.json s3://<s3バケット名>/
aws s3 cp target/index.html s3://<s3バケット名>/
aws s3 cp target/catalog.json s3://<s3バケット名>/

ドキュメント確認

ECS タスクを実行後、dbt のドキュメントを Cloud Front 経由で s3 から閲覧できるか確認してみます。

Cloud Front のディストリビューションドメインにアクセスしてみると、Basic 認証がかかっていることがわかります。

認証後、dbt docs を閲覧することができました。

S3 に直接アクセスしてみると、アクセスが拒否されることも確認できます。

まとめ

dbt docs を s3 でホスティングして Basic 認証をかけることで、セキュリティを担保しながら、dbt 実行サーバーへのホスティング負荷を回避し、安価にドキュメントを閲覧できる環境を構築することができます。

参考

info
備考

Hakky ではエンジニアを募集中です!まずは話してみたいなどでも構いませんので、ぜひお気軽に採用ページからお問い合わせくださいませ。

2025年07月06日に最終更新
読み込み中...