1. ホーム
  2. 記事一覧
  3. 【Terraformハンズオン】ECS Fargateでアプリケーションデプロイを実践してみよう

2024.07.23

【Terraformハンズオン】ECS Fargateでアプリケーションデプロイを実践してみよう

ECS(Amazon Elastic Container Service)は、AWSが提供するフルマネージドなコンテナ管理サービスです。ECSを使用することで、Dockerコンテナを簡単に実行、停止など管理することができます。

ECSでは、Dockerコンテナが実行されている1つ以上のEC2インスタンスを「クラスタ」と呼ばれる単位で扱います。

そのクラスタは、「EC2起動型」と「Fargate」から選択することができ、今回の記事ではFargateをTerraformで構築する手順を解説します。

AWSを使用するインフラエンジニアとしては抑えておきたい内容ですので、一緒に学んでいきましょう。

Fargateとは

AWS Fargateは、ECSとEKS(Amazon Elastic Kubernetes Service)で利用可能な、Dockerコンテナをサーバーレスで実行する機能を提供してくれるサービスです。

Fargateを使用すると、コンテナを実行するために必要なEC2インスタンスの管理、サーバーの設定、スケーリングを行う必要がなくなるため、インフラの管理よりもアプリケーション開発に集中したい時に効果を発揮します。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/AWS_Fargate.html

Fargateの特徴

EC2起動型と比較し、Fargateにはどのような特徴があるのかを解説します。

サーバーレスであること

出典: 202108 AWS Black Belt Online Seminar ECS での Fargate 入門

Fargateはサーバーレスなサービスなため、OSの管理はAWSに任せることができます。 EC2起動型では、コンテナの実行にEC2を使用するため、OSの脆弱性に対するパッチ適用や、DockerAgent、ECSAgentなどのツールのバージョン管理が必要でした。

Fargateではこの部分をAWS側で管理してくれるため、サーバーに対する管理コストを削減することができます。

サーバーレスのメリット、デメリットに関しては以下の記事で解説しています。

https://envader.plus/article/36

インスタンスクラスの指定が不要

Fargateを起動する時には、t3.microなどのインスタンスクラスの指定が不要となります。

管理者は、CPU、メモリの組み合わせのみを意識すれば良くなるため、m5.largeを準備しておいたが実際はここまで必要なかった。といった設計へのコスト、無駄な費用の発生を抑えることができます。

スケールアウトのコストが低い

「実行環境のスケールアウトが不要」という点もFargateの特徴です。

EC2起動型では、コンテナを増やす前にEC2インスタンス自体のスケールアウトが必要になり、コンテナの実行環境を増やしてから新しいコンテナを起動します。

Fargateではこの実行環境のスケールアウトが必要なく、タスクを増やすのみで済むためEC2起動型に比べてスケールアウトのコストが低くなります。

タスクレベルの細かい制御が可能

Fargateにはタスクと呼ばれるコンテナの集まりがあります。スケールアウトした際にタスクが増加し、このタスクごとにENI(Elastic Network Interface)が割り当てられます。

この場合、それぞれのタスクごとにセキュリティグループの設定をすることが可能になり、各タスクごとに通信の許可、拒否の細かい制御が行えます。

また、各タスクごとに独自のIAM Roleをアタッチすることもできるため、こちらでも細かい権限管理が可能になります。

EC2よりも費用が高い

FargateはOSの管理が必要ないなどの運用コストが低い代わりに、EC2よりも若干ランニングコストが高くなります。

EC2のm5.largeを2vCPU、メモリを8GB東京リージョンで使用したとすると、2024年7月時点で1時間あたり0.02136USD、1日では0.51264USD、Fargateの方が費用がかかります。

金額が若干高くなりますが、運用していくコストを下げられるというメリットは大きいため、どちらを選択するかは検討する必要があります。

https://aws.amazon.com/jp/blogs/news/theoretical-cost-optimization-by-amazon-ecs-launch-type-fargate-vs-ec2/

タスク定義の基本

ここからは、タスク定義の基本を解説していきます。

タスク定義とは?

タスク定義は、ECSでコンテナを実行するための「設計書」のようなもので、JSON形式のファイルです。タスク定義には、起動タイプ、タスクに割り当てるIAM Role、使用するコンテナの元となるイメージ、ネットワークの設定などが含まれます。

このタスク定義を元にして、ECSがコンテナを起動します。

タスク定義のパラメータ

タスク定義を作成するための主なパラメータをテーブルにまとめました。

パラメータ名説明
familyタスク定義の名前。タスク定義のバージョンをまとめて管理できる。
taskRoleArnタスクが各AWSリソースにアクセスするための権限を付与したIAM RoleのARN。アプリケーションの設計によるもの。
executionRoleArnタスクの実行に必要なAWSサービスにアクセスするためのIAM RoleのARN。イメージのpullなどに必要。
networkModeタスクがどのようにネットワークに接続するかを設定。Fargateはawsvpcのみ。
containerDefinitionsタスクに含まれるコンテナの設定。複数のコンテナを定義することが可能。
cpuタスクが使用するCPUの量。
memoryタスクが使用するメモリの量。
requiresCompatibilities起動タイプの設定。タスクがFargate、EC2のどちらで実行されるかを記述。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html

ネットワークモードの基礎

ECSにはタスクのコンテナで使用するDockerのネットワークモードがあります。

項目としてはdefault、bridge、Host、None、awsvpcの5つがあり、Fargateを使用する場合にはawsvpcモードのみ選択が可能です。

ECSの基本的な構成要素やネットワークモードについては、以下の記事で解説しています。

https://envader.plus/article/180

awsvpcモードの特徴

awsvpcモードでは各タスクに専用のENIが割り当てられるため、それぞれのタスクにセキュリティグループをアタッチすることが可能になります。こうすることで、細かい通信の制御ができるようになります。

各ENIは独自のPrivate IPアドレスを持つため、VPC内のリソースとの通信はこのPrivate IPアドレスを使用して行います。その結果、内部ネットワークの通信が安全に行われます。

また、VPC Flow Logsでのモニタリングにも有用です。ログのレコード内に含まれるNetwork Interfaceの情報を確認することで、どのタスクがどのリソースと通信しているかを識別することが可能になります。

Terraformでの実践

ここからは、ECS FargateをTerraformを使用して作成します。コンテナの元となるイメージはローカルで作成し、イメージをECRへpush、pushしたイメージを使用してFargateを起動していきます。

Dockerイメージの作成

はじめにFargateで使用するDockerイメージを作成します。今回のハンズオンでは、Node.jsを使用して文字列を返すようにします。

// app.js

import express from 'express';

const app = express();
const port = 3000;

app.get('/', (req, res) => {
   res.send('Hello from ECS Fargate!');
});

app.listen(port, () => {
  console.log(`App listening at <http://localhost>:${port}`);
 });

package.jsonは以下の内容で作成します。

# package.json
{
   "name": "fargate",
   "version": "1.0.0",
   "type": "module",
   "description": "",
   "main": "app.js",
   "dependencies": {
     "express": "^4.17.1"
   },
   "scripts": {
     "test": "echo \\"Error: no test specified\\" && exit 1"
   },
   "keywords": [],
   "author": "",
   "license": "ISC"
}

Dockerfileを作成します。

# Dockerfile
FROM node:20

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD [ "node", "app.js" ]

Docker imageをタグをつけてビルドします。

docker build -t dev-fargate .

一度コンテナを起動し、きちんと動くか確認します。

docker run -p 3000:3000 -d dev-fargate

ECRリポジトリの作成

Fargateで使用するイメージをECRへpushする必要があるため、リポジトリを作成します。

aws ecr create-repository --repository-name dev-fargate

ECRとは、Amazon Elastic Container Registryの略で、AWSが提供するフルマネージドのDockerコンテナレジストリサービスです。ECRを使うことで、コンテナイメージの保存、管理、デプロイが簡単に行えます。作成したリポジトリにイメージをpushすることで、Fargateタスクで使用することができます。

https://aws.amazon.com/jp/ecr/

Dockerイメージのpush

ローカルで作成したDockerイメージを、作成したECRリポジトリにpushします。

まず、ECRにログインします。

AWSアカウントIDは、自身のAWSアカウントIDを記述します。

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com

先ほど作成したDockerイメージに対して、ECRへpushするためのタグをつけます。

docker tag dev-fargate:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/dev-fargate:1.0.0

タグ付けを確認します。

docker images

タグ付けしたDockerイメージをECRへpushします。

docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/dev-fargate:1.0.0

pushできたかを確認します。

aws ecr describe-images --repository-name dev-fargate --region ap-northeast-1

ロググループの作成

ECSを使用してコンテナを起動する際、ログの保存先が必要になります。

ログの保存先として、CloudWatchLogsにロググループを作成します。ロググループの名前は任意の名前で問題ありません。

aws logs create-log-group --log-group-name /ecs/dev-ecs --region ap-northeast-1

ロググループが作成できたかを確認します。

aws logs describe-log-groups --log-group-name-prefix /ecs/dev-ecs --region ap-northeast-1

TerraformでFargateを作成する

Terraformを使用して、Fargateを作成します。今回は簡単にアクセスできるようPublicIPを有効にし、コンテナをパブリックサブネットへ配置します。

はじめにversions.tfを作成します。

# versions.tf
terraform {
   required_version = ">= 1.0.0"
   required_providers {
     aws = {
       source  = "hashicorp/aws"
       version = "5.3.0"
     }
   }
 }

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

続いてmain.tfを作成します。

locals {
  dev = {
    system = "dev-ec2"
  }
  vpc = {
    cidr_block  = "10.0.0.0/16"
    subnet_cidr = "10.0.1.0/24"
  }
}

data "aws_ecr_repository" "dev_ecr_repository" {
  name = "dev-fargate"
}

# ECS Task Execution Role
data "aws_iam_policy_document" "assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

# vpc
resource "aws_vpc" "dev_vpc" {
  cidr_block           = local.vpc.cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name  = "dev-vpc"
    Roles = "vpc"
  }
}

# subnet
resource "aws_subnet" "dev_subnet_public" {
  vpc_id                  = aws_vpc.dev_vpc.id
  cidr_block              = local.vpc.subnet_cidr
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true
  tags = {
    Name  = "dev-subnet-public"
    Roles = "subnet"
  }
}

# RouteTable
resource "aws_route_table" "dev_route_table" {
  vpc_id = aws_vpc.dev_vpc.id
  tags = {
    Name = "dev-route-table"
  }
}

# RouteTableの関連付け
resource "aws_route_table_association" "dev_public_route" {
  subnet_id      = aws_subnet.dev_subnet_public.id
  route_table_id = aws_route_table.dev_route_table.id
}

# InternetGateway
resource "aws_internet_gateway" "dev_igw" {
  vpc_id = aws_vpc.dev_vpc.id
  tags = {
    Name = "dev-igw"
  }
}

# Route
resource "aws_route" "dev_route" {
  route_table_id         = aws_route_table.dev_route_table.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.dev_igw.id
}

# SecurityGroup
resource "aws_security_group" "dev" {
  name        = "dev-sg"
  description = "dev-sg"
  vpc_id      = aws_vpc.dev_vpc.id

  tags = {
    Name = "${local.dev.system}-sg"
  }
}

# Fargate用インバウンドルール
resource "aws_security_group_rule" "dev_rule_ingress" {
  type              = "ingress"
  from_port         = 3000
  to_port           = 3000
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.dev.id
}

# アウトバウンドルール
resource "aws_security_group_rule" "dev_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.dev.id
}

# ECS Cluster
resource "aws_ecs_cluster" "dev_ecs_cluster" {
  name = "dev-ecs-cluster"
}

# ECS タスク定義
resource "aws_ecs_task_definition" "dev_ecs_task_definition" {
  family                   = "dev-ecs-task-definition"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.dev_ecs_task_execution_role.arn
  container_definitions = jsonencode([
    {
      name      = "dev-container"
      image     = "AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/dev-fargate:1.0.0"
      cpu       = 256
      memory    = 512
      essential = true

      portMappings = [
        {
          containerPort = 3000
          protocol      = "tcp"
        }
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = "/ecs/dev-ecs"
          "awslogs-region"        = "ap-northeast-1"
          "awslogs-stream-prefix" = "ecs"
        }
      }
    }
  ])

  runtime_platform {
    cpu_architecture        = "ARM64"
    operating_system_family = "LINUX"
  }
}

# ECS Service
resource "aws_ecs_service" "dev_ecs_service" {
  name            = "dev-ecs-service"
  cluster         = aws_ecs_cluster.dev_ecs_cluster.id
  task_definition = aws_ecs_task_definition.dev_ecs_task_definition.arn
  desired_count   = 1
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = [aws_subnet.dev_subnet_public.id]
    security_groups  = [aws_security_group.dev.id]
    assign_public_ip = true
  }
}

# Task実行用 IAM Role ECRリポジトリからイメージをpullしてくる際に必要
resource "aws_iam_role" "dev_ecs_task_execution_role" {
  name               = "dev-ecs-task-execution-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

# IAM Policyをロールにアタッチ
resource "aws_iam_role_policy_attachment" "dev_ecs_task_execution_role" {
  role       = aws_iam_role.dev_ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

クラスターとタスク定義の解説

以下で、クラスターを作成し、サービスとタスクを実行する基盤を作成します。

# ECS Cluster
resource "aws_ecs_cluster" "dev_ecs_cluster" {
  name = "dev-ecs-cluster"
}

次に、タスク定義を作成します。

このタスク定義で、タスクに割り当てるCPUやメモリ、どのリポジトリにあるDockerイメージを使用するかなどを記述します。

# ECS タスク定義
resource "aws_ecs_task_definition" "dev_ecs_task_definition" {
  family                   = "dev-ecs-task-definition" # タスク定義のファミリー名。バージョン管理に使用される
  cpu                      = "256" # タスクに割り当てるCPUユニット。256は0.25 vCPUに相当
  memory                   = "512" # タスクに割り当てるメモリ(MiB)
  network_mode             = "awsvpc" # タスクのネットワークモード。awsvpcはFargateで必須
  requires_compatibilities = ["FARGATE"] # このタスク定義の起動タイプ
  execution_role_arn       = aws_iam_role.dev_ecs_task_execution_role.arn # タスク実行時に使用するIAMロールのARN
  container_definitions = jsonencode([ # コンテナ定義。
    {
      name      = "dev-container" # コンテナ名
      image     = "AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/dev-fargate:1.0.0" # 使用するDockerイメージのURI
      cpu       = 256 # コンテナに割り当てるCPUユニット
      memory    = 512 # コンテナに割り当てるメモリ(MiB)
      essential = true # このコンテナが必須かどうか。trueの場合、このコンテナが停止するとタスクも停止する

      portMappings = [
        {
          containerPort = 3000 # コンテナ内のポート番号
          protocol      = "tcp" # 使用するプロトコル
        }
      ]

      logConfiguration = { # ログ設定
        logDriver = "awslogs" # 使用するログドライバー
        options = {
          "awslogs-group"         = "/ecs/dev-ecs" # CloudWatchロググループ名
          "awslogs-region"        = "ap-northeast-1"
          "awslogs-stream-prefix" = "ecs"
        }
      }
    }
  ])

  runtime_platform {
    cpu_architecture        = "ARM64" # 使用するCPUアーキテクチャ
    operating_system_family = "LINUX" # 使用するOS
  }
}

今回は、runtime_platformARM64を指定しています。これは、デフォルトではAWS側でx86_64アーキテクチャが採用されるためです。

Dockerイメージを作成するPCのアーキテクチャによって変わってくるため、こちらの記述は注意が必要です。

筆者はここを指定せずにデプロイし、タスクが起動しない事例が発生しました。

Dockerイメージのアーキテクチャを確認するには、以下コマンドを実行します。

docker inspect [タグ名] | grep Architecture

ECSサービスの解説

# ECS Service
resource "aws_ecs_service" "dev_ecs_service" {
  name            = "dev-ecs-service"
  cluster         = aws_ecs_cluster.dev_ecs_cluster.id # このサービスが属するECSクラスターのID
  task_definition = aws_ecs_task_definition.dev_ecs_task_definition.arn # 実行するタスク定義のARN。
  desired_count   = 1 # 常に実行しておきたいタスクの数。
  launch_type     = "FARGATE" # 起動タイプ。

  network_configuration {
    subnets          = [aws_subnet.dev_subnet_public.id]
    security_groups  = [aws_security_group.dev.id]
    assign_public_ip = true # パブリックIPアドレスを割り当てるかどうか。
  }
}

こちらでは、サービス名、どのクラスターに属するか、タスク定義はどれを使用するかなどを指定します。

今回はインターネットから直接通信したいため、パブリックIPを付与しています。

apply後の接続確認

今回はapply後の接続確認をAWS CLIで行います。

はじめに、タスクに割り当てられたENI情報を確認します。

aws ecs describe-tasks --cluster クラスター名 --tasks タスクID --region ap-northeast-1

上記コマンドの結果から、networkInterfaceIdの値を確認し、そのIDを元に次のコマンドでENIの詳細を確認します。

aws ec2 describe-network-interfaces --network-interface-ids eni-xxxxxxxxxxxxx

パブリックIPを確認します。"PublicIp"の項目にIPアドレスが表示されますので、このIPアドレスを控えておきます。

"PublicIp": "x.xxx.xxx.165"

このIPアドレスを使用して、curlコマンドでローカルからアクセスします。

Hello from ECS Fargate!の文字列が返ってくれば完了です。

curl x.xxx.xxx.165:3000
Hello from ECS Fargate!

まとめ

この記事では、AWS Fargateの特徴と、Terraformを使用して構築する手順を解説しました。

Fargateを使用することで、コンテナアプリケーションの運用が簡単になります。開発者はサーバーの管理に時間を取られることなく、アプリケーションの開発により多くの時間を使うことができます。

Fargateは全てのケースに適しているわけではありませんが、適切な場面で使えば非常に効果的ですので、この記事で手を動かしていただき、理解を深めていただければと思います。

【番外編】USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

IT未経験者必見 USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

プログラミング塾に半年通えば、一人前になれると思っているあなた。それ、勘違いですよ。「なぜ間違いなの?」「正しい勉強法とは何なの?」ITを学び始める全ての人に知って欲しい。そう思って書きました。是非読んでみてください。

「フリーランスエンジニア」

近年やっと世間に浸透した言葉だ。ひと昔まえ、終身雇用は当たり前で、大企業に就職することは一種のステータスだった。しかし、そんな時代も終わり「優秀な人材は転職する」ことが当たり前の時代となる。フリーランスエンジニアに高価値が付く現在、ネットを見ると「未経験でも年収400万以上」などと書いてある。これに釣られて、多くの人がフリーランスになろうとITの世界に入ってきている。私もその中の1人だ。数年前、USBも知らない状態からITの世界に没入し、そこから約2年間、毎日勉学を行なった。他人の何十倍も努力した。そして、企業研修やIT塾で数多くの受講生の指導経験も得た。そこで私は、伸びるエンジニアとそうでないエンジニアをたくさん見てきた。そして、稼げるエンジニア、稼げないエンジニアを見てきた。

「成功する人とそうでない人の違いは何か?」

私が出した答えは、「量産型エンジニアか否か」である。今のエンジニア市場には、量産型エンジニアが溢れている!!ここでの量産型エンジニアの定義は以下の通りである。

比較的簡単に学習可能なWebフレームワーク(WordPress, Rails)やPython等の知識はあるが、ITの基本概念を理解していないため、単調な作業しかこなすことができないエンジニアのこと。

多くの人がフリーランスエンジニアを目指す時代に中途半端な知識や技術力でこの世界に飛び込むと返って過酷な労働条件で働くことになる。そこで、エンジニアを目指すあなたがどう学習していくべきかを私の経験を交えて書こうと思った。続きはこちらから、、、、

note記事3000いいね超えの殿堂記事 今すぐ読む

エンベーダー編集部

エンベーダーは、ITスクールRareTECHのインフラ学習教材として誕生しました。 「遊びながらインフラエンジニアへ」をコンセプトに、インフラへの学習ハードルを下げるツールとして運営されています。

RareTECH 無料体験授業開催中! オンラインにて実施中! Top10%のエンジニアになる秘訣を伝授します! RareTECH講師への質疑応答可

関連記事