叡智に富んだ読者さん、ご機嫌よう。
前回の投稿からエロ画像検出APIサービスの運用を続けている私ですが、最近ちょっと困ったことが起きました。
なんと、仕事を辞めてから爪に火をともす生活をしている私にとって、予期しないコストが発生するなんてまさに「おはぎゃー!!!」な状況です。
11日間で450円、これが1ヶ月放置すると1300円に膨れ上がるという恐ろしい現実に直面しました。
ラーメン1,2杯分のお金が、何もしていないのに飛んでいくなんて恐ろしい・・・
ということで今回は、コマンド一つでエロ画像検出APIサーバーをサクッと立ち上げてシャットダウンできる、そんな魔法のようなコードを作っていきたいと思います!
これでお金もかからない!Terraformの力を借りて、自動化していきましょう!
前提知識
前回の記事ではECSタスクにIPv4アドレスが割り当てられることを確認しました
しかし、そのアドレスは動的に割り当てられるため、ECSを起動するたびにS3バケットのポリシーや環境変数を都度変更しなければならないという手間が発生していました。
「それなら、ECSタスクにElastic IPを紐づければ済むのでは?」とひらめいたものの、AWSは公式に「FargateタスクとElastic IPアドレスを直接紐づけることはできない」と発表しています。
静的 IP アドレスまたは Elastic IP アドレスを Fargate タスクに直接追加することはできません。Fargate タスクで静的 IP または Elastic IP を使用するには、まず Network Load Balancer で Fargate サービスを作成する必要があります。その後、タスクの Elastic IP アドレスをロードバランサーにアタッチします。
どうやら、固定のIPアドレスからECSタスクにアクセスするためには、Network Load Balancer (NLB) を使ってElastic IPを割り当てる必要があるようです。
用意しておくべきもの
variable.tfの設定
- ElasticIP 割り当てID
- VPCのID
- VPC パブリックサブネットのID
上記3つについては事前に用意しておきましょう。
ElasticIP 割り当てIDは一覧テーブルの左から4番目に表示されます。
値を取得し終えたらvariable.tfを以下のように変更してください。
variable "vpc_id" {
default = "<VPCのID>"
}
variable "public_subnet_ids" {
default = ["<VPC パブリックサブネットのID>"]
}
variable "eip_alloc" {
default = "<ElasticIP 割り当てID>"
}
ECRリポジトリの設定
以下のリンクを参考に手元にあるdocker-imageをaws ecrにpushしてください。
1. NLBの設定
この章ではnlb.tfについて解説します。
1-1. NLBターゲットグループの設定
# NLBのターゲットグループを作成
resource "aws_lb_target_group" "ecs_tg" {
target_type = "ip"
name = "ecs-target-group"
port = 5000
protocol = "TCP"
vpc_id = var.vpc_id
health_check {
port = "traffic-port"
protocol = "TCP"
}
}
属性 | 説明 |
---|---|
target_type |
ip が指定されており、IPアドレスを使ってターゲットを指定するタイプのターゲットグループを作成しています。ECSタスクの各コンテナに動的なIPアドレスを割り当てる場合に使用します。 |
port | ターゲットがリッスンするポート番号。ここでは 5000 番ポートが使用されています。 |
protocol | 使用するプロトコル。この場合、通信に TCP プロトコルを使用します。 |
vpc_id | ターゲットグループが作成されるVPCのIDです。変数 var.vpc_id を使ってVPCのIDを指定しています。 |
health_check | ターゲットの正常性を確認するためのヘルスチェックを設定しています。port と protocol はどちらも TCP で設定され、ロードバランサーがターゲットのヘルスを監視します。 |
1-2. Network Load Balancer (NLB) を作成
# NLBを作成
resource "aws_lb" "nlb" {
name = "ecs-nlb"
internal = false # パブリック NLB の場合は false
load_balancer_type = "network"
ip_address_type = "ipv4"
idle_timeout = 60
subnet_mapping {
subnet_id = var.public_subnet_ids[0]
allocation_id = var.eip_alloc
}
}
属性 | 説明 |
---|---|
internal |
false が指定されており、これはパブリック向けのNLBであることを示しています。内部(プライベート)向けのNLBの場合は true に設定します。 |
load_balancer_type |
network が指定されており、作成されるロードバランサーはNetwork Load Balancerであることを示しています。 |
ip_address_type |
ipv4 を指定して、IPv4アドレスを使用します。 |
idle_timeout | コネクションがアイドル状態になった際に、NLBがそのコネクションを終了するまでの待ち時間(秒単位)です。この場合は 60秒 が指定されています。 |
subnet_mapping | NLBのサブネットマッピングを設定しています。 |
└ subnet_id | NLBが所属するサブネットIDを指定しています。例では、変数 var.public_subnet_ids[0] を使用して最初のパブリックサブネットを指定しています。 |
└ allocation_id | Elastic IP(EIP)のアロケーションIDを指定して、NLBに静的なIPアドレスを関連付けています。このIDは var.eip_alloc に格納された値を参照します。 |
1-3. リスナーを作成
# リスナーを作成
resource "aws_lb_listener" "nlb_listener" {
load_balancer_arn = aws_lb.nlb.arn
port = 5000
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.ecs_tg.arn
}
}
属性 | 説明 |
---|---|
load_balancer_arn | 1-2で作成したNLB(aws_lb.nlb )のARNを指定する。このリスナーがそのNLBに紐付けられる。 |
port | リスナーが待ち受けるポート番号。この例では 5000 番ポートが指定される。 |
protocol | 使用するプロトコルをTCP に設定。リスナーはこのプロトコルで通信を受け取る |
default_action | リスナーが受け取ったリクエストに対して実行するデフォルトのアクション。 |
└ type | アクションのタイプは forward で、リクエストをターゲットグループに転送します。 |
└ target_group_arn | 先ほど作成したターゲットグループ(aws_lb_target_group.ecs_tg )のARNを指定して、リクエストをそのターゲットグループに転送します。 |
上記の手順によりElastic IPを持ったNLBが設定され、指定したターゲットグループにリクエストを転送するようになります。
2. ECSの設定
この章ではfargate.tfについて解説します。
- ECSクラスターの定義
- ECSタスクの設定
- ECSサービスの設定
2-1. ECSクラスターの定義
resource "aws_ecs_cluster" "nsfw_cluster" {
name = "nsfw_api_cluster"
}
# 省略
2-2. ECSタスクの定義
# 省略
resource "aws_ecs_task_definition" "nsfw_task" {
family = "nsfw-api-container"
cpu = "256"
memory = "512"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
container_definitions = file("./container_definition.json")
execution_role_arn = module.ecs_task_execution_role.iam_role_arn
}
# 省略
コンテナ定義ファイルであるcontainer_definition.json
を読み込んでください。
image
にはECRの設定で作成されたイメージURIを設定してください。
"image": "<自身のAWSアカウントID>.dkr.ecr.us-east-1.amazonaws.com/detect-noodie-image:latest",
タスク実行ロールの定義
ECSタスクの実行に必要なIAMロールの設定を行います。タスク実行ロールがタスクに正しく紐付いていない場合、以下のようなエラーが表示され、タスクが起動しません。
An error occurred (ClientException) when calling the RegisterTaskDefinition operation: Fargate requires task definition to have execution role ARN to support log driver awslogs.
data "aws_iam_policy" "ecs_task_execution_role_policy" {
arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
data "aws_iam_policy_document" "ecs_task_execution" {
source_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy
statement {
effect = "Allow"
actions = ["ssm:GetParameters", "kms:Decrypt"]
resources = ["*"]
}
}
module "ecs_task_execution_role" {
source = "./iam_role"
name = "ecs-task-execution"
identifier = "ecs-tasks.amazonaws.com"
policy = data.aws_iam_policy_document.ecs_task_execution.json
}
variable "name" {}
variable "policy" {}
variable "identifier" {
default = "ecs-tasks.amazonaws.com"
}
resource "aws_iam_role" "default" {
name = var.name
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
data "aws_iam_policy_document" "assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [var.identifier]
}
}
}
resource "aws_iam_policy" "default" {
name = var.name
policy = var.policy
}
resource "aws_iam_role_policy_attachment" "default" {
role = aws_iam_role.default.name
policy_arn = aws_iam_policy.default.arn
}
output "iam_role_arn" {
value = aws_iam_role.default.arn
}
output "iam_role_name" {
value = aws_iam_role.default.name
}
iam_role/main.tf
は、オブジェクト指向プログラミングにおけるクラスに相当します。一方、module "ecs_task_execution_role"はインスタンスに相当します。
IAMロールやIAMポリシーは非常に汎用性が高いため、同じようなコードを毎回書くのではなく、このようにテンプレート化することで、コードの再利用性と可読性が大幅に向上します。
2-3. ECSサービスの設定
# 省略
resource "aws_ecs_service" "nsfw_service" {
name = "nsfw_api_service"
cluster = aws_ecs_cluster.nsfw_cluster.arn
task_definition = aws_ecs_task_definition.nsfw_task.arn
desired_count = 1
launch_type = "FARGATE"
platform_version = "1.3.0"
# ネットワーク設定(後述)
network_configuration{...}
# NLBとの紐付け(後述)
load_balancer {...}
lifecycle {
ignore_changes = [task_definition]
}
}
ネットワークの設定
# ネットワーク設定
network_configuration {
assign_public_ip = true
security_groups = [module.nsfw_api_sg.security_group_id]
subnets = var.public_subnet_ids
}
この network_configuration
ブロックは、ECSタスクのネットワーク設定を定義しています
属性 | 説明 |
---|---|
assign_public_ip |
true に設定するとECSタスクにパブリックIPアドレスが自動的に割り当てられ、タスクがインターネット経由で外部と通信できるようになる。 |
security_groups |
module.nsfw_api_sg.security_group_id という変数を使用して、セキュリティグループのIDを参照。セキュリティグループは、タスクの通信に対するインバウンドとアウトバウンドのルールを管理する。 |
subnets | タスクが配置されるサブネットを指定。var.public_subnet_ids という変数に格納された複数のパブリックサブネットIDを参照して、タスクがパブリックサブネット内に配置される。 |
これら設定によりタスクはインターネットに接続可能なパブリックIPを持ち、指定されたセキュリティグループに従い、パブリックサブネットに配置されます。
module.nsfw_api_sg.security_group_id
は以下で生成されたセキュリティグループのIDのことです。
セキュリティグループの設定
module "nsfw_api_sg" {
source = "./security_group"
name = "nsfw_api_sg"
vpc_id = var.vpc_id
port = 5000
protocol = "tcp"
}
上記のコードは ./security_group
モジュールを呼び出して、新しいセキュリティグループを作成しています。モジュールのパラメータとして以下が渡されています。
属性 | 説明 |
---|---|
name | セキュリティグループの名前(nsfw_api_sg ) |
vpc_id | このセキュリティグループが属するVPCのID。var.vpc_id を指定する。 |
port | インバウンド通信で許可するポート番号。 この場合、5000 ポートが指定される。 |
protocol | 使用するプロトコル。ここでは tcp プロトコルを使用します。 |
variable "name" {}
variable "vpc_id" {}
variable "port" {}
variable "protocol" {}
resource "aws_security_group" "default"{
name = var.name
vpc_id = var.vpc_id
}
resource "aws_security_group_rule" "ingress"{
type = "ingress"
from_port = var.port
to_port = var.port
protocol = var.protocol
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.default.id
}
resource "aws_security_group_rule" "egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.default.id
}
output "security_group_id" {
value = aws_security_group.default.id
}
これらのコードはサービス設定画面の「ネットワーキング > セキュリティグループ」に対応します。
ここで注意して欲しいのが、画面上にはインバウンドルール(ingress
)の設定画面だけが表示されているものの、裏側ではアウトバウンドルール(egress
)も設定される点です。
terraform上ではアウトバウンドルールの設定も明示的に行いましょう。
NLBとの紐付け
# NLBとの紐付け
load_balancer {
target_group_arn = aws_lb_target_group.ecs_tg.arn
container_name = "nsfw-api-container"
container_port = 5000
}
#省略
この load_balancer ブロックは、ECSサービスに対してロードバランサーの設定を行う部分です。
属性 | 説明 |
---|---|
target_group_arn | ECSタスクのトラフィックを転送するためのターゲットグループを指定。aws_lb_target_group.ecs_tg.arn という変数で、定義されたターゲットグループ(ecs_tg)のARNを参照する。 |
container_name | ECSタスク内でロードバランサーと連携させるコンテナの名前(nsfw-api-container )を指定。この名前は、タスク定義で指定されたコンテナ名と一致している必要がある。
|
container_port | 指定されたコンテナがリッスンするポート番号を指定。この例では、5000番ポートを使ってコンテナが外部からのリクエストを受け付ける。 |
この設定により、ロードバランサーはnsfw-api-container
コンテナの5000番ポートにリクエストを転送し、そのトラフィックは指定されたターゲットグループ経由で処理されるように設定されています。
3. terraformでサクッとAPIサーバーを構築
terraformを使えばUIでポチポチ設定しなおす手間が省けたり、サービスの消し忘れによる不本意な料金発生に悩まされる必要がなくなります。
terraform init
terraform apply
上記のコマンドを叩き、3分程待ちましょう,
以下のように表示されたら成功です!
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
使わなくなった時は以下のコマンドでdestroyすれば綺麗さっぱりなくなります。
terraform destroy
NLBとECSを付けっぱなしにすると月に3000円以上取られますが、
terraformで自分の使いたいにだけ立ち上げるとElasticIpの料金しか取られません。
以下のURLを打ち込めば前回と同様、APIレスポンスが返却されます。
http://<ElasticIP>:5000/?url=https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgA0XNyhytw0thfWMu4X6yK89mQ4lwuqPWmOTy4-UQ4NCOLFiYMJfetMdfCze2aGIZra6_SSqLtHvl-zXFmlFVqfu5RySPBi7gEXg8sP-QFPePD88dSmb0EQUU9nRZneIm36zSbUMT60nE/s400/animal_stand_neko.png
まとめ
この記事では、Amazon ECSのタスクにElastic IPを直接紐付けられない制約に対し、Terraformを活用してNetwork Load Balancer(NLB)を用いた解決策を紹介しました!
ECSタスクに動的に割り当てられるIPアドレスの管理が課題となっている場合,
NLBを構築しElastic IPを固定することができます。
この構成を使用することで、IPアドレスの動的変更による運用上の手間を削減し、安定したサービス運用が可能となります!Terraformを使った自動化の利点を最大限に活用し、今後もシステムのスケーラビリティや信頼性を高めるアプローチを模索していきたいですね
最後までお読みいただき誠にありがとうございます!
関連記事
参考にしたコード