記事を書こうと思ったきっかけ
筆者は現在、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に配置している構成です。
基本的にAPIへのアクセスはフロントエンドサービスからのみだったため、ECSをPrivateに置き、同VPC内に配置したALBからのアクセスのみを許可することでサービスを堅牢に保ちましょうというコメントでした。
そこで設計を下記に変更しました。
ポイントとしてはECSをPrivateに配置する場合は合わせてNATが必要になる点です。
ECS側から外部のネットワークに出ていく経路を確保してあげるためです。
本記事は無停止での切り替えにフォーカスしているため、NAT周りの構築については別の記事でまとめようと思います。
さて、話がそれましたが引越しが必要になった理由はサービスを堅牢に保つためです。
ですが引越しをして数日間経過したときに気づくのです。
.....NAT超高いじゃん😱
そこで本リリースまではPublicで運用しようと思い、Public→Privateに切り替えたものを再度Private→Publicに切り替えました。
ということで2つ目のお引越し理由はNATの料金が予想以上に高かったことです。
実は初めのお引越し(Public→Private)の際はサービスを停止せずに切り替える試みをしましたが、自力ではアイディアが浮かばず、実装後にメンターの方に方法を教えていただく感じでした。
なので自力で無停止で切り替えられるいい機会だと思ったのも1つの理由です。
無停止での切り替え方法
さて、前置きが長くなりましたが本題に入ります。
今回はPrivate→Publicの切り替えについてまとめますが、同様の手順で逆の切り替えも対応可能です。
初めに手順をざっくりとまとめます。
- 新しいALBターゲットグループ(以下TG)を作る
- 新しいECSサービスを作る(TGに1を紐づける)
- 旧ECSに紐づいているALBのリスナーに1のTGへリクエストが向くようなリスナールールを追加する(ダミーのドメインを指定)
- 3のドメインを正しいものにする
- 旧ECSに紐づいているリスナーの
default_action
に新しいTGを紐づける - 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_ip
、subnets
を切り替えることも忘れないようにしましょう。
// 旧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_header
、values
にダミーの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周りの知識のインプットとアウトプットをバランスよくしていけたらなと思います。
少々長い記事となりましたがお付き合いいただきありがとうございました!