ALB + Cognito をTerraformでまとめて作ってみた。

ALB + Cognito をTerraformでまとめて作ってみた。

Clock Icon2025.06.09

はじめに

皆様こんにちは、あかいけです。

最近 AWS Cognito に入門して、
スクラッチで作る場合に比べて簡単に認証機能を実装できることに感動しました。

とはいえ Cognito の設定、ドメイン関連の設定、紐付け先の設定 (Cloudfront、ALB、API Gateway…etc) などなど、意外と関連するサービスが多く、設定に手間取ってしまうこともあるのではないでしょうか?

というわけで一括でデプロイする方法を調べてみたのですが意外と紹介されていなかったので、
今回は Terraform でまとめてデプロイできるようにしたものを共有します。

構成図

今回は ALB + Cognito の構成です。
またパブリック証明書の発行やドメインの登録も Terraform 内に含めています。

shapes-1749456061088

事前準備

事前に準備が必要となるのは、ドメインだけです。
そのため、 Route53 でドメインの取得 + デフォルトで作成されるホストゾーン があれば OK です。

Terraform

デプロイ方法は簡単で、
variable "domain" の値を利用する Route53 のドメイン名に置き換えて、apply するだけです。
またALBに紐づけるドメイン名はcognito-alb-app.<var.domain>の形式にしているので、ここはお好みで変更してください。

main.tf
provider "aws" {
  region = "ap-northeast-1"
  default_tags {
    tags = {
      app     = local.app_name
      project = "terraform"
    }
  }
}

locals {
  app_name = "cognito-alb-app"
}

############
## Domain
############

variable "domain" {
  type    = string
  default = "example.com"
}

data "aws_route53_zone" "main" {
  name         = var.domain
  private_zone = false
}

locals {
  cognito_domain = "${local.app_name}.${var.domain}"
}

resource "aws_acm_certificate" "main" {
  domain_name = local.cognito_domain

  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "validation" {
  for_each = {
    for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  zone_id = data.aws_route53_zone.main.id
  name    = each.value.name
  type    = each.value.type
  records = [each.value.record]
  ttl     = 60
}

resource "aws_acm_certificate_validation" "main" {
  certificate_arn         = aws_acm_certificate.main.arn
  validation_record_fqdns = [for record in aws_route53_record.validation : record.fqdn]
}

resource "aws_route53_record" "main" {
  type = "A"

  name    = local.cognito_domain
  zone_id = data.aws_route53_zone.main.id

  alias {
    name                   = aws_lb.alb.dns_name
    zone_id                = aws_lb.alb.zone_id
    evaluate_target_health = true
  }
}

output "site_domain" {
  value = "https://${aws_route53_record.main.fqdn}"
}

############
## Cognito 
############

locals {
  users = {
    test_user = {
      name     = "TestUser"
      email    = "example1@example.com"
      password = "Temp123!"
    },
    test_user2 = {
      name     = "TestUser2"
      email    = "example2@example.com"
      password = "Temp123!"
    },
    test_user3 = {
      name     = "TestUser3"
      email    = "example3@example.com"
      password = "Temp123!"
    }
  }
}

resource "aws_cognito_user_pool" "main" {
  name = "${local.app_name}-user-pool"

  admin_create_user_config {
    allow_admin_create_user_only = true
  }

  password_policy {
    minimum_length    = 8
    require_lowercase = true
    require_numbers   = true
    require_symbols   = true
    require_uppercase = true
  }

  auto_verified_attributes = ["email"]
}

resource "aws_cognito_user" "users" {
  for_each = local.users

  user_pool_id = aws_cognito_user_pool.main.id
  username     = each.value.name

  attributes = {
    email          = each.value.email
    email_verified = true
    name           = each.value.name
  }

  temporary_password = each.value.password
}

resource "aws_cognito_user_pool_client" "main" {
  name                                 = "${local.app_name}-user-pool-client"
  user_pool_id                         = aws_cognito_user_pool.main.id
  generate_secret                      = true
  allowed_oauth_flows                  = ["code"]
  allowed_oauth_scopes                 = ["email", "openid", "profile"]
  allowed_oauth_flows_user_pool_client = true
  callback_urls                        = ["https://${local.cognito_domain}/oauth2/idpresponse"]
  supported_identity_providers         = ["COGNITO"]
}

resource "random_id" "suffix" {
  byte_length = 24
}

resource "aws_cognito_user_pool_domain" "main" {
  domain                = "auth-${random_id.suffix.hex}"
  user_pool_id          = aws_cognito_user_pool.main.id
  managed_login_version = 2
}

############
## Network
############

locals {
  vpc_cidr = "10.0.0.0/16"
  subnets = {
    public1 = {
      cidr_block        = "10.0.1.0/24"
      availability_zone = "ap-northeast-1a"
    }
    public2 = {
      cidr_block        = "10.0.2.0/24"
      availability_zone = "ap-northeast-1c"
    }
  }
}

resource "aws_vpc" "main" {
  cidr_block = local.vpc_cidr
  tags = {
    Name = "${local.app_name}-vpc"
  }
}

resource "aws_subnet" "public" {
  for_each                = local.subnets
  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value.cidr_block
  map_public_ip_on_launch = true
  availability_zone       = each.value.availability_zone
  tags = {
    Name = "${local.app_name}-${each.key}"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${local.app_name}-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
  tags = {
    Name = "${local.app_name}-public-rtb"
  }
}

resource "aws_route_table_association" "public" {
  for_each       = local.subnets
  subnet_id      = aws_subnet.public[each.key].id
  route_table_id = aws_route_table.public.id
}

############
## ALB
############

resource "aws_security_group" "alb_sg" {
  name        = "alb-sg"
  description = "Allow HTTPS"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${local.app_name}-alb-sg"
  }
}

resource "aws_lb_target_group" "tg" {
  name     = "${local.app_name}-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main.id

  health_check {
    enabled             = true
    interval            = 30
    path                = "/"
    port                = "traffic-port"
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 5
    protocol            = "HTTP"
    matcher             = "200"
  }

  tags = {
    Name = "${local.app_name}-target-group"
  }
}

resource "aws_lb" "alb" {
  name               = "${local.app_name}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets            = [for s in aws_subnet.public : s.id]
  tags = {
    Name = "${local.app_name}-alb"
  }
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = 443
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate_validation.main.certificate_arn

  default_action {
    type = "authenticate-cognito"
    authenticate_cognito {
      user_pool_arn              = aws_cognito_user_pool.main.arn
      user_pool_client_id        = aws_cognito_user_pool_client.main.id
      user_pool_domain           = aws_cognito_user_pool_domain.main.domain
      on_unauthenticated_request = "authenticate"
      scope                      = "openid profile email"
      session_cookie_name        = "AWSELBAuthSessionCookie"
      authentication_request_extra_params = {
        lang = "ja"
      }

    }
    order = 1
  }

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg.arn
    order            = 2
  }

  tags = {
    Name = "${local.app_name}-listener"
  }

  depends_on = [aws_acm_certificate_validation.main]
}

resource "aws_lb_target_group_attachment" "https" {
  target_group_arn = aws_lb_target_group.tg.arn
  target_id        = aws_instance.web.id
  port             = 80
}

############
## EC2
############

data "aws_ami" "amazonlinux_2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023*-kernel-6.1-x86_64"]
  }
}

resource "aws_security_group" "ec2_sg" {
  name        = "${local.app_name}-ec2-sg"
  description = "Allow HTTP from ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb_sg.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "${local.app_name}-ec2-sg"
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazonlinux_2023.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public["public1"].id
  vpc_security_group_ids = [aws_security_group.ec2_sg.id]
  iam_instance_profile   = aws_iam_instance_profile.main.name
  user_data              = <<EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "Hello World" >> /var/www/html/index.html
EOF

  tags = {
    Name = "${local.app_name}-ec2"
  }
}

resource "aws_iam_role" "main" {
  name = "${local.app_name}-ssm-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    Name = "${local.app_name}-ssm-cloudwatch-role"
  }
}

resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.main.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "main" {
  name = "SSMAndCloudWatchInstanceProfile"
  role = aws_iam_role.main.name
}

output "ssm_start_session" {
  value = "aws ssm start-session --target ${aws_instance.web.id}"
}
terraform apply;

デプロイが完了したら、マネジメントコンソールのCognitoのマネージドログインの画面に移動し、
「スタイルを作成」をクリックし、作成したアプリケーションクライアントを指定してスタイルを作成します。

スクリーンショット 2025-06-09 16.56.35

スクリーンショット 2025-06-09 16.56.48

ここまで完了すると、Cognitoで認証できるようになります。

login-image (1)

なお最後のスタイルを作成する作業については、おそらく現在Terraform側で対応していないため手動で作業しています。
ただし以下のIssueでこの機能のリクエストが出ているっぽいので、
Terraform側で定義できるようになる日も遠くはなさそうです。

https://github.com/hashicorp/terraform-provider-aws/issues/42580

設定値の解説

このままだとあまりに内容が薄いので、重要となる設定をいくつか解説します。

Domain

まずはパブリック証明書検証用のレコードです。
ぱっと見だと謎の呪文にしか見えませんが、パブリック証明書作成時に指定したドメイン検証用の値を参照して、それをCNAMEレコードとして登録しています。

resource "aws_route53_record" "validation" {
  for_each = {
    for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  zone_id = data.aws_route53_zone.main.id
  name    = each.value.name
  type    = each.value.type
  records = [each.value.record]
  ttl     = 60
}

次に以下のリソースはパブリック証明書の検証が完了したことを表すリソースです。
こちらで作成したCNAMEレコードを参照して、正常に検証が完了したか確認しています。

resource "aws_acm_certificate_validation" "main" {
  certificate_arn         = aws_acm_certificate.main.arn
  validation_record_fqdns = [for record in aws_route53_record.validation : record.fqdn]
}

Cognito

ユーザープールの設定では、認証方式に関する全般を設定しています。
今回は利用者側で勝手に新規ユーザーを作成できないようにしたかったため、
allow_admin_create_user_only = true とすることで管理者側でのみ作成できるようにしています。

resource "aws_cognito_user_pool" "main" {
  name = "${local.app_name}-user-pool"

  admin_create_user_config {
    allow_admin_create_user_only = true
  }

  password_policy {
    minimum_length    = 8
    require_lowercase = true
    require_numbers   = true
    require_symbols   = true
    require_uppercase = true
  }

  auto_verified_attributes = ["email"]
}

次にアプリケーションクライアントの設定では、callback_urls が重要な設定で、
名前の通り認証が成功した後にリダイレクトするURLを指定します。

またCognitoの仕様上、末尾に/oauth2/idpresponseを付与する必要があるため、
実際に指定するのは https:// + ドメイン名 + /oauth2/idpresponse の形式となります。

resource "aws_cognito_user_pool_client" "main" {
  name                                 = "${local.app_name}-user-pool-client"
  user_pool_id                         = aws_cognito_user_pool.main.id
  generate_secret                      = true
  allowed_oauth_flows                  = ["code"]
  allowed_oauth_scopes                 = ["email", "openid", "profile"]
  allowed_oauth_flows_user_pool_client = true
  callback_urls                        = ["https://${local.cognito_domain}/oauth2/idpresponse"]
  supported_identity_providers         = ["COGNITO"]
}

最後にユーザープールドメインの設定では、managed_login_version を指定しています。

resource "aws_cognito_user_pool_domain" "main" {
  domain                = "auth-${random_id.suffix.hex}"
  user_pool_id          = aws_cognito_user_pool.main.id
  managed_login_version = 2
}

この設定はデフォルトだとバージョン1になるのですが、その場合は以下のような奥ゆかしいデフォルトのログイン画面になります。

login-image-old

ALB

ALBについてはリスナーの設定が重要です。

まず認証方法としてCognitoを指定して、オプションとしてlang = "ja"を指定しています。
これによりログインページの言語を日本語に設定できます。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-managed-login.html

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = 443
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate_validation.main.certificate_arn

  default_action {
    type = "authenticate-cognito"
    authenticate_cognito {
      user_pool_arn              = aws_cognito_user_pool.main.arn
      user_pool_client_id        = aws_cognito_user_pool_client.main.id
      user_pool_domain           = aws_cognito_user_pool_domain.main.domain
      on_unauthenticated_request = "authenticate"
      scope                      = "openid profile email"
      session_cookie_name        = "AWSELBAuthSessionCookie"
      authentication_request_extra_params = {
        lang = "ja"
      }

    }
    order = 1
  }

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg.arn
    order            = 2
  }

  tags = {
    Name = "${local.app_name}-listener"
  }

  depends_on = [aws_acm_certificate_validation.main]
}

さいごに

以上、ALB + Cognito をTerraformでまとめて作る方法でした。

個人的にCognitoって触る機会がないとまず触らないサービスの代表だと思います。
なので触ったことのない方も、これを機に触ってみてはいかがでしょうか。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.