プログラミング学習サイト

プログラミングの学習を開始される方を対象としたプログラミング入門サイトです。

Terraformで複数台のEC2を起動し負荷分散する【Terraform】

一台のサーバを動かすのははじめの一歩としては良いですが、現実的には一台のサーバは単一障害点となり得ます。

単一障害点とは、簡単に言えばサービスが停止し利用不可になる弱点のこと。 これを防ぐためには複数のサーバーで負荷分散をするのが必須となりますがAWSのASG(オートスケーリンググループ)サービスを使うことで EC2のクラスタ管理を行なってくれます。 ECS Fargateなどのコンテナをしようしてスケーリングする手法もありますが、今回はEC2のスケーリングASGを使用します。

ASGをTerraformから使用する場合は、aws_autoscaling_groupリソースを使用します。 これは単一のEC2インスタンスを表すリソースaws_instanceと似ていますが、min_sizemax_sizeのパラメーターに値を入れることでその値の範囲でオートスケーリングしてくれます。

また、それらスケーリンググループの起動設定リソースとして、aws_launch_configurationリソースをしようします。

resource "aws_launch_configuration" "example" {
  image_id        = "ami-0fb653ca2d3203ac1"
  instance_type   = "t2.micro"
  security_groups = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p ${var.server_port} &
              EOF

  # Required when using a launch configuration with an auto scaling group.
  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_autoscaling_group" "example" {
  launch_configuration = aws_launch_configuration.example.name
  vpc_zone_identifier  = data.aws_subnets.default.ids

  target_group_arns = [aws_lb_target_group.asg.arn]
  health_check_type = "ELB"

  min_size = 2
  max_size = 10

  tag {
    key                 = "Name"
    value               = "terraform-asg-example"
    propagate_at_launch = true
  }
}

from https://github.com/brikis98/terraform-up-and-running-code/blob/master/code/terraform/02-intro-to-terraform-syntax/webserver-cluster/main.tf

  • 上記の例では、EC2インスタンスは2~10の間でスケーリングします。
  • launch_configuration = aws_launch_configuration.example.nameという設定を使用することで、aws_autoscaling_groupaws_launch_configurationを利用することがわかります。
  • また、vpc_zone_identifier = data.aws_subnets.default.idsでは、デフォルトのVPCを採用したサブネットidを指定しております。 ここでは、AWSがデフォルトで用意してくれているサービスに接続するためにデータソースという構文をしようしてサービスに接続します。
data "aws_vpc" "default" {
  default = true
}

data "aws_subnets" "default" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
}

ロードバランサーで負荷分散

ここまでで複数のサーバーを立ち上げる準備が整いましたが、エンドユーザーが全てのIPアドレスを把握するというのは大変なことです。 これらのサーバーにトラフィックを分散するロードバランサをデプロイし、エンドユーザーにはこのIPへアクセスさせるのが良いでしょう。

AWSではElastic Load Balancingというサービスがロードバランサーの役割を担ってくれます。 ELBには主に2種類あります。

  • Application Load Balancer (ALB)
  • Network Load Balancer (NLB)
    • TCP, UDPなどのレイヤーのロードバランシングが可能であり、OSI参照モデルでも比較的低い層に一します

他にもClassic Load Balancerが存在しますが、比較的機能が少ない&古いため省きます。

今回のケースではWebトラフィックを使用するためHTTPSを扱うALBを採用します。

Listener, Target Group

ロードバランサーは一つのAWSリソースから構成されているわけではありません。 リスナ、ターゲットグループ、リスナールールから構成されます。

  • リスナ
    • 特定のポート,特定のプロトコルでリッスンすることの設定。ユーザーからのリクエストの窓口
  • ターゲットグループ
    • ロードバランサからリクエストを渡すサーバ群の設定。加えて、サーバーに対するヘルスチェックも行い、正常なノードに対してリクエストを送ります。
  • リスナルール
    • リスナーに対するアクセスを受け取り、特定のパスやホスト名に一致したリクエストを指定したターゲットグループに送ります。

ALB本体

まずはALB本体を作成する必要があります。

resource "aws_lb" "example" {

  name               = var.alb_name

  load_balancer_type = "application"
  subnets            = data.aws_subnets.default.ids
  security_groups    = [aws_security_group.alb.id]
}

load_balancer_typeの引数にapplicationを指定することで、ALBの作成が可能です。

上記の例では、EC2とロードバランサが同じサブネットグループに所属していますが、本来はEC2はプライベートサブネット、ALBはパブリックサブネットに配置し、 ユーザーが直接EC2にアクセスできない用にした方がいいでしょう。

リスナ

次に、このALBに登録するリスナを作成します。

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.example.arn
  port              = 80
  protocol          = "HTTP"

  # By default, return a simple 404 page
  default_action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/plain"
      message_body = "404: page not found"
      status_code  = 404
    }
  }
}

このリスナは80番ポートのHTTPでリッスンし、条件に一致しない場合は404 page not foundを返すようにしています。

ターゲットグループ

resource "aws_lb_target_group" "asg" {

  name = var.alb_name

  port     = var.server_port
  protocol = "HTTP"
  vpc_id   = data.aws_vpc.default.id

  health_check {
    path                = "/"
    protocol            = "HTTP"
    matcher             = "200"
    interval            = 15
    timeout             = 3
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }
}

このターゲットグループは、インスタンスに対して定期的にHTTPリクエストを送ることでインスタンスがhealthyであることを確認します。 インスタンスがダウンしている時はunhealthyと判断され、他のインスタンストラフィックが分散される仕組みです。

このあとは、aws_autoscaling_groupの属性target_group_arnsに上記のターゲットグループを指定することで、どのEC2をターゲットグループにするかを伝えることができます。 このように、ユーザーから見てロードバランサーよりも奥の層では、オートスケーリンググループとターゲットグループが密な結合になっているため、この部分でEC2を指定できるという状況です

resource "aws_autoscaling_group" "example" {
  launch_configuration = aws_launch_configuration.example.name
  vpc_zone_identifier  = data.aws_subnets.default.ids

  target_group_arns = [aws_lb_target_group.asg.arn]
  health_check_type = "ELB"

  min_size = 2
  max_size = 10

  tag {
    key                 = "Name"
    value               = "terraform-asg-example"
    propagate_at_launch = true
  }
}

りすなルール

resource "aws_lb_listener_rule" "asg" {
  listener_arn = aws_lb_listener.http.arn
  priority     = 100

  condition {
    path_pattern {
      values = ["*"]
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.asg.arn
  }
}

最後にリスナルールを作成し、ここまで見てきたものを一つにまとめましょう。 リスナールールの役割は、リスナーから受け取った情報を、特定のターゲットグループに渡すことでした。

上記のコードは、パスが一致するリクエストをASGが含まれるターゲットグループに送るリスナルールを追加します。

page:https://minegishirei.hatenablog.com/entry/2024/06/22/073328