1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Terraform + AWS 〜ECSを無停止で切り替えるPublic ↔︎ Private〜

Last updated at Posted at 2023-01-24

記事を書こうと思ったきっかけ

筆者は現在、fjord bootcamp(以下FBC)にてテイスティングノートという自作サービスの開発をしているのですが、
インフラの構築にTerraformを使用しており、ECSを停止せずにサブネットを切り替えるという貴重な経験をしたので備忘も兼ねて記事にまとめようと思ったのがきっかけです。

Terraformを使用することで効率的且つサービスをStopせずに切り替得られることに大きな魅力とインフラの楽しさを感じました。

拙い説明もありますがお付き合い願います。

※ Terraform、AWSの基礎に関する記事ではありませんのでご注意ください。

対象読者

  • TerraformでECSを管理している方
  • ECSを停めずにお引越し(サブネットの切り替え)したいと思っている方

筆者が引っ越しに迫られた理由

FBCでは嬉しいことにメンター(現役エンジニア)の方々に自作サービスのコードレビューをしていただけます。
TerraformでECSを初めて構築したときのPRにこんなコメントをいただきました。

ECSサービスがpublic subnetsで動いているのが少し気になりました。
ECSサービスをprivateにおいて、publicにはALBだけを置く方がセキュリティ的に堅牢なため。

PublicとPrivateの大きな違いは下記です。

Public Subnet
インターネット上のどこからでもアクセス可能
Private Subnet
同じVPC内のIPアドレスからのみアクセス可能

今回、自分が開発をしているサービスはフロントエンドにReact、バックエンドにRailsのAPIモードを採用しており、RailsサービスをECSに配置している構成です。

image.png

基本的にAPIへのアクセスはフロントエンドサービスからのみだったため、ECSをPrivateに置き、同VPC内に配置したALBからのアクセスのみを許可することでサービスを堅牢に保ちましょうというコメントでした。

そこで設計を下記に変更しました。

Image

ポイントとしてはECSをPrivateに配置する場合は合わせてNATが必要になる点です。
ECS側から外部のネットワークに出ていく経路を確保してあげるためです。
本記事は無停止での切り替えにフォーカスしているため、NAT周りの構築については別の記事でまとめようと思います。

さて、話がそれましたが引越しが必要になった理由はサービスを堅牢に保つためです。

ですが引越しをして数日間経過したときに気づくのです。

.....NAT超高いじゃん😱
そこで本リリースまではPublicで運用しようと思い、Public→Privateに切り替えたものを再度Private→Publicに切り替えました。
ということで2つ目のお引越し理由はNATの料金が予想以上に高かったことです。

実は初めのお引越し(Public→Private)の際はサービスを停止せずに切り替える試みをしましたが、自力ではアイディアが浮かばず、実装後にメンターの方に方法を教えていただく感じでした。
なので自力で無停止で切り替えられるいい機会だと思ったのも1つの理由です。

無停止での切り替え方法

さて、前置きが長くなりましたが本題に入ります。
今回はPrivate→Publicの切り替えについてまとめますが、同様の手順で逆の切り替えも対応可能です。

初めに手順をざっくりとまとめます。

  1. 新しいALBターゲットグループ(以下TG)を作る
  2. 新しいECSサービスを作る(TGに1を紐づける)
  3. 旧ECSに紐づいているALBのリスナーに1のTGへリクエストが向くようなリスナールールを追加する(ダミーのドメインを指定)
  4. 3のドメインを正しいものにする
  5. 旧ECSに紐づいているリスナーのdefault_actionに新しいTGを紐づける
  6. 5と同じタイミングで旧ECSと旧TG、3を削除する

一度のterraform applyでは不可能なので複数回実行することで実現します。
それではそれぞれについて細かい部分の解説をしていきます。

1. 新しいALBターゲットグループを作る

新しいECSに紐づけるターゲットグループです。
基本的には旧ECSで使用しているターゲットグループと同じ内容で問題ありません。

nameとtags内のnameのみ変更しています。

// 旧
resource "aws_alb_target_group" "ecs_target_group" {
  name        = "${var.project}-ecs-tg"
  port        = 3000
  protocol    = "HTTP"
  vpc_id      = aws_vpc.vpc.id
  target_type = "ip"
  health_check {
    healthy_threshold   = 5
    path                = "/api/v1/health_check"
    unhealthy_threshold = 2
    protocol            = "HTTP"
    port                = 3000
  }

  tags = {
    Name = "${var.project}-ecs-tg"
  }
}

// 新
resource "aws_alb_target_group" "ecs_target_group_v2" {
  name        = "${var.project}-ecs-tg-v2"
  port        = 3000
  protocol    = "HTTP"
  vpc_id      = aws_vpc.vpc.id
  target_type = "ip"
  health_check {
    healthy_threshold   = 5
    path                = "/api/v1/health_check"
    unhealthy_threshold = 2
    protocol            = "HTTP"
    port                = 3000
  }

  tags = {
    Name = "${var.project}-ecs-tg-v2"
  }
}

2. 新しいECSサービスを作る

1で作成したターゲットグループを紐づけたECSを作成します。
また、サブネットタイプがパブリック↔︎プライベートのように変更になる場合はnetwork_configuration内のassign_public_ipsubnetsを切り替えることも忘れないようにしましょう。

// 旧ECS
resource "aws_ecs_service" "tasting_note" {
  name                              = "${var.project}-service"
  cluster                           = aws_ecs_cluster.tasting_note.arn
  task_definition                   = aws_ecs_task_definition.rails_task.arn
  desired_count                     = 2
  launch_type                       = "FARGATE"
  platform_version                  = "1.4.0"
  health_check_grace_period_seconds = 60

  network_configuration {
    assign_public_ip = false
    security_groups  = [aws_security_group.ecs_sg.id]
    subnets          = [for subnet in aws_subnet.private : subnet.id]
  }

  load_balancer {
    target_group_arn = aws_alb_target_group.ecs_target_group.arn
    container_name   = "${var.project}-container"
    container_port   = 3000
  }

  lifecycle {
    ignore_changes = [
      task_definition
    ]
  }
}

// 新ECS
resource "aws_ecs_service" "tasting_note_v2" {
  name                              = "${var.project}-service-v2"
  cluster                           = aws_ecs_cluster.tasting_note.arn
  task_definition                   = aws_ecs_task_definition.rails_task.arn
  desired_count                     = 2
  launch_type                       = "FARGATE"
  platform_version                  = "1.4.0"
  health_check_grace_period_seconds = 60

  network_configuration {
    // publicの場合true
    assign_public_ip = true
    security_groups  = [aws_security_group.ecs_sg.id]
    subnets          = [for subnet in aws_subnet.public : subnet.id]
  }

  load_balancer {
    // 1で作成したecs_target_group_v2.arnを指定
    target_group_arn = aws_alb_target_group.ecs_target_group_v2.arn
    container_name   = "${var.project}-container"
    container_port   = 3000
  }

  lifecycle {
    ignore_changes = [
      task_definition
    ]
  }
}

3. 新ターゲットグループにリクエストが向くようなリスナールールを作成する

今回のお引っ越しにおけるポイントの1つです。

リスナールールを追加する理由としては1で作成した新ターゲットグループへのルーティングが設定されていないとECSの起動に失敗するためです。

ですが旧ECSを停止するまでは実際のリクエストが届かないようにしたいので、condition内のhost_headervaluesにダミーのURLを与えることで実際にリクエストがあってもルーティングされないようにします。

resource "aws_lb_listener_rule" "v2" {
  // 旧target_groupに紐づけているhttpsリスナー
  listener_arn = aws_lb_listener.https.arn

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

  condition {
    host_header {
      // ダミードメインを指定することでリクエストがルーティングされないようにする
      values = ["example.com"]
    }
  }
}

ここまで行ったら一旦terraform applyします。

4. 3で作ったリスナールールに正しいドメインを指定する

ドメインを変更したらterraform applyします。
この時点で新ECSにリクエストが届くようになります。

5. 旧ECSに紐づいているリスナーのdefault_actionに新しいターゲットグループを紐づける

ここまで特に説明はしてきませんでしたが、旧ターゲットグループにはhttpsリスナーとhttpsへリダイレクトさせるhttpリスナーを用意しています。

こちらの手順では、このhttpsリスナーのdefault_actionで紐付けされているターゲットグループを新ターゲットグループに変更します。

// 変更前
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"
  certificate_arn   = aws_acm_certificate.tokyo.arn
  default_action {
    type             = "forward"
    target_group_arn = aws_alb_target_group.ecs_target_group.arn
  }
}

// 変更後
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"
  certificate_arn   = aws_acm_certificate.tokyo.arn
  default_action {
    type             = "forward"
    // ecs_target_group_v2.arnに変更
    target_group_arn = aws_alb_target_group.ecs_target_group_v2.arn
  }
}

6. 5と同じタイミングで旧ECSと旧ターゲットグループ、3を削除する

最後に不要なリソースをお掃除して終了です。

以上が大まかな手順となります。

Public→Privateの場合はECS作成時のassign_public_ipがfalseになり、subnetsにprivateサブネットを指定すればOKです。

この場合はNATの作成も発生するのでPrivate→Publicよりも手順が増えますが、これについては別の記事でまとめようと思います。

まとめ

タスクを行ってみて、率直な感想としては「すごい楽ちん」です。
また、時間をかけず効率的に実装ができたので、Terraformでインフラを管理することのメリットを体感できたのも良かったかなと思います。

個人的にインフラ周りの学習は楽しいので、今後もTerraformやAWS周りの知識のインプットとアウトプットをバランスよくしていけたらなと思います。

少々長い記事となりましたがお付き合いいただきありがとうございました!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?