ECSとNLBで遊ぼうと思ったのにTerragruntとOpenTofuでインフラをセットアップして満足した

タイトルのとおりです。楽しかった。

以前にECSで遊んだ記事2つ

bufferings.hatenablog.com

bufferings.hatenablog.com

1つ目の記事では、NLB有無でかかった時間を比べたのだけど

  • NLBあり + Service Connectあり
  • NLBなし + Service Connectあり

と、サービスコネクトをどちらも有効にした状態で比べてしまっていた。じゃあ、と2つ目の記事では、

  • NLBなし + Service Connectあり
  • NLBなし + Service Connectなし

で比べていた。そして、比べるべきだったのはこれだったなぁ、またこんどやろう、と思ったのがこの組み合わせ。

  • NLBあり + Service Connectなし
  • NLBなし + Service Connectあり

今日は、これを見てみようと思う。(終わった後の僕より「見てないよ!」)

Terragrunt + OpenTofu

前回は、手でポチポチやったので、今回はTerragruntを使うことにする。せっかくだからOpenTofuを使ってみるか。Terragruntは、Terraformのちょっとかゆいところをかいてくれるやつ。OpenTofuはTerraformのライセンス変更起因でフォークされたやつ。brewで入れといた。

❯ brew install terragrunt

❯ terragrunt -v
terragrunt version 0.54.20

❯ brew install opentofu

❯ tofu -v
OpenTofu v1.6.0
on darwin_arm64

OpenTofuは今はTerraformとほぼ同じだけど、今後Terraformとは別の道を進むことになると思うから、どちらにどんな機能が入っていくか・周辺ツールがどんな対応をしていくか、をよく見ておきたいなと思っている。どちらも2.0くらいになったときが考えるポイントになりそう。今のところはTerraformを使い続けるでいいんじゃないかなと思ってる。

プロジェクトフォルダを作る

適当に作った

❯ mkdir -p ecs20240121/infra
❯ cd ecs20240121/infra

Terragruntの設定とは別で、ecspresso用の設定も作るだろうなと思ったので、Terragrunt用にはinfraフォルダを作って使うことにした。

Terragruntで必要なので、ここに空の terragrunt.hcl ファイルを作っておく。

❯ touch terragrunt.hcl

僕のマシンにはTerraformもインストールされているので、初期状態ではTerragruntはTerraformを使うようになっている。

❯ terragrunt terragrunt-info | grep TerraformBinary
  "TerraformBinary": "terraform",

ので、OpenTofuを使うように設定する。

TerragruntからOpenTofuを使う設定

Terragrunt 0.52.0のリリースノートにこう書いてあるので、Option 2の環境変数を使おうかな。

https://github.com/gruntwork-io/terragrunt/releases/tag/v0.52.0

  • Option 1: Remove terraform binary from PATH
  • Option 2: Define env variable TERRAGRUNT_TFPATH=tofu
  • Option 3: When launching terragrunt, specify --terragrunt-tfpath tofu

環境変数の切り替えにはdirenvを使うか。と思ったら入ってなかったのでインストール。よりみち楽しい。

direnvをインストール

❯ brew install direnv

zshを使っているので下記の内容を ~/.zshrc に追記した

eval "$(direnv hook zsh)"

追加した設定を読み込んでおく

source ~/.zshrc

TerragruntからOpenTofuを使う設定に戻ってきた

infraディレクトリに入ったら TERRAGRUNT_TFPATH=tofu が有効になるようにしたいので .envrc を作る

echo 'export TERRAGRUNT_TFPATH=tofu' > .envrc
direnv: error <>/ecs20240121/infra/.envrc is blocked. Run `direnv allow` to approve its content

と言われるので許可しておく

❯ direnv allow
direnv: loading <>/ecs20240121/infra/.envrc
direnv: export +TERRAGRUNT_TFPATH

これで、このフォルダに入ったらOpenTofuが使われるようになった。わーい。

❯ terragrunt terragrunt-info | grep TerraformBinary
  "TerraformBinary": "tofu",

この記事ではクレデンシャルを入れるつもりはないから気にしなくていいんだけど、一応、安全のために .envrc.gitignore に入れておこうか。

❯ cp .envrc example.envrc
❯ echo '.envrc' > ../.gitignore

できた!・・・って、満足してこの記事を終わりにしようと思ってしまった。何をやろうとしてたんだっけ。ECSか。でも、その前にリモートステートの設定入れておくか。よりみち楽しい!

S3バックエンド

ステートを管理するのにS3を使いたいんだけど、このS3のバケット自体をTerraform(というかOpenTofuだけど)で管理するのは少しめんどくさい。そこをTerragruntがいい感じにやってくれるので、その設定を入れる。

さっき空で作成した terragrunt.hcl をこんな風にする。

remote_state {
  backend = "s3"
  config = {
    bucket  = "ecs20240121"
    key     = "${path_relative_to_include()}/terraform.tfstate"
    encrypt = true
    region  = "ap-northeast-1"
  }
}

dynamodb_table でステートをロックする設定もあるけど、今回はそこまでは入れなかった。

参照:https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#remote_state

で、このTerragruntで作ったステートをOpenTofuから使うようにするために main.tf を用意してこんな感じに設定を書いておく。

terraform {
  backend "s3" {}
}

これで terragrunt init を実行すると、S3バケットないけど作る?って聞かれるので作るって答える。そうそう、僕の環境では、AWSには接続できるように環境変数を設定済み。

❯ terragrunt init
Remote state S3 bucket ecs20240121 does not exist or you don't have permissions to access it. Would you like Terragrunt to create it? (y/n) y

Initializing the backend...

Successfully configured the backend "s3"! OpenTofu will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

OpenTofu has been successfully initialized!

作られてた。わいわい。

AWS Providerを入れる

じゃAWS Provider入れるか。main.tf をこんな風にして。

terraform {
  backend "s3" {}
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

からの terragrunt init

❯ terragrunt init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.33.0...
- Installed hashicorp/aws v5.33.0 (signed, key ID 0C0AF313E5FD9F80)

.terraform.gitignore に追加しとこ。忘れる前に。

echo '.terraform/' >> ../.gitignore

インフラを作る

これで、だいたい準備ができたので、適当にNLBやECSを作ってこ。ファイルを分けることが多いかなとは思うけど今回は趣味だし、全部 main.tf に書く、でいいか。

適当に名前を用意

あると便利なのでローカル変数を用意。なんでもいい。

locals {
  main_name = "ecs20240121"
}

VPCの設定

VPCとPublic Subnetを用意する。これも普通だったらPrivate Subnetを用意したり、マルチAZにしたりすると思うけど今回は1つで全部やることにする。

#########################################
# VPC
#########################################

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "main" {
  vpc_id = aws_vpc.main.id

  availability_zone = "ap-northeast-1a"
  cidr_block        = "10.0.1.0/24"
}

#########################################
# Internet Gateway
#########################################

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}

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

resource "aws_route" "main" {
  route_table_id         = aws_route_table.main.id
  gateway_id             = aws_internet_gateway.main.id
  destination_cidr_block = "0.0.0.0/0"
}

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

Security Groupを用意

NLBにもSGが設定できるようになったのでNLB用と、APP用で用意。ポートは80番だけあけてたらいっかという気持ち。

#########################################
# Security Groups
#########################################

resource "aws_security_group" "nlb" {
  name   = "${local.main_name}-sg-nlb"
  vpc_id = aws_vpc.main.id
}

resource "aws_security_group" "app" {
  name   = "${local.main_name}-sg-app"
  vpc_id = aws_vpc.main.id
}

resource "aws_vpc_security_group_ingress_rule" "nlb_ingress" {
  security_group_id = aws_security_group.nlb.id

  cidr_ipv4   = "0.0.0.0/0"
  ip_protocol = "tcp"
  from_port   = 80
  to_port     = 80
}

resource "aws_vpc_security_group_egress_rule" "nlb_egress" {
  security_group_id = aws_security_group.nlb.id

  referenced_security_group_id = aws_security_group.app.id
  ip_protocol                  = "-1"
}

resource "aws_vpc_security_group_ingress_rule" "app_ingress_nlb" {
  security_group_id = aws_security_group.app.id

  referenced_security_group_id = aws_security_group.nlb.id
  ip_protocol                  = "tcp"
  from_port                    = 80
  to_port                      = 80
}

resource "aws_vpc_security_group_ingress_rule" "app_ingress_self" {
  security_group_id = aws_security_group.app.id

  referenced_security_group_id = aws_security_group.app.id
  ip_protocol                  = "tcp"
  from_port                    = 80
  to_port                      = 80
}

resource "aws_vpc_security_group_egress_rule" "app_egress" {
  security_group_id = aws_security_group.app.id

  cidr_ipv4   = "0.0.0.0/0"
  ip_protocol = "-1"
}

NLBの設定

ターゲットグループは、デフォルトだと待ち時間が長いので、こんな感じにしてみた。あとで変えるかもしれない。

  • deregistration_delay: 30s
  • health_check: 10s x 2

それと、ECSではFARGATEを使うつもりなので、ターゲットグループの target_typeip にしておいた。

#########################################
# NLB
#########################################

resource "aws_lb" "main" {
  name               = "${local.main_name}-nlb"
  internal           = false
  load_balancer_type = "network"
  security_groups    = [aws_security_group.nlb.id]
  subnets            = [aws_subnet.main.id]
}

resource "aws_lb_target_group" "main" {
  name                 = "${local.main_name}-target-group"
  port                 = 80
  protocol             = "TCP"
  vpc_id               = aws_vpc.main.id
  target_type          = "ip"
  deregistration_delay = 30

  health_check {
    interval            = 10
    port                = "traffic-port"
    protocol            = "TCP"
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }
}

resource "aws_lb_listener" "main" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "TCP"

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

ECSの設定

で、ECS。Service Connectで遊ぶつもりなのでCloudMapの設定も入れておく。

#########################################
# ECS
#########################################

resource "aws_ecs_cluster" "main" {
  name = "${local.main_name}-ecs"
}

resource "aws_service_discovery_http_namespace" "namespace" {
  name = "${local.main_name}-namespace"
}

Outputを書いておく

NLBのDNS名だけ欲しいので出しておく

#########################################
# Output
#########################################

output "nlb_dns_name" {
  value = aws_lb.main.dns_name
}

設定終わり

planしてapplyだ!

❯ terragrunt plan

いろいろ出力される

❯ terragrunt apply

いろいろ出力される

Apply complete! Resources: 18 added, 0 changed, 0 destroyed.

Outputs:

nlb_dns_name = "<NLBのDNS名が出力される>"

でけた。

動作確認

ECSにサービスを作るのとかはOpenTofuからじゃなくてecspressoからやりたいので、今は手でゴニョゴニョ作って確認する。nginxのタスクを作って、そのサービスを作った。からの、NLBのDNSを叩くと、nginxのデフォルトページが返ってきた。

❯ curl <NLBのDNS名>
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

ヽ(=´▽`=)ノ

destroy

やりたかったことの準備だけして満足した(えー!)ので、今日はここまででいいや。今、手で作ったECS Serviceを手で強制削除してから、destroyだー!

❯ terragrunt destroy -auto-approve

...

Destroy complete! Resources: 18 destroyed.

おしまい。次は、このファイルで apply したら環境が立ち上がるので続きができる。あー。ECRも欲しくなりそうだな・・・。次回の自分、たのむ!ファイルはGitHubにあげといた。

github.com

楽しかった。