【AWS】Teraformを使って、すぐに実用できるALBを作成する!

August 28, 2020

TerraformでALB(Application Load Balancing)を作成する方法を解説します。

目次

  • 環境
  • 概要
  • VPCの作成
  • セキュリティグループの作成
  • ALBのログ格納用のS3バケットを作成
  • 【ALB】ターゲットグループを作成
  • 【ALB】リスナーを作成
  • 【ALB】ALBを作成
  • Terraformの実行

環境

Terraformを使用します。ソースコードの構成は以下の通り。

|-- main.tf ←モジュールを読み込むトップのファイル。
`-- modules
    |-- vpc.tf             ←VPCを設定するファイル。
    |-- security_group.tf  ←SecurityGroupを設定するファイル。
    |-- iam.tf   ←バケットポリシーを設定するファイル。
    |-- s3.tf    ←ALBのログ格納用バケットを設定するファイル。
    |-- elb.tf   ←ALBを設定するファイル。

GitHub

また、Terraformのトップファイルは以下の通りです。共通の設定のため解説は割愛します。
【AWS】AssumeRoleしてTerraformをより安全に実行する!

./main.tf

provider "aws" {
  region = "ap-northeast-1"
  assume_role {
    role_arn = "arn:aws:iam::XXXXXXXXXX:role/SystemAdmin"
  }
}

terraform {
  required_version = "0.12.24"
  backend "s3" {
    bucket   = "tfstate.mini-schna.com"
    region   = "ap-northeast-1"
    key      = "blog/terraform-elb-basic.tfstate"
    encrypt  = true
    role_arn = "arn:aws:iam::XXXXXXXXXX:role/SystemAdmin"
  }
}

module "aws" {
  source = "./modules"
}

概要

以下の図は、今回作成する構成の全体像です。

全体像

今回のポイント!

  • インターネットと通信できるサブネット(public)、インターネットと通信できないサブネット(private)を用意。
  • セキュリティの観点から、ECSへはALB経由でしかアクセスできないようにpublicサブネットに配置する。(インターネットからECSに直接アクセスすることをできなくする。
  • ALBのログをS3バケットに格納されるようにする。

※今回はALBの解説になるため、振り分け先であるECSを作成するコードは記載しておりません。


VPCの作成

ALBを配置するVPCを作成します。今回のメインはALBのため、詳細な解説は省略します。

./modules/vpc.tf

variable "vpc_cidr_block" {
  default = "150.0.0.0/16"
}
variable "subnet_cidr_block_public_a" {
  default = "150.0.1.0/24"
}
variable "subnet_cidr_block_public_c" {
  default = "150.0.2.0/24"
}
variable "subnet_cidr_block_private_web_a" {
  default = "150.0.10.0/24"
}
variable "subnet_cidr_block_private_web_c" {
  default = "150.0.20.0/24"
}
variable "az_a" {
  default = "ap-northeast-1a"
}
variable "az_c" {
  default = "ap-northeast-1c"
}

##################################################
# vpc
##################################################
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
}

##################################################
# Internet gateway
##################################################
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
}

##################################################
# public subnet
##################################################
resource "aws_subnet" "public_a" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.subnet_cidr_block_public_a
  availability_zone = var.az_a
}

resource "aws_subnet" "public_c" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.subnet_cidr_block_public_c
  availability_zone = var.az_c
}

resource "aws_route_table" "public_a" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table" "public_c" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table_association" "public_a" {
  subnet_id      = aws_subnet.public_a.id
  route_table_id = aws_route_table.public_a.id
}

resource "aws_route_table_association" "public_c" {
  subnet_id      = aws_subnet.public_c.id
  route_table_id = aws_route_table.public_c.id
}

# public_aがインターネットと通信するためのルーティング。
resource "aws_route" "to_internet_from_public_a" {
  route_table_id         = aws_route_table.public_a.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

# public_aがインターネットと通信するためのルーティング。
resource "aws_route" "to_internet_from_public_c" {
  route_table_id         = aws_route_table.public_c.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

##################################################
# private subnet (web)
##################################################
resource "aws_subnet" "private_web_a" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.subnet_cidr_block_private_web_a
  availability_zone = var.az_a
}

resource "aws_subnet" "private_web_c" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.subnet_cidr_block_private_web_c
  availability_zone = var.az_c
}

resource "aws_route_table" "private_web_a" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table" "private_web_c" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table_association" "private_web_a" {
  subnet_id      = aws_subnet.private_web_a.id
  route_table_id = aws_route_table.private_web_a.id
}

resource "aws_route_table_association" "private_web_c" {
  subnet_id      = aws_subnet.private_web_c.id
  route_table_id = aws_route_table.private_web_c.id
}

上述のコードでは、ざっくりと以下のようなリソースを作成しています。

・インターネットゲートウェイ

・パブリックサブネット
インターネットと通信できるサブネット。ここにALBを配置していきます。

・プライベートサブネット
インターネットと通信できないサブネット。ALBの振り分け先であるWebサーバを配置していきます。


セキュリティグループの作成

ALBとWebサーバ用のセキュリティグループを作成します。

今回、許可が必要な通信を図にしました。

セキュリティグループ

・インターネット→ALB
・ALB→ECS
この2パターンを作成していきます。

./moudles/security_group.tf

##################################################
# security group (ALB)
##################################################
resource "aws_security_group" "alb" {
  name                   = "alb"
  vpc_id                 = aws_vpc.main.id
  revoke_rules_on_delete = false
}

# インターネットからALBに対するHTTP通信(ポート80)を許可するSG
resource "aws_security_group_rule" "alb_permit_from_internet_http" {
  security_group_id = aws_security_group.alb.id
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "permit from internet for http."
  type              = "ingress"
  protocol          = "tcp"
  from_port         = "80"
  to_port           = "80"
}

# インターネットからALBに対するHTTPS通信(ポート80)を許可するSG
resource "aws_security_group_rule" "alb_permit_from_internet_https" {
  security_group_id = aws_security_group.alb.id
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "permit from internet for https."
  type              = "ingress"
  protocol          = "tcp"
  from_port         = "443"
  to_port           = "443"
}

##################################################
# security group (web)
##################################################
resource "aws_security_group" "web" {
  name                   = "web"
  vpc_id                 = aws_vpc.main.id
  revoke_rules_on_delete = false
}

# ALBからのHTTP通信(ポート80)を許可するSG
resource "aws_security_group_rule" "web_permit_from_alb" {
  security_group_id        = aws_security_group.web.id
  source_security_group_id = aws_security_group.alb.id
  description              = "permit from alb."
  type                     = "ingress"
  protocol                 = "tcp"
  from_port                = "80"
  to_port                  = "80"
}

ALBのログ格納用のS3バケットを作成

ALBのアクセスログを格納するためのS3バケットを作成します。

【所要時間10分】Terraformを使って、S3でWebサイトを公開する!

バケットポリシー

ALBがS3にログを格納できるようにポリシーを作成していきます。

./modules/iam.tf

# ALBアカウントIDを取得するために使用
data "aws_elb_service_account" "main" {}

###############################################
# ログ格納用バケットポリシー
###############################################
data "aws_iam_policy_document" "logging_bucket" {
  statement {
    sid    = ""
    effect = "Allow"

    principals {
      identifiers = [
        data.aws_elb_service_account.main.arn
      ]
      type        = "AWS"
    }

    actions = [
      "s3:PutObject"
    ]

    resources = [
      "arn:aws:s3:::logging.mini-schna.com",
      "arn:aws:s3:::logging.mini-schna.com/*"
    ]
  }
}

ポイントは、principalsidentifiers に設定する値。

ALBのアクセスログをS3に格納するためには、バケットポリシーにてリージョン毎に設定されているELBアカウントを許可する必要があります。(このELBアカウントは、自分のアカウントとは違うので注意!)

Terraformでは、このELBアカウントIDを取得するためのデータソースが用意されています。

data "aws_elb_service_account" "main" {}

こちらを使用して、principals を設定します。

principals {
  identifiers = [
    data.aws_elb_service_account.main.arn
  ]
  type        = "AWS"
}

S3バケット

CloudFrontのアクセスログを格納するS3バケットの設定と同じため、より詳しい解説を知りたい方は以下の記事をご参照ください。
【AWS】Terraformを使って、CloudFrontを構築する!

./modules/s3.tf

###############################################
# ログ格納用バケット
###############################################
resource "aws_s3_bucket" "logging" {
  bucket = "logging.mini-schna.com"
  policy = data.aws_iam_policy_document.logging_bucket.json ## iam.tfで設定したポリシーを使用。
  force_destroy = true
  versioning {
    enabled    = true
    mfa_delete = false
  }

  ## オブジェクトのライフサイクル設定。
  lifecycle_rule {
    id      = "assets"
    enabled = true

    ## オブジェクトの保存期限。
    expiration {
      days = "365" ## 1年
    }

    ## 現在のオブジェクトの移行設定。
    transition {
      ## オブジェクトが作成されてから移行するまでの日数。
      days          = "93" ## 3ヶ月
      storage_class = "STANDARD_IA"
    }

    ## オブジェクトの以前のバージョンの保存期限。
    noncurrent_version_expiration {
      days = "1095" ## 3年
    }

    ## 古いのオブジェクトの移行設定。
    noncurrent_version_transition {
      ## オブジェクトが古いバージョンになってから移行するまでの日数。
      days          = "365" ## 1年
      storage_class = "GLACIER"
    }
  }

  request_payer = "BucketOwner"
}

# S3 Public Access Block
## パブリックアクセスはしないため全て有効にする。
resource "aws_s3_bucket_public_access_block" "logging" {
  bucket                  = aws_s3_bucket.logging.bucket
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

【ALB】ターゲットグループを作成

ALBの振り分け先であるターゲットグループを作成します。

./modules/elb.tf

##################################################
# Target group
##################################################
resource "aws_lb_target_group" "mini_schna_com" {
  name        = "mini-schna-com"
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = aws_vpc.main.id

  # 登録解除を実行するまでの待機時間。
  deregistration_delay = 300 # 処理中のリクエストが完了するのを待つためにデフォルト値を採用。

  # 登録された後にリクエストを開始する猶予時間
  slow_start = 0 # 登録されたらすぐに開始してよいので無効。

  load_balancing_algorithm_type = "round_robin" # ラウンドロビンで平均的にリクエストを分散。

  stickiness {
    type            = "lb_cookie"
    cookie_duration = 86400 # 要件が決まっていないのでとりあえず1日を設定。
    enabled         = true
  }

  health_check {
    enabled             = true
    interval            = 30
    path                = "/"
    port                = "traffic-port" # トラフィックを受信するポートを使用。デフォルト。
    protocol            = "HTTP"
    timeout             = 5
    healthy_threshold   = 3
    unhealthy_threshold = 3
    matcher             = "200-299"
  }
}

コメントで解説しきれていない部分を補足していきます。

target_type = "ip"

ターゲットの種類を設定します。設定可能な値は以下の通り。
instance:インスタンス
ip:IPアドレス
lambda:Lambda関数
今回はターゲットにECSを使用する予定なので、ip を指定しています。


stickiness {
  type            = "lb_cookie"
  cookie_duration = 86400 # 要件が決まっていないのでとりあえず1日を設定。
  enabled         = true
}

スティッキーセッションの設定。ALBでCookieに「AWSALB」を自動で設定して、同じターゲットに振り分けを行うことができる機能。

スティッキーセッション


【ALB】リスナーを作成

リスナー(接続をチェックする機能)を作成します。

./modules/elb.tf

variable "acm_certificate_arn" {
  default = "arn:aws:acm:ap-northeast-1:XXXXXXXXXXXX:certificate/XXXXXXXXXXXXXXXXXXX"
}

...

##################################################
# Listener
##################################################
# HTTPS通信のためのリスナー
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.mini_schna_com.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = var.acm_certificate_arn
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.mini_schna_com.arn
  }
}

# HTTPをHTTPSにリダイレクトするためのリスナー
resource "aws_lb_listener" "http_redirect_to_https" {
  load_balancer_arn = aws_lb.mini_schna_com.arn
  port              = 80
  protocol          = "HTTP"
  default_action {
    type = "redirect"
    redirect {
      port        = 443
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

それでは解説します。

作成するリスナーは2種類。
1つ目は、HTTPS通信のためのリスナー。「https://〜」にアクセスされた時、その通信をターゲットグループに転送するようにルールを設定しています。また、SSL証明書は、ACMで作成したもの指定しています。

2つ目は、HTTP通信のためのリスナー。「http://~」にアクセスされた時、「https://~」にリダイレクトするようルールを設定しています。

default_action {
  type = "redirect"
  redirect {
    port        = 443
    protocol    = "HTTPS"
    status_code = "HTTP_301"
  }
}

【ALB】ALBを作成

ALBを作成します。

./modules/elb.tf

##################################################
# LB
##################################################
resource "aws_lb" "mini_schna_com" {
  name               = "mini-schna-com"
  internal           = false # 内部で使用しないため無効。
  load_balancer_type = "application"

  security_groups = [
    aws_security_group.alb.id
  ]

  access_logs {
    bucket  = aws_s3_bucket.logging.bucket
    prefix  = "elb"
    enabled = true
  }

  subnets = [
    aws_subnet.public_a.id,
    aws_subnet.public_c.id
  ]

  idle_timeout               = 60    # デフォルトの60秒を設定。
  enable_deletion_protection = false # Terraformで削除したいため無効。
  enable_http2               = true
  ip_address_type            = "ipv4" # ipv6は使用しないためipv4を指定。
}

それでは解説します。

load_balancer_type = "application"

ALBを使用するため、application を設定します。


idle_timeout = 60

①ALBからターゲットにリクエストを転送する。
②ターゲットで処理を行いALBに通信を返す。
この①と②に掛かる時間の設定になります。①と②が idle_timeout の時間を超えた場合、ALBはエラーをクライアントに返します。今回はデフォルト値の60sを設定しました。適宜、アプリによって調整が必要です。


Terraformの実行

それでは作成したコードを元にTerraformを実行して、ALBを作成していきます。

$ cd /infra/terraform/20200825_terraform-elb-basic
$ terraform init
$ terraform apply

※Dockerコンテナ内での実行を前提にしています。


以上で、ALBの作成が完了です。

最後までご覧頂きありがとうございました。