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"] } ] }
コメント