【ECS Fargate(rails + nginx)実行】terraform AWS環境構築 第7回

AWS

AWSRails + Nginxな環境をTerraformで構築してみようと思います。

はじめに

本連載で一つずつインフラを構築していきます。

ドメインのhttps化したり、ECS Fargateを使用したコンテナオーケストレーションを用いてアプリケーションをデプロイします。

この記事ではRails + NginxECS Fargateで起動をします。

環境は以下です。

OS Cataline 10.15.6
Terraform 0.14.4

 

 

 

基本構文などこちらにまとめてますので、よかったらみてください!
AWS Terraform 基本コード まとめ

連載一覧

やること

基本は、第4回で定義することは変わらないですね。

各々説明は省きます!パラメーターの変数が変わるくらいなので。

ECS Fargate(rails + nginx)の作成

NginxECSは不要なので、コメントアウトしておきましょう。

./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の作成

railsnginx、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.jsonData Sourceで読み込みますが、
今回は以下の変数を渡しています。

  • account_id :AWSのアカウントID
  • db_host RDSのエンドポイント
  • db_username DBユーザ名
  • db_passwordDBパスワード
  • db_name DB
  • master_key :RAILSMASTER 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 ecsjqコマンドを使用してパラメーターを設定します。

[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"]
      }
  ]
}

コメント

タイトルとURLをコピーしました