技術検証：TerraformでVPCとALBを構成し、スティッキーセッションを設定する方法

## はじめに
AWSのスティッキーセッションは、ロードバランサーを介した通信で特定のクライアントが同じターゲット（例：EC2インスタンス）に接続され続けるようにする機能です。

:::note
**この設定は、セッションの一貫性が求められるアプリケーション（例：ショッピングカートやログインセッション）で特に有用です。**
:::

本記事では、Terraformを使用してAWSのApplication Load Balancer（ALB）にスティッキーセッションを有効にする方法をご紹介します。

## 前提整理

前回の記事では、EC2インスタンスの起動からApplication Load Balancer（ALB）の設定までを、検証目的でシンプルに構成しました。

![スクリーンショット 2024-11-30 11.37.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/42460462-2a7b-ec7e-94fb-942496abe424.png)
引用画像：https://canmakewakuwaku.com/elb_alb/

https://qiita.com/free-honda/items/0db2627c0c377b1cb9b2

## ステッキーセッションとは？
スティッキーセッション（セッションスティッキネスとも呼ばれます）は、**ロードバランサーが特定のクライアント（IPやブラウザなど）のリクエストを、同じターゲット（サーバーやインスタンス）に送信し続ける仕組み**です。

![スクリーンショット 2024-12-01 6.57.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/af037c3d-2838-a14a-c88f-c2ab6ab28a59.png)
引用画像：https://www.sunnycloud.jp/column/20210217-01/

これにより、ショッピングカートやログイン状態など、ユーザーセッション情報を保持したまま処理を継続できます。

https://docs.aws.amazon.com/ja_jp/prescriptive-guidance/latest/load-balancer-stickiness/welcome.html

:::note warn
**前回の記事でコード化した部分に、今回はステッキーセッションの設定コードを追加して技術検証を行います。**
:::

## ステッキーセッションのコード追加

スティッキーセッションは、ターゲットグループの設定で有効にできます。既存の ``aws_lb_target_group`` リソースに ``stickiness`` ブロックを追加することで設定します。

```main.tf
# ターゲットグループ作成 (EC2インスタンスをターゲットに)
resource "aws_lb_target_group" "main_target_group" {
  name     = "terraform-main-target-group"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main_vpc.id

  health_check {
    interval = 30
    path     = "/"
    protocol = "HTTP"
    timeout  = 5
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }

  # スティッキーセッション設定
  stickiness {
    type            = "lb_cookie"  # ロードバランサークッキーを使用
    enabled         = true         # 有効化
    cookie_duration = 86400        # クッキーの有効期間 (秒)
  }

  tags = {
    Name = "terraform-main-target-group"
  }
}
```

このブロックにより、ロードバランサーが生成するクッキーを使用して、クライアントが同じターゲットに接続され続けます。

## 実際に完成したコード
以下は、Terraformで作成したコードです。コードは少し長めですが、内容としてはシンプルで、理解しやすい構成になっています。

```main.tf
provider "aws" {
  region = "ap-northeast-1"  # 東京リージョンを指定
}

# VPC作成
resource "aws_vpc" "main_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "terraform-main-vpc"
  }
}

# パブリックサブネット1作成
resource "aws_subnet" "public_subnet_1" {
  vpc_id                  = aws_vpc.main_vpc.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "terraform-public-subnet-1"
  }
}

# パブリックサブネット2作成
resource "aws_subnet" "public_subnet_2" {
  vpc_id                  = aws_vpc.main_vpc.id
  cidr_block              = "10.0.2.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "terraform-public-subnet-2"
  }
}

# インターネットゲートウェイ作成
resource "aws_internet_gateway" "main_igw" {
  vpc_id = aws_vpc.main_vpc.id

  tags = {
    Name = "terraform-main-igw"
  }
}

# ルートテーブル作成 (パブリックルート)
resource "aws_route_table" "public_route_table" {
  vpc_id = aws_vpc.main_vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main_igw.id
  }

  tags = {
    Name = "terraform-public-route-table"
  }
}

# サブネット1とルートテーブルを関連付ける
resource "aws_route_table_association" "public_association_1" {
  subnet_id      = aws_subnet.public_subnet_1.id
  route_table_id = aws_route_table.public_route_table.id
}

# サブネット2とルートテーブルを関連付ける
resource "aws_route_table_association" "public_association_2" {
  subnet_id      = aws_subnet.public_subnet_2.id
  route_table_id = aws_route_table.public_route_table.id
}

# セキュリティグループ作成
resource "aws_security_group" "public_sg" {
  vpc_id = aws_vpc.main_vpc.id

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

  ingress {
    from_port   = 22
    to_port     = 22
    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 = "terraform-public-sg"
  }
}

# EC2インスタンス1作成
resource "aws_instance" "apache_ec2_1" {
  ami                         = "ami-0b6fe957a0eb4c1b9"
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.public_subnet_1.id
  security_groups             = [aws_security_group.public_sg.id]
  associate_public_ip_address = true

  user_data = <<-EOF
    #!/bin/bash
    yum update -y
    yum install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<html><body><h1>自分に負けるな〜（Aサイト）</h1></body></html>" > /var/www/html/index.html
  EOF

  depends_on = [aws_security_group.public_sg]

  tags = {
    Name = "terraform-apache-ec2-1"
  }
}

# EC2インスタンス2作成
resource "aws_instance" "apache_ec2_2" {
  ami                         = "ami-0b6fe957a0eb4c1b9"
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.public_subnet_2.id
  security_groups             = [aws_security_group.public_sg.id]
  associate_public_ip_address = true

  user_data = <<-EOF
    #!/bin/bash
    yum update -y
    yum install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<html><body><h1>自分に負けるな〜（Bサイト）</h1></body></html>" > /var/www/html/index.html
  EOF

  depends_on = [aws_security_group.public_sg]

  tags = {
    Name = "terraform-apache-ec2-2"
  }
}

# アプリケーションロードバランサー(ALB)作成
resource "aws_lb" "main_alb" {
  name               = "terraform-main-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.public_sg.id]
  subnets            = [aws_subnet.public_subnet_1.id, aws_subnet.public_subnet_2.id]

  enable_deletion_protection = false

  tags = {
    Name = "terraform-main-alb"
  }
}

# ターゲットグループ作成
resource "aws_lb_target_group" "main_target_group" {
  name     = "terraform-main-target-group"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main_vpc.id

  health_check {
    interval = 30
    path     = "/"
    protocol = "HTTP"
    timeout  = 5
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }

  # スティッキーセッション設定
  stickiness {
    type            = "lb_cookie"  # ロードバランサークッキーを使用
    enabled         = true         # 有効化
    cookie_duration = 86400        # クッキーの有効期間 (秒)
  }

  tags = {
    Name = "terraform-main-target-group"
  }
}

# ALBのリスナー作成
resource "aws_lb_listener" "http_listener" {
  load_balancer_arn = aws_lb.main_alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main_target_group.arn
  }
}

# EC2インスタンス1をターゲットグループに追加
resource "aws_lb_target_group_attachment" "ec2_attachment_1" {
  target_group_arn = aws_lb_target_group.main_target_group.arn
  target_id        = aws_instance.apache_ec2_1.id
  port             = 80
}

# EC2インスタンス2をターゲットグループに追加
resource "aws_lb_target_group_attachment" "ec2_attachment_2" {
  target_group_arn = aws_lb_target_group.main_target_group.arn
  target_id        = aws_instance.apache_ec2_2.id
  port             = 80
}
```

## 実際にやってみた

ここでは、CloudShellでの Terraform のセットアップが完了していることを前提に進めます。

**まだセットアップが完了していない方は、過去の記事を参考にしてセットアップを実施してください。**

https://qiita.com/free-honda/items/19b4e7db8b8f24d41277

まず、以下のコマンドを入力して、**main.tf**というファイルを作成します。

```bash
vi main.tf
```

``vi``では、ファイル編集後にESCキーを押し、``:wq``で保存して終了します。

```main.tf
# 上記の完成した全体のコードをコピーして貼り付けてください。
```

次に、以下のコマンドで Terraform を初期化します。

```bash
terraform init
```

実際に、以下のように「``Terraform has been successfully initialized!``」と表示され、成功が確認できました。

```bash
Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

次に、以下のコマンドを入力して、確認メッセージが表示されたら「``yes``」と入力します。

```bash
terraform apply
```

その後、以下のように「``Apply complete! Resources: xx added, 0 changed, 0 destroyed.``」と表示され、作成が成功したことが確認できました。

![スクリーンショット 2024-11-17 1.30.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/c3356544-3934-dfa9-7e6f-8f6bf29926e6.png)

## デプロイ後の動作確認

ALBの詳細画面からDNS名を取得し、ブラウザでアクセスして、リクエストが固定されているか確認します。

私の環境では、``http://terraform-main-alb-1729934941.ap-northeast-1.elb.amazonaws.com/``がロードバランサーのDNS名ですので、ブラウザで確認してみます。

![スクリーンショット 2024-12-01 7.33.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/26054e13-8285-88bd-c05c-2c18d434d80d.png)


何度かアクセスしても「``自分に負けるな〜（Aサイト）``」と表示され、ALB側でリクエストが同じターゲット（サーバーやインスタンス）に送信されていることが確認できました。

:::note
**これで、スティッキーセッションのコード化の検証は大成功です！**
:::

## まとめ
ステッキーセッションの設定は、ユーザー体験を向上させる重要な要素です。

:::note warn
**Terraformを使用することで、この設定をコードベースで管理し、再現性の高いインフラ構築を実現できます。**
:::

今回の記事で紹介した方法を活用し、スティッキーセッションが有効なロードバランシングを構築していただき、**この記事が誰かの技術の支えになれば幸いです！**

## おまけ：手動での設定

ちなみに、手動での設定も非常に簡単です。ターゲットグループの「属性」タブから維持設定をオンにし、維持設定のタイプを選択するだけです。

![スクリーンショット 2024-12-01 7.35.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/cacc6b80-0439-0bf2-a083-278e0da44bb8.png)

## 関連記事

Terraformについての記事はいくつか投稿しているので興味のあるものがあれば、読んでみてください！

https://qiita.com/free-honda/items/19b4e7db8b8f24d41277

https://qiita.com/free-honda/items/fb49c3000aaa79639265

https://qiita.com/free-honda/items/ae9b120e2a8f652ae7d2