AWSにRails + Nginxな環境をTerraformで構築してみようと思います。
はじめに
本連載で一つずつインフラを構築していきます。
ドメインのhttps化したり、ECS Fargateを使用したコンテナオーケストレーションを用いてアプリケーションをデプロイします。
この記事ではRails + NginxをECS Fargateで起動をします。
環境は以下です。
| OS | Cataline 10.15.6 |
| Terraform | 0.14.4 |
基本構文などこちらにまとめてますので、よかったらみてください!
AWS Terraform 基本コード まとめ
連載一覧
- terraform AWS環境構築 事前準備
- 【ネットワーク環境構築】terraform AWS環境構築 第1回
- 【ドメインhttps化・ACM(SSL)証明書発行】terraform AWS環境構築 第2回
- 【ロードバランサー構築】terraform AWS環境構築 第3回
- 【ECS Fargate(nginx)実行】terraform AWS環境構築 第4回
- 【RDS構築】terraform AWS環境構築 第5回
- 【Docker/ECR作成】terraform AWS環境構築 第6回
- 【ECS Fargate(rails + nginx)実行】terraform AWS環境構築 第7回 ←ここ
- 【CircleCIによるCI/CD】terraform AWS環境構築 番外
やること
基本は、第4回で定義することは変わらないですね。
- ECSクラスター:ドキュメント
- CloudWatchLogs:ドキュメント
- タスク定義:ドキュメント
- ターゲットグループ:ドキュメント
- ロードバランサーリスナルール:ドキュメント
- ECSサービス:ドキュメント
各々説明は省きます!パラメーターの変数が変わるくらいなので。
ECS Fargate(rails + nginx)の作成
NginxのECSは不要なので、コメントアウトしておきましょう。
./main.tf
module "network" {
source = "./network"
app_name = var.app_name
}
module "elb" {
source = "./elb"
app_name = var.app_name
vpc_id = module.network.vpc_id
public_subnet_ids = module.network.public_subnet_ids
acm_id = module.acm.acm_id
domain = var.domain
}
module "acm" {
source = "./acm"
domain = var.domain
}
module "rds" {
source = "./rds"
app_name = var.app_name
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
vpc_id = module.network.vpc_id
alb_security_group = module.elb.alb_security_group
private_subnet_ids = module.network.private_subnet_ids
}
#module "ecs_nginx" {
# source = "./ecs_nginx"
#
# vpc_id = module.network.vpc_id
# http_listener_arn = module.elb.http_listener_arn
# https_listener_arn = module.elb.https_listener_arn
# cluster_name = module.ecs_cluster.cluster_name
# public_subnet_ids = module.network.public_subnet_ids
#}
module "ecs_cluster" {
source = "./ecs_cluster"
}
ecs railsモジュール作成
Rails + Nginx用のECSを作成するモジュールを作成します。
terraformフォルダ内にecs_railsフォルダを作成しましょう。main.tfも作成しておきます。
ecs_railsモジュールを使用できるよう./main.tfに以下を追記します。
./main.tf
module "ecs_rails" {
source = "./ecs_rails"
}
ディレクトリ構成は以下のようにしています。
[terraform] $ tree . ├── ecs_rails │ └── main.tf ├── ecs_cluster │ ├── main.tf │ └── output.tf ├── ecs_nginx │ ├── container_definitions.json │ ├── data.tf │ ├── main.tf │ └── variable.tf ├── iam_role │ ├── data.tf │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── elb │ ├── data.tf │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── acm │ ├── data.tf │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── network │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── env │ └── backend.config ├── main.tf ├── output.tf ├── backend.tf ├── provider.tf ├── terraform.tfvars └── variable.tf
用意できたら、terraformの初期化を行いましょう。
[terraform] $ terraform init -backend-config=env/backend.config -upgrade
ECSクラスターの作成
./ecs_cluster/main.tf
locals {
# 第4回から名前変更
name = "rails-hello"
}
resource "aws_ecs_cluster" "ecs_cluster" {
name = local.name
}
CloudWatchLogsの作成
railsとnginx、2つのログ出力先を作成します。
./ecs_rails/variable.tf に以下を追記します。
variable "apps_name" {
type = list(string)
default = ["nginx", "rails"]
}
./ecs_rails/main.tf に以下を追記します。
resource "aws_cloudwatch_log_group" "log" {
count = length(var.apps_name)
name = "/ecs/rails-hello/${var.apps_name[count.index]}"
}
タスク定義の作成
コンテナの定義
./ecs_rails/container_definitions.json
[
{
"name": "nginx",
"image": "${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
"essential": true,
"memory": 128,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "nginx",
"awslogs-group": "/ecs/rails-hello/nginx"
}
},
"portMappings": [
{
"containerPort": 80,
"hostPort": 80
}
]
},
{
"name": "rails",
"image":"${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/rails_hello:latest",
"essential": true,
"memory": 128,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "rails",
"awslogs-group": "/ecs/rails-hello/rails"
}
},
"command": ["bundle","exec","puma","-C","config/puma.rb"],
"environment": [
{
"name": "RDS_HOST",
"value": "${db_host}"
},
{
"name": "RDS_USERNAME",
"value": "${db_username}"
},
{
"name": "RDS_PASSWORD",
"value": "${db_password}"
},
{
"name": "RDS_PORT",
"value": "3306"
},
{
"name": "RDS_DB_NAME",
"value": "${db_name}"
},
{
"name": "RAILS_MASTER_KEY",
"value": "${master_key}"
},
{
"name": "RAILS_LOG_TO_STDOUT",
"value": "true"
},
{
"name": "RAILS_SERVE_STATIC_FILES",
"value": "true"
}
],
"portMappings": [
{
"containerPort": 3000,
"hostPort": 3000
}
]
}
]
- name:コンテナ名
- image:コンテナイメージ
- essential:trueのコンテナが落ちると全てのコンテナが落ちる
- memory:コンテナに適用するメモリの量
- logConfiguration:log設定
- portMappings:ポートマッピング
- command:コンテナ実行コマンド
- environment:環境変数
ECSタスク実行IAMロールの作成
第4回と同じです。
./ecs_rails/data.tf
# AmazonECSTaskExecutionRolePolicy の参照
data "aws_iam_policy" "ecs_task_execution_role_policy" {
arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
./ecs_rails/main.tf
module "ecs_task_execution_role" {
source = "../iam_role"
name = "ecs-task-execution"
identifier = "ecs-tasks.amazonaws.com"
policy_arn = data.aws_iam_policy.ecs_task_execution_role_policy.arn
}
タスク定義
第4回と同じく、container_definitions.jsonをData Sourceで読み込みますが、
今回は以下の変数を渡しています。
- account_id :AWSのアカウントID
- db_host :RDSのエンドポイント
- db_username :DBユーザ名
- db_password:DBパスワード
- db_name :DB名
- master_key :RAILSのMASTER KEY
account_id はローカル変数、それ以外は環境変数として定義します。
./terraform.tfvars
db_name = "rails-hello" db_username = "root" db_password = "password" master_key = "<your rails master_key>" db_database = "rails_hello"
./variable.tf に以下を追記します。
variable "db_name" {}
variable "db_username" {}
variable "db_password" {}
variable "master_key" {}
./ecs_rails/main.tf に以下を追記します。
locals {
account_id = data.aws_caller_identity.user.account_id
}
./main.tf
module "ecs_rails" {
source = "./ecs_rails"
app_name = var.app_name
#追記
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
db_host = module.rds.db_endpoint
master_key = var.master_key
vpc_id = module.network.vpc_id
http_listener_arn = module.elb.http_listener_arn
https_listener_arn = module.elb.https_listener_arn
alb_security_group = module.elb.alb_security_group
cluster_name = module.ecs_cluster.cluster_name
public_subnet_ids = module.network.public_subnet_ids
}
./ecs_rails/variable.tf に以下を追記します。
variable "db_name" {}
variable "db_username" {}
variable "db_password" {}
variable "db_host" {}
variable "master_key" {}
./ecs_rails/data.tf に以下を追記します。
data "aws_caller_identity" "user" {}
data "template_file" "container_definitions" {
template = file("./ecs_rails/container_definitions.json")
vars = {
account_id = local.account_id
db_host = var.db_host
db_username = var.db_username
db_password = var.db_password
db_name = var.db_name
# RAILS MASTER KEY
master_key = var.master_key
}
}
ターゲットグループの作成
こちらも第4回と同じです。
./ecs_rails/main.tf に以下を追記します。
resource "aws_lb_target_group" "target_group" {
# _ が使えなかった。。
name = "rails-hello"
vpc_id = var.vpc_id
# ALBからECSタスクのコンテナへトラフィックを振り分ける設定
port = 80
protocol = "HTTP"
target_type = "ip"
# コンテナへの死活監視設定
health_check {
port = 80
path = "/"
}
}
ロードバランサーリスナルールの作成
こちらも第4回と同じです。
./ecs_rails/main.tf に以下を追記します。
resource "aws_lb_listener_rule" "http_rule" {
listener_arn = var.http_listener_arn
# 受け取ったトラフィックをターゲットグループへ受け渡す
action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group.id
}
# ターゲットグループへ受け渡すトラフィックの条件
condition {
path_pattern {
values = ["*"]
}
}
}
resource "aws_lb_listener_rule" "https_rule" {
listener_arn = var.https_listener_arn
action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group.id
}
condition {
path_pattern {
values = ["*"]
}
}
}
ECSサービスの作成
こちらも第4回と同じですが、セキュリティーグループをALBのものと同じにします。
./main.tf
module "ecs_rails" {
source = "./ecs_rails"
app_name = var.app_name
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
db_host = module.rds.db_endpoint
master_key = var.master_key
vpc_id = module.network.vpc_id
http_listener_arn = module.elb.http_listener_arn
https_listener_arn = module.elb.https_listener_arn
# 追記
alb_security_group = module.elb.alb_security_group
cluster_name = module.ecs_cluster.cluster_name
public_subnet_ids = module.network.public_subnet_ids
}
./ecs_rails/variable.tf に以下を追記します。
variable "alb_security_group" {}
./ecs_rails/main.tf に以下を追記します。
resource "aws_ecs_service" "ecs_service" {
name = "${var.app_name}-service"
launch_type = "FARGATE"
desired_count = "1"
cluster = var.cluster_name
task_definition = aws_ecs_task_definition.task_definition.arn
network_configuration {
security_groups = [var.alb_security_group]
subnets = var.public_subnet_ids
assign_public_ip = true
}
load_balancer {
target_group_arn = aws_lb_target_group.target_group.arn
container_name = "nginx"
container_port = 80
}
}
terraform plan, apply して作成されるか見てみましょう。
問題なく作成できたら、ドメインにアクセスしてみましょう。

正常に表示されていますね!これで完了です!
DB作成
画面表示のみでDBは使用しないので作成しなくてもいいのですが、後々使用すると思うのでここで作成しておきましょう。
既存のタスク定義をオーバーライドして、DB作成コマンドを実行するテスクを定義し実行します。
どのコンテンをどういうふうにオーバーライドするか定義するファイルを作成します。
./ecs_rails/db_create/run_task_db_create.json
{
"containerOverrides": [
{
"name": "rails",
"command": ["bundle", "exec", "rails", "db:create"]
}
]
}
railsコンテナを実行するコマンドをbundle exec rails db:createに上書きします。
aws ecs run-taskコマンドでタスクを実行します。
ただ、実行するにはパラメーターが必要になります。これもaws ecsとjqコマンドを使用してパラメーターを設定します。
[rails_hello] $ config=`aws ecs describe-services --cluster rails-hello --services rails_hello-service | jq ".services[0].networkConfiguration"`
[rails_hello] $ <span class="token assign-left variable">subnets</span><span class="token operator">=</span><span class="token variable">`<span class="token builtin class-name">echo</span> $config <span class="token operator">|</span> jq -r <span class="token string">'.awsvpcConfiguration.subnets|join(",")'</span>`
</span>[rails_hello] $ <span class="token assign-left variable">securityGroups</span><span class="token operator">=</span><span class="token variable">`<span class="token builtin class-name">echo</span> $config <span class="token operator">|</span> jq -r <span class="token string">'.awsvpcConfiguration.securityGroups|join(",")'</span>`
</span>[rails_hello] $ <span class="token assign-left variable">assignPublicIp</span><span class="token operator">=</span><span class="token variable">`<span class="token builtin class-name">echo</span> $config <span class="token operator">|</span> jq -r <span class="token string">'.awsvpcConfiguration.assignPublicIp'</span>`
</span>
[rails_hello] $ aws ecs run-task \
--cluster rails-hello \
--task-definition rails_hello \
--network-configuration "awsvpcConfiguration={subnets=[${subnets}],securityGroups=[${securityGroups}],assignPublicIp=${assignPublicIp}}" \
--overrides file://ecs_rails/db_create/run_task_db_create.json \
--launch-type FARGATE
実行できるともう1つ追加でタスクが実行されると思います。
実行されたタスクのログを見てみましょう。

DBを作成完了したログが出力されていますね。これで、DBの作成は完了です!
まとめ
今回、作成したコードとディレクトリ構成は以下になります。
[terraform] $ tree . ├── ecs_rails │ ├── container_definitions.json │ ├── data.tf │ ├── main.tf │ └── variable.tf ├── rds │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── ecs_cluster │ ├── main.tf │ └── output.tf ├── ecs_nginx │ ├── container_definitions.json │ ├── data.tf │ ├── db_create │ │ └── run_task_db_create.json │ ├── main.tf │ └── variable.tf ├── iam_role │ ├── data.tf │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── elb │ ├── data.tf │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── acm │ ├── data.tf │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── network │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── env │ └── backend.config ├── main.tf ├── output.tf ├── backend.tf ├── provider.tf ├── terraform.tfvars └── variable.tf
./ecs_rails/main.tf
locals {
account_id = data.aws_caller_identity.user.account_id
}
module "ecs_task_execution_role" {
source = "../iam_role"
name = "ecs-task-execution"
identifier = "ecs-tasks.amazonaws.com"
policy.arn = data.aws_iam_policy.ecs_task_execution_role_policy.arn
}
resource "aws_ecs_task_definition" "task_definition" {
family = var.app_name
cpu = 256
memory = 512
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
container_definitions = data.template_file.container_definitions.rendered
execution_role_arn = module.ecs_task_execution_role.iam_role_arn
}
resource "aws_cloudwatch_log_group" "log" {
count = length(var.apps_name)
name = "/ecs/rails-hello/${var.apps_name[count.index]}"
}
resource "aws_lb_target_group" "target_group" {
name = "rails-hello"
vpc_id = var.vpc_id
port = 80
protocol = "HTTP"
target_type = "ip"
health_check {
port = 80
path = "/"
}
}
resource "aws_lb_listener_rule" "http_rule" {
listener_arn = var.http_listener_arn
action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group.id
}
condition {
path_pattern {
values = ["*"]
}
}
}
resource "aws_lb_listener_rule" "https_rule" {
listener_arn = var.https_listener_arn
action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group.id
}
condition {
path_pattern {
values = ["*"]
}
}
}
resource "aws_ecs_service" "ecs_service" {
name = "${var.app_name}-service"
launch_type = "FARGATE"
desired_count = "1"
cluster = var.cluster_name
task_definition = aws_ecs_task_definition.task_definition.arn
network_configuration {
security_groups = [var.alb_security_group]
subnets = var.public_subnet_ids
assign_public_ip = true
}
load_balancer {
target_group_arn = aws_lb_target_group.target_group.arn
container_name = "nginx"
container_port = 80
}
}
./ecs_rails/data.tf
data "aws_caller_identity" "user" {}
data "template_file" "container_definitions" {
template = file("./ecs_rails/container_definitions.json")
vars = {
account_id = local.account_id
db_host = var.db_host
db_username = var.db_username
db_password = var.db_password
db_name = var.db_name
master_key = var.master_key
}
}
# AmazonECSTaskExecutionRolePolicy の参照
data "aws_iam_policy" "ecs_task_execution_role_policy" {
arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
#「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 = ["*"]
}
}
./ecs_rails/variable.tf
variable "app_name" {}
variable "db_name" {}
variable "db_username" {}
variable "db_password" {}
variable "db_host" {}
variable "master_key" {}
variable "apps_name" {
type = list(string)
default = ["nginx", "rails"]
}
variable "vpc_id" {}
variable "http_listener_arn" {}
variable "https_listener_arn" {}
variable "alb_security_group" {}
variable "cluster_name" {}
variable "public_subnet_ids" {}
./ecs_rails/container_definitions.json
[
{
"name": "nginx",
"image": "${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
"essential": true,
"memory": 128,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "nginx",
"awslogs-group": "/ecs/rails-hello/nginx"
}
},
"portMappings": [
{
"containerPort": 80,
"hostPort": 80
}
]
},
{
"name": "rails",
"image":"${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/rails_hello:latest",
"essential": true,
"memory": 128,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "rails",
"awslogs-group": "/ecs/rails-hello/rails"
}
},
"command": ["bundle","exec","puma","-C","config/puma.rb"],
"environment": [
{
"name": "RDS_HOST",
"value": "${db_host}"
},
{
"name": "RDS_USERNAME",
"value": "${db_username}"
},
{
"name": "RDS_PASSWORD",
"value": "${db_password}"
},
{
"name": "RDS_PORT",
"value": "3306"
},
{
"name": "RDS_DB_NAME",
"value": "${db_name}"
},
{
"name": "RAILS_MASTER_KEY",
"value": "${master_key}"
},
{
"name": "RAILS_LOG_TO_STDOUT",
"value": "true"
},
{
"name": "RAILS_SERVE_STATIC_FILES",
"value": "true"
}
],
"portMappings": [
{
"containerPort": 3000,
"hostPort": 3000
}
]
}
]
./ecs_rails/db_create/run_task_db_create.json
{
"containerOverrides": [
{
"name": "rails",
"command": ["bundle", "exec", "rails", "db:create"]
}
]
}

コメント