RemixプロジェクトのコンテナをECSにデプロイしてみた
こんちには。
データ事業本部 インテグレーション部 機械学習チームの中村( @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
インストール方法は以下を参考にしています。
- Node.js
- pnpm
- docker
- terraform
- AWS CLI
やってみる
フォルダ構成
フォルダ構成は以下のようにしています。前回作成したものは 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を作成します。
# リソースプレフィックス
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するためのレポジトリを作ります。
# リソースプレフィックス
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まで行うスクリプトを作成します。
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
記述後は以下を実行します。
pushd ./infra/ecs
bash ecr_push.sh
popd
ECSの作成
最後にECSのリソースを作成します。
# リソースプレフィックス
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を以下から確認します。
そして、http://{確認したIPアドレス}:3000/
にアクセスすれば前回と同じ画面が確認できました。
まとめ
いかがでしたでしょうか。今回はパブリックサブネットで確認しましたので、今後はプライベートサブネットとALBを組み合わせたケースも試してみたいと思います。
またterraformのモジュール化やecspressoも使用して、一発でデプロイできるような改善もやって行けたらと思います。
本記事がご参考になれば幸いです。