
TerraformでECSにコンテナをデプロイする方法を解説!ECRへイメージをプッシュする方法もあわせて解説
Amazon ECS(Elastic Container Service)は、コンテナ化されたアプリケーションを簡単に実行・管理できる便利なサービスです。Terraformと組み合わせることで、インフラをコード化し、繰り返し可能かつ管理しやすい形でデプロイが可能です。
本記事は、「Hello OpsToday ECS」という言葉を表示させるWebページを作成して、それをECS上で起動する手順の解説記事となります。Terraformの基本的な知識を持つエンジニアが、より効率的にAWS環境を運用する方法を知りたい方に最適な内容です!
terraformでECRとECSを作成し、コンテナを起動する手順
実際にterraformでECRとECSを作成し、コンテナをAWS上で起動させる方法を解説します。
コンテナの概要やECSとは何か、ECRとは何か、についてはこちらの記事で解説していますので概要を知りたいという方は、まずはこちらをご覧ください。
1. HTMLファイルの作成
まずは、OpsToday-ECSフォルダ内にindex.htmlファイルを作成し、次の内容を記述してください。
<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8″> <meta name=”viewport” content=”width=device-width, initial-scale=1.0″> <title>Hello OpsToday ECS</title> </head> <body> <h1>Hello OpsToday ECS</h1> </body> </html> |
2. Docker イメージをビルド
HTMLファイルと同一のフォルダ内にDockerfileという名前のファイルを作成し、次の内容を Dockerfile に保存してください。
FROM nginx:alpine COPY index.html /usr/share/nginx/html/index.html |
Dockerfile作成が完了したら次のコマンドで、まずはローカル環境にDocker イメージをビルドします。
(base) apple@MacBook OpsToday-ECS % docker build -t hello-opstoday-ecs . ERROR: Cannot connect to the Docker daemon at unix:///Users/apple/.docker/run/docker.sock. Is the docker daemon running? |
上記エラーは、Dockerのアプリが起動していない場合に発生します。
Docekrアプリケーションを起動し、コマンドを再実行してください。

3. tfstateファイルを格納するためのS3バケットを作成する
terraformの基本的な使い方については以前の記事で解説しています。使い方がわからない場合や環境設定がまだの場合は、こちらの記事を参照してください。
001-00004のリンク
今回は、S3に「opstoday-terraform-tstate-0111」 という名前のバケットを作成してそのバケットの中にtfstateファイルを格納します。

今回は、「opstoday-terraform-tstate-0111」の名前でバケットを作成しましたが、バケット名は全世界で一意である必要があります。他のバケット名と被らないように環境に合わせて命名してください。

4. tfstateファイルの作成
VScodeで「tfstate.tf」という名前のファイルを作成し、下記のコードを記述します。
# tfstate管理バケット terraform { required_version = “= 1.9.6” #terraformのバージョン backend “s3” { bucket = “<ステップ6で作成したバケット名>” #AWSコンソール上のバケット名を入力 key = “opstoday-terrafom-tfstate-ecs” #オブジェクト名を入力 参照/新規作成 region = “ap-northeast-1” } } # プロバイダー指定 provider “aws” { region = “ap-northeast-1” } |
コードの入力が完了したら、VScodeのターミナル上で下記コマンドを実行します。
terraform init |
下記の画像のようにTerraform has been successfully initialized!と表示されていれば、tfstateファイルの作成が完了しています。

5. Terraformコードの作成
OpsToday-ECSフォルダ内に以下の4つのファイルを作成します。
- ecr.tf
- ecs.tf
- iam.tf
- network.tf
4つのファイル内にそれぞれコードを記述していきます。
ecr.tf
resource “aws_ecr_repository” “main” { name = “hello-world-opstoday” force_delete = true } output “repository_url” { value = aws_ecr_repository.main.repository_url } |
ecs.tf
resource “aws_ecs_cluster” “main” { name = “OpsToday-ecs-cluster” } locals { container_name = “apache-helloworld” } resource “aws_ecs_task_definition” “main” { family = “OpsToday-ecs-task-definition” cpu = 512 memory = 1024 requires_compatibilities = [“FARGATE”] execution_role_arn = aws_iam_role.ecs_task_execution_role.arn network_mode = “awsvpc” container_definitions = jsonencode([ { “name”: local.container_name, “image”: “${aws_ecr_repository.main.repository_url}:latest”, “cpu”: 0, “portMappings”: [ { “name”: local.container_name, “containerPort”: 80, “hostPort”: 80, “protocol”: “tcp”, “appProtocol”: “http” } ], “essential”: true, “environment”: [], “environmentFiles”: [], “mountPoints”: [], “volumesFrom”: [], “logConfiguration”: { “logDriver”: “awslogs”, “options”: { “awslogs-create-group”: “true”, “awslogs-group”: “/ecs/OpsToday-ecs-task-definition”, “awslogs-region”: “ap-northeast-1”, “awslogs-stream-prefix”: “ecs” } } } ]) } resource “aws_ecs_service” “main” { name = “OpsToday-ecs-service” cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.main.arn launch_type = “FARGATE” desired_count = 2 network_configuration { subnets = [ aws_subnet.public1.id, aws_subnet.public2.id ] security_groups = [ aws_default_security_group.default.id ] assign_public_ip = true } load_balancer { target_group_arn = aws_lb_target_group.main.arn container_name = local.container_name container_port = 80 } } |
iam.tf
resource “aws_iam_role” “ecs_task_execution_role” { name = “OpsToday-ecs-task-execution-role” assume_role_policy = <<-EOS { “Version”: “2008-10-17”, “Statement”: [ { “Sid”: “”, “Effect”: “Allow”, “Principal”: { “Service”: “ecs-tasks.amazonaws.com” }, “Action”: “sts:AssumeRole” } ] } EOS } resource “aws_iam_policy” “ecs_task_execution_role_policy” { name = “OpsToday-ecs-task-execution-role-policy” policy = <<-EOS { “Version”: “2012-10-17”, “Statement”: [ { “Effect”: “Allow”, “Action”: [ “ecr:GetAuthorizationToken”, “ecr:BatchCheckLayerAvailability”, “ecr:GetDownloadUrlForLayer”, “ecr:BatchGetImage”, “logs:CreateLogStream”, “logs:PutLogEvents”, “logs:CreateLogGroup” ], “Resource”: “*” } ] } EOS } resource “aws_iam_role_policy_attachment” “ecs_task_execution_role_policy” { role = aws_iam_role.ecs_task_execution_role.name policy_arn = aws_iam_policy.ecs_task_execution_role_policy.arn } |
network.tf
resource “aws_vpc” “main” { cidr_block = “10.0.0.0/16” enable_dns_support = true enable_dns_hostnames = true tags = { Name = “OpsToday-ecs” } } resource “aws_subnet” “public1” { vpc_id = aws_vpc.main.id cidr_block = “10.0.0.0/20” availability_zone = “ap-northeast-1a” tags = { Name = “OpsToday-ecs-public1-ap-northeast-1a” } } resource “aws_subnet” “public2” { vpc_id = aws_vpc.main.id cidr_block = “10.0.16.0/20” availability_zone = “ap-northeast-1c” tags = { Name = “OpsToday-ecs-public2-ap-northeast-1c” } } resource “aws_internet_gateway” “main” { vpc_id = aws_vpc.main.id tags = { Name = “OpsToday-ecs” } } resource “aws_route_table” “public” { vpc_id = aws_vpc.main.id route { cidr_block = “0.0.0.0/0” gateway_id = aws_internet_gateway.main.id } tags = { Name = “OpsToday-ecs-rtb-public” } } resource “aws_route_table_association” “public1” { subnet_id = aws_subnet.public1.id route_table_id = aws_route_table.public.id } resource “aws_route_table_association” “public2” { subnet_id = aws_subnet.public2.id route_table_id = aws_route_table.public.id } resource “aws_default_security_group” “default” { vpc_id = aws_vpc.main.id ingress { protocol = -1 self = true from_port = 0 to_port = 0 } ingress { protocol = “tcp” cidr_blocks = [“0.0.0.0/0”] from_port = 80 to_port = 80 } egress { protocol = -1 cidr_blocks = [“0.0.0.0/0”] from_port = 0 to_port = 0 } } resource “aws_lb” “main” { name = “OpsToday-ecs-alb” internal = false load_balancer_type = “application” security_groups = [aws_default_security_group.default.id] subnets = [aws_subnet.public1.id, aws_subnet.public2.id] } resource “aws_lb_listener” “main” { load_balancer_arn = aws_lb.main.arn port = “80” protocol = “HTTP” default_action { type = “forward” target_group_arn = aws_lb_target_group.main.arn } } resource “aws_lb_target_group” “main” { name = “OpsToday-ecs-targetgroup” port = 80 protocol = “HTTP” vpc_id = aws_vpc.main.id target_type = “ip” } |
4ファイル全てにコードをかけたら下記コマンドで文法に誤りがないかチェックを行います。
terraform plan |
エラーが出ることなく、下記のように18個のリソースを作成するという出力となっていれば問題ありません。
Plan: 18 to add, 0 to change, 0 to destroy. |
6. AWSリソースを作成する
terraform apply |
上記コマンドを実行して実際にAWS上にリソースをデプロイします。
下記のように、リソース作成を行って良いかの確認が入るため、問題なければ「yes」を入力して実行します。
Do you want to perform these actions? Terraform will perform the actions described above. Only ‘yes’ will be accepted to approve. Enter a value: |
デプロイには数分かかりますが、
Apply complete! Resources: 18 added, 0 changed, 0 destroyed.
の文字が出力されていればリソース作成完了です。
7. イメージを ECR にプッシュ
リソース作成補は、正しくリソースが作成されているか確認をしましょう。

作成したリポジトリのリポジトリ名をクリックし、右上の「プッシュコマンドを表示」をクリックします。

プッシュコマンドが記載されたポップアップが表示されるため、ここに記載されている手順通りコマンドを入力し、ECR上にDockerファイルをプッシュします。

プッシュが完了したら、ページを更新しイメージが作成されているか確認してください。

最後にECSのタスクを開き、その中に記載されているIPアドレスをWebで検索し、ページが開かれるか確認します。
「Hello world OpsToday」と表示されていれば成功です。

今回利用したリソースは、下記コマンドで削除できます。
terraform destroy |
場合によっては高額な料金が発生するため、不要なリソースは削除しましょう。
まとめ
本記事では、Terraformを使用してAWSのECSにコンテナをデプロイする手順を解説しました。シンプルな「Hello OpsToday ECS」アプリを例に、インフラのコード化からECS上でのサービス実行までを実践しました。
Terraformを活用することで、複雑なAWSリソースの構築や管理が効率的になり、再現性やスケーラビリティの向上が期待できます。今回の手順を基に、さらに高度な構成や自動化の実現に挑戦してみてください。
ECSの運用を最適化し、クラウド環境を効果的に活用する第一歩を踏み出しましょう!