RemixプロジェクトのコンテナをECSにデプロイしてみた

RemixプロジェクトのコンテナをECSにデプロイしてみた

Clock Icon2024.09.23

こんちには。

データ事業本部 インテグレーション部 機械学習チームの中村( @nokomoro3 )です。

今回は以下の記事で作成したコンテナイメージをECSにデプロイしてみたいと思います。

手始めにまずはPublic SubnetにこのRemixのコンテナイメージをデプロイしてみたいと思います。

実行環境

実行環境は以下となります。

  • OS: Windows 10 / WSL2 / Ubuntu 24.04 LTS
  • Node.js : v20.17.0
  • pnpm : 9.11.0
  • docker : Docker version 24.0.7-rd, build 72ffacf
  • terraform : v1.9.6 on linux_amd64
  • AWS CLI : aws-cli/2.17.56 Python/3.12.6 Linux/5.15.153.1-microsoft-standard-WSL2 exe/x86_64.ubuntu.24

インストール方法は以下を参考にしています。

やってみる

フォルダ構成

フォルダ構成は以下のようにしています。前回作成したものは ui/ 配下に置いています。

また後述するリソース作成はそれぞれのフォルダの main.tf に記述していきます。

sample-app
├── infra
│   ├── ecr
│   │   └── main.tf
│   ├── ecs
│   │   ├── ecr_push.sh
│   │   └── main.tf
│   └── vpc
│       └── main.tf
└── ui
    ├── Dockerfile
    ├── README.md
    ├── app
    │   ├── entry.client.tsx
    │   ├── entry.server.tsx
    │   ├── root.tsx
    │   ├── routes
    │   │   └── _index.tsx
    │   └── tailwind.css
    ├── node_modules/
    ├── package.json
    ├── pnpm-lock.yaml
    ├── postcss.config.js
    ├── public
    │   ├── favicon.ico
    │   ├── logo-dark.png
    │   └── logo-light.png
    ├── tailwind.config.ts
    ├── tsconfig.json
    └── vite.config.ts

VPCの作成

AWSのリソース作成をterraformからからやっていきます。まずはVPCを作成します。

infra/vpc/main.tf
# リソースプレフィックス
variable "resource_prefix" {
  description = "Resource prefix"
  type        = string
  default     = "sample-app" # 要設定:共通プレフィックス
}

# 変数の定義(リージョン用)
variable "region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-1" # デフォルトリージョンを設定
}

# 開発マシンのIPアドレス
variable "local_ip_address" {
  type = string
  default = "192.168.0.1/32" # 要設定:開発マシンのIPアドレス
}

# プロバイダーの設定
provider "aws" {
  region = "ap-northeast-1"

  # 共通タグ
  default_tags {
    tags = {
      Name = "${var.resource_prefix}"
    }
  }
}

# 利用可能なアベイラビリティーゾーンのデータソース
data "aws_availability_zones" "available" {
  state = "available"
}

# VPC
resource "aws_vpc" "this" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
}

# インターネットゲートウェイ
resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
}

# パブリックサブネット(2個)
resource "aws_subnet" "public" {
  count                   = 2
  vpc_id                  = aws_vpc.this.id
  cidr_block              = "10.0.${count.index + 1}.0/24"
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.resource_prefix}-public-${count.index + 1}"
  }
}

# パブリックルートテーブル
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.this.id
  }
}

# パブリックサブネットとルートテーブルの関連付け
resource "aws_route_table_association" "public" {
  count          = 2
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# デフォルトセキュリティグループの作成
resource "aws_security_group" "default" {
  name   = "${var.resource_prefix}-default"
  vpc_id = aws_vpc.this.id

  # セキュリティグループ内の全てのインバウンドトラフィックを許可
  ingress {
    protocol  = -1
    self      = "true"
    from_port = 0
    to_port   = 0
  }

  # 開発マシンからの接続用
  ingress {
    protocol  = "tcp"
    from_port = 3000
    to_port   = 3000
    cidr_blocks = ["${var.local_ip_address}"]
  }

  # 全てのアウトバウンドトラフィックを許可
  egress {
    protocol  = -1
    cidr_blocks = ["0.0.0.0/0"]
    from_port = 0
    to_port   = 0
  }
}

# 出力の修正
output "vpc_id" {
  value = aws_vpc.this.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "default_security_group_id" {
  value = aws_security_group.default.id
}

今回は簡易に実施するため、Public Subnetにデプロイしてlocal_ip_addressで指定したアドレスからのみ3000ポートにアクセスできるようにします。

またサブネットは今後のALBを考えて2つにしています。

今回はモジュール化などを実施して一括でデプロイできるようにする等は実施しないこととします。(なので一つ一つapplyしていきます)

記述後は以下を実行します。

pushd ./infra/vpc
terraform init
terraform apply
popd

ECRの作成

コンテナイメージをpushするためのレポジトリを作ります。

infra/ecr/main.tf
# リソースプレフィックス
variable "resource_prefix" {
  description = "Resource prefix"
  type        = string
  default     = "sample-app" # 要設定:共通プレフィックス
}

# 変数の定義(リージョン用)
variable "region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-1" # デフォルトリージョンを設定
}

# プロバイダーの設定
provider "aws" {
  region = "ap-northeast-1"

  # 共通タグ
  default_tags {
    tags = {
      Name = "${var.resource_prefix}"
    }
  }
}

resource "aws_ecr_repository" "this" {
  name = "${var.resource_prefix}-ui"
  force_delete = true
}

output "ui_ecr_repository_name" {
  description = "The name of the ECR repository"
  value       = aws_ecr_repository.this.name
}

記述後は以下を実行します。

pushd ./infra/ecr
terraform init
terraform apply
popd

コンテナイメージのpush

buildからpushまで行うスクリプトを作成します。

infra/ecs/ecr_push.sh
resource_prefix="sample-app" # 要設定:共通プレフィックス

ecr_repository_name="${resource_prefix}-ui"

region=$(aws configure get region)
account_id=$(aws sts get-caller-identity --query 'Account' --output text)

pushd ../../ui
docker build . -t ${ecr_repository_name}
popd

aws ecr get-login-password --region ${region} \
    | docker login --username AWS --password-stdin ${account_id}.dkr.ecr.${region}.amazonaws.com

docker tag \
    ${ecr_repository_name}:latest \
    ${account_id}.dkr.ecr.${region}.amazonaws.com/${ecr_repository_name}:latest

docker push ${account_id}.dkr.ecr.${region}.amazonaws.com/${ecr_repository_name}:latest

記述後は以下を実行します。

infra/ecs/ecr_push.sh
pushd ./infra/ecs
bash ecr_push.sh
popd

ECSの作成

最後にECSのリソースを作成します。

infra/ecs/main.tf
# リソースプレフィックス
variable "resource_prefix" {
  description = "Resource prefix"
  type        = string
  default     = "sample-app" # 要設定:共通プレフィックス
}

# 変数の定義(リージョン用)
variable "region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-1" # デフォルトリージョンを設定
}

# プロバイダーの設定
provider "aws" {
  region = "ap-northeast-1"

  # 共通タグ
  default_tags {
    tags = {
      Name = "${var.resource_prefix}"
    }
  }
}

data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

locals {
  account_id = data.aws_caller_identity.current.account_id
  region     = data.aws_region.current.name
}

# ECSクラスター
resource "aws_ecs_cluster" "this" {
  name = var.resource_prefix
}

# ECSタスク定義
resource "aws_ecs_task_definition" "this" {
  family                   = var.resource_prefix
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"                             # FARGATEの場合かならずawsvpc
  cpu                      = 1024                                 # FARGATEの場合指定必須
  memory                   = 2048                                 # FARGATEの場合指定必須
  execution_role_arn       = aws_iam_role.task_execution_role.arn # コンテナエージェントとDockerデーモン用のロール

  container_definitions = jsonencode([
    {
      name  = "ui"
      image = "${local.account_id}.dkr.ecr.${local.region}.amazonaws.com/${var.resource_prefix}-ui"

      # awsvpcの場合ポート番号は同じである必要がある
      portMappings = [
        {
          containerPort = 3000
          hostPort      = 3000
        }
      ]
    }
  ])
}

# ECSサービス
resource "aws_ecs_service" "this" {
  name            = var.resource_prefix
  cluster         = aws_ecs_cluster.this.id
  task_definition = aws_ecs_task_definition.this.arn
  desired_count   = 1
  launch_type     = "FARGATE"

  network_configuration {
    # 要設定:サブネットIDはVPCリソース作成時のoutputから取得してください
    subnets = [
      "subnet-xxxxxxxxxxxxxxxxx",
      "subnet-xxxxxxxxxxxxxxxxx"
    ]
    # 要設定:セキュリティグループIDはVPCリソース作成時のoutputから取得してください
    security_groups = [
      "sg-xxxxxxxxxxxxxxxxx"
    ]
    assign_public_ip = true
  }
}

# タスク実行ロール
resource "aws_iam_role" "task_execution_role" {
  name = "${var.resource_prefix}-ecs-task-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      }
    ]
  })

  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
  ]
}

コメントに記載している通り、サブネットIDとセキュリティグループIDはVPCリソース作成時のoutputから取得して編集します。

記述後は以下を実行します。

pushd ./infra/ecs
terraform init
terraform apply
popd

動作確認

ここまでのデプロイ後、マネジメントコンソールでECSタスクのIPを以下から確認します。

remix-container-build-on-ecs_2024-09-23-20-16-08

そして、http://{確認したIPアドレス}:3000/ にアクセスすれば前回と同じ画面が確認できました。

remix-container-build_2024-09-23-11-05-22

まとめ

いかがでしたでしょうか。今回はパブリックサブネットで確認しましたので、今後はプライベートサブネットとALBを組み合わせたケースも試してみたいと思います。
またterraformのモジュール化やecspressoも使用して、一発でデプロイできるような改善もやって行けたらと思います。

本記事がご参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.