4
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS × Terraform】ECSでエロ画像検出APIを爆速デプロイしてみた

Last updated at Posted at 2024-09-21

叡智に富んだ読者さん、ご機嫌よう。

前回の投稿からエロ画像検出APIサービスの運用を続けている私ですが、最近ちょっと困ったことが起きました。

なんと、仕事を辞めてから爪に火をともす生活をしている私にとって、予期しないコストが発生するなんてまさに「おはぎゃー!!!」な状況です。

11日間で450円、これが1ヶ月放置すると1300円に膨れ上がるという恐ろしい現実に直面しました。

スクリーンショット 2024-09-12 16.42.12.png

ラーメン1,2杯分のお金が、何もしていないのに飛んでいくなんて恐ろしい・・・ :ramen: :cry:

ということで今回は、コマンド一つでエロ画像検出APIサーバーをサクッと立ち上げてシャットダウンできる、そんな魔法のようなコードを作っていきたいと思います!

これでお金もかからない!Terraformの力を借りて、自動化していきましょう!

前提知識

スクリーンショット 2024-09-25 16.17.52.png

前回の記事ではECSタスクにIPv4アドレスが割り当てられることを確認しました :cat:

しかし、そのアドレスは動的に割り当てられるため、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番目に表示されます。

スクリーンショット 2024-09-21 0.05.37.png

値を取得し終えたらvariable.tfを以下のように変更してください。

variable.tf

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.tf
# 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 ターゲットの正常性を確認するためのヘルスチェックを設定しています。portprotocol はどちらも TCP で設定され、ロードバランサーがターゲットのヘルスを監視します。

1-2. Network Load Balancer (NLB) を作成

nlb.tf
# 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. リスナーを作成

nlb.tf
# リスナーを作成
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について解説します。

  1. ECSクラスターの定義
  2. ECSタスクの設定
  3. ECSサービスの設定

2-1. ECSクラスターの定義

fargate.tf
resource "aws_ecs_cluster" "nsfw_cluster" {
    name = "nsfw_api_cluster"
}
# 省略

2-2. ECSタスクの定義

fargate.tf
# 省略
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.

ecs_task_execution_role.tf

ecs_task_execution_role.tf
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
}

iam_role/main.tf

iam_role/main.tf
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サービスの設定

fargate.tf
# 省略
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]
    }
}

ネットワークの設定

fargate.tf
    # ネットワーク設定
    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のことです。

セキュリティグループの設定

nsfw_api_sg.tf

nsfw_api_sg.tf
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 プロトコルを使用します。

security_group/main.tf

security_group/main.tf
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上ではアウトバウンドルールの設定も明示的に行いましょう。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3430313536312f63393936343865322d326137622d653430632d303031302d6234643432373866386433652e706e67.png

NLBとの紐付け

fargate.tf
    # 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)を用いた解決策を紹介しました! :smile:

ECSタスクに動的に割り当てられるIPアドレスの管理が課題となっている場合,
NLBを構築しElastic IPを固定することができます。

この構成を使用することで、IPアドレスの動的変更による運用上の手間を削減し、安定したサービス運用が可能となります!Terraformを使った自動化の利点を最大限に活用し、今後もシステムのスケーラビリティや信頼性を高めるアプローチを模索していきたいですね :cat:

最後までお読みいただき誠にありがとうございます!

関連記事

参考にしたコード

4
8
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
4
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?