2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Terraformを使ってAWSのECS Fargate(Django+Nginx)を構築しよう!

Last updated at Posted at 2023-08-27

概要

今回はTerraformを使ってECS Fargateを構築します
かなり長い記事になっていて難易度が高いですが一緒に頑張っていきましょう
構成は下記の通りです

ecs構成図.drawio.png

前提

  • Terraformのmain.tfを作成済み
  • VPC、プライベートサブネットをはじめとするネットワークを構築済み
  • RDSを構築済み
  • Djangoのプロジェクトを作成済み
  • パッケージ管理はPoetryを使って行います
  • アプリケーションサーバはGunicornを使用します

main.tfをまだ作成していない方は下記の記事を参考にしてください

Terraformを使ってネットワークを構築する方法について知りたい方は以下の記事を参考にしてください

Terraformを使ってRDSを構築する方法について知りたい方は以下の記事を参考にしてください

また、コンテナ経由でTerraformを使用すると複数ブロジェクトで使用する際にバージョンによる違いを意識せずに済みます
コンテナを使用したい方はこちらの記事も参考にしてみてください

ディレクトリ構成

構成は以下の通りです

tree 
.
├── application
│   ├── application
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── migrations
│   │   │   ├── __init__.py
│   │   │   └── 0001_initial.py
│   │   ├── models.py
│   │   ├── serializers.py
│   │   ├── tests.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── project
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── dev.py
│   │   │   ├── environment.py
│   │   │   └── local.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── manage.py
│   ├── poetry.lock
│   └── pyproject.toml
├── containers
│   ├── django
│   │   ├── Dockerfile
│   │   └── entrypoint.sh
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
└── infra
    ├── cloudwatch.tf
    ├── database.tf
    ├── ecs.tf
    ├── main.tf
    ├── network.tf
    ├── templates
    │   └── ecs
    │       └── taskdef.json.tpl
    ├── terraform.tfvars
    └── variables.tf

まずは、

  • variables.tf
  • database.tf
  • ecs.tf
  • templates.json.tpl
  • terraform.tfvars

の順にTerraformの設定を記載していきます

その後

  • Dockerfileの作成
  • 手動でECRとIAMロール作成と設定

をします
最後に実際にTerraformを実行し、自動デプロイしていきます

variables

必要な変数の設定を行います

  • ecr_image_app
  • ecr_image_web

に関しましては後述するタスク定義とECRリポジトリの作成でも登場します

variables.tf
# ------------------------------
# Variables
# ------------------------------

# プロジェクトを識別する一意の識別子
variable "prefix" {
  default = "tf-pg"
}

variable "project" {
  default = "terraform-playground"
}

variable "owner" {
  default = "shun198"
}

variable "db_username" {
  description = "Username for RDB MySQL Instance"
}

variable "db_password" {
  description = "Password for RDB MySQL Instance"
}

variable "ecr_image_app" {
  description = "ECR Image URI for Django App"
  default = "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/tf-pg/django"
}

variable "ecr_image_web" {
  description = "ECR Image URI for Nginx"
  default = "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/tf-pg/nginx"
}

cloudwatch.tf

ECS内のCloudWatchの設定を行います

  • Django
  • Nginx

の2種類のロググループを作成します

cloudwatch.tf
# Django
resource "aws_cloudwatch_log_group" "app" {
  name = "${local.prefix}-app"

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-cloudwatch-logs" })
  )
}

# Nginx
resource "aws_cloudwatch_log_group" "web" {
  name = "${local.prefix}-web"

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-cloudwatch-logs" })
  )
}

database.tf

RDSのセキュリティグループのインバウンドルールに後ほど作成するECSのセキュリティグループを追加します

database.tf
# ------------------------------
# Database Configuration
# ------------------------------
resource "aws_db_subnet_group" "main" {
  name = "${local.prefix}-main"
  subnet_ids = [
    aws_subnet.private_a.id,
    aws_subnet.private_c.id,
  ]


  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-rds-subnet-group" })
  )
}

resource "aws_security_group" "rds" {
  description = "Allow access to RDS"
  name        = "${local.prefix}-rds-sg"
  vpc_id      = aws_vpc.main.id

  ingress {
    protocol  = "tcp"
    from_port = 5432
    to_port   = 5432
    security_groups = [
      # ECSからRDSへのアクセスを許可する
      aws_security_group.ecs_sg.id
    ]

  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-rds-sg" })
  )
}

resource "aws_db_instance" "main" {
  identifier              = "${local.prefix}-db"
  db_name                 = "postgres"
  allocated_storage       = 20
  storage_type            = "gp2"
  engine                  = "postgres"
  engine_version          = "15.2"
  instance_class          = "db.t3.small"
  db_subnet_group_name    = aws_db_subnet_group.main.name
  password                = var.db_password
  username                = var.db_username
  backup_retention_period = 0
  multi_az                = false
  skip_final_snapshot     = true
  vpc_security_group_ids  = [aws_security_group.rds.id]

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-rds-instance" })
  )
}

ecs.tf

ecs.tf内で

  • クラスター
  • タスク定義ファイル
  • タスク定義
  • セキュリティグループ

の設定を行います

ecs.tf
# ------------------------------
# ECS Configuration
# ------------------------------
resource "aws_ecs_cluster" "main" {
  name = "${local.prefix}-cluster"

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-cluster" })
  )
}

# ECSのタスク定義用のtplファイルの設定
data "template_file" "app_container_definitions" {
  template = file("./templates/ecs/taskdef.json.tpl")

  vars = {
    log_group_name_app = aws_cloudwatch_log_group.app.name
    log_group_name_web = aws_cloudwatch_log_group.web.name
    ecr_image_app = var.ecr_image_app
    ecr_image_web = var.ecr_image_web
  }
}

# タスク定義
resource "aws_ecs_task_definition" "app" {
  family                   = "${local.prefix}-app"
  container_definitions    = data.template_file.app_container_definitions.rendered
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512
  execution_role_arn = "arn:aws:iam::XXXXXXXXXXXX:role/tf-pg-dev-task-exec-role"
  task_role_arn =  "arn:aws:iam::XXXXXXXXXXXX:role/tf-pg-dev-task-role"
  container_definitions    = data.template_file.app_container_definitions.rendered

  volume {
    name = "tmp-data"
  }

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-task-def" })
  )
}

# ECSのセキュリテーグループ
resource "aws_security_group" "ecs_sg" {
  description = "Access for the ECS Service"
  name        = "${local.prefix}-ecs-sg"
  vpc_id      = aws_vpc.main.id

  # ECSからPublicな通信へのアウトバウンドアクセスを許可
  egress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # ECSからPostgresへのアウトバウンドアクセスを許可
  egress {
    from_port = 5432
    to_port   = 5432
    protocol  = "tcp"
    cidr_blocks = [
      aws_subnet.private_a.cidr_block,
      aws_subnet.private_c.cidr_block,
    ]
  }

  # Publicな通信からNginxへのインバウンドアクセスを許可
  # 全ての通信をNginxを経由させたいのでECSの8000ポートへ直接アクセスさせない
  ingress {
    from_port = 80
    to_port   = 80
    protocol  = "tcp"
    cidr_blocks = [
      "0.0.0.0/0"
    ]
  }

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-sg" })
  )
}

resource "aws_ecs_service" "app" {
  name            = "${local.prefix}-app"
  cluster         = aws_ecs_cluster.main.name
  task_definition = aws_ecs_task_definition.app.family
  # 今回は検証用のためタスクを1つだけ実行させる
  desired_count    = 1
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  network_configuration {
    subnets = [
      aws_subnet.private_a.id,
      aws_subnet.private_c.id,
    ]
    security_groups  = [aws_security_group.ecs_sg.id]
  }

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-service" })
  )
}

一つずつ解説します

クラスター

クラスターを以下のように作成します

resource "aws_ecs_cluster" "main" {
  name = "${local.prefix}-cluster"

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-cluster" })
  )
}

タスク定義ファイル

コンテナの設定を記載するタスク定義ファイルを作成します
また、コンテナを起動させる際に環境変数が必要になるので今回はAWSのパラメータストアに環境変数を格納していきます

環境変数の作成

作成する環境変数は以下のとおりです
今回作成するパスは/tf-pg/dev/{環境変数名}にします

環境変数 説明
DJANGO_SETTINGS_MODULE Djangoの設定ファイルを指定するための環境変数
今回はdev環境をデプロイするのでproject.settings.devを指定します
SECRET_KEY Djangoのシークレットキー
ALLOWED_HOSTS Djangoのアプリケーションへのアクセスを許可するオリジン
今回は*(ワイルドカード)を指定します
POSTGRES_NAME PostgresのDB名
POSTGRES_USER Postgresのユーザ名
POSTGRES_PASSWORD Postgresのパスワード
POSTGRES_HOST Postgresのホスト名
POSTGRES_PORT Postgresのポート番号

タスク定義を作成する際はパラメータストアに格納された各環境変数のarnが必要になってくるので

aws ssm get-parameters-by-path --path "/tf-pg/dev/"  

を実行するとパラメータのarnを含む詳細を取得できます

        {
            "Name": "/tf-pg/dev/ALLOWED_HOSTS",
            "Type": "String",
            "Value": "*",
            "Version": 1,
            "LastModifiedDate": 1692970086.203,
            "ARN": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/ALLOWED_HOSTS",
            "DataType": "text"
        },
        {
            "Name": "/tf-pg/dev/DJANGO_SETTINGS_MODULE",
            "Type": "String",
            "Value": "project.settings.dev",
            "Version": 1,
            "LastModifiedDate": 1692969979.44,
            "ARN": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/DJANGO_SETTINGS_MODULE",
            "DataType": "text"
        },
        {
            "Name": "/tf-pg/dev/POSTGRES_HOST",
            "Type": "SecureString",
            "Value": "AQICAHi9FCyTN3Cx9X97OrqEYX75d2ri/mNsGlu32o4ATI7IIgGG0EZgiE2FFsuhXbOgzkfAAAAAnDCBmQYJKoZIhvcNAQcGoIGLMIGIAgEAMIGCBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDCCdXbyl84z4/eHMMgIBEIBVptSBcKfw8E6zRA+FcMYruM7OtEpDPmLLgjjRr6xWCeT8D66cCQDrpO+nJOlozWdRk1sPCuoP8B+oaYCTcJdGgm0D+84fKnEqS+OrZaSGnomjG2fxFw==",
            "Version": 1,
            "LastModifiedDate": 1692970064.661,
            "ARN": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/POSTGRES_HOST",
            "DataType": "text"
        },
(略)

taskdef.json.tpl

タスク定義を作成します
先ほど作成した環境変数のarnをsecretsのvalueFromに記載します
containerPathに関しましては今回は/code/tmpにします
Dockerfileの作成でも後ほど説明します

[
    {
        "name" : "app",
        "image" : "${ecr_image_app}",
        "cpu" : 0,
        "portMappings" : [
            {
                "containerPort" : 8000,
                "hostPort" : 8000,
                "protocol" : "tcp",
                "appProtocol" : "http"
            }
        ],
        "essential" : true,
        "secrets": [
            {
                "name" : "POSTGRES_USER",
                "valueFrom" : "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/POSTGRES_USER"
            },
            {
                "name" : "DJANGO_SETTINGS_MODULE",
                "valueFrom" : "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/DJANGO_SETTINGS_MODULE"
            },
            {
                "name" : "POSTGRES_HOST",
                "valueFrom" : "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/POSTGRES_HOST"
            },
            {
                "name" : "ALLOWED_HOSTS",
                "valueFrom" : "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/ALLOWED_HOSTS"
            },
            {
                "name" : "SECRET_KEY",
                "valueFrom" : "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/SECRET_KEY"
            },
            {
                "name" : "POSTGRES_PASSWORD",
                "valueFrom" : "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/POSTGRES_PASSWORD"
            },
            {
                "name" : "POSTGRES_PORT",
                "valueFrom" : "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/POSTGRES_PORT"
            },
            {
                "name" : "POSTGRES_NAME",
                "valueFrom" : "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/tf-pg/dev/POSTGRES_NAME"
            }
        ],
        "entryPoint" : [
            "/usr/local/bin/entrypoint.sh"
        ],
        "mountPoints" : [
        {
            "sourceVolume" : "tmp-data",
            "containerPath" : "/code/tmp"
        }
        ],
        "logConfiguration" : {
            "logDriver" : "awslogs",
            "options" : {
                "awslogs-group" : "${log_group_name_app}",
                "awslogs-region" : "ap-northeast-1",
                "awslogs-stream-prefix" : "app"
            }
        }
    },
    {
        "name" : "web",
        "image" : "${ecr_image_web}",
        "essential" : true,
        "portMappings" : [
        {
            "containerPort" : 80,
            "hostPort"      : 80,
            "protocol"      : "tcp"
        }
        ],
        "dependsOn" : [{
            "containerName" : "app",
            "condition"     : "START"
        }],
        "mountPoints" : [
        {
            "sourceVolume" : "tmp-data",
            "containerPath" : "/code/tmp"
        }
        ],
        "logConfiguration" : {
        "logDriver" : "awslogs",
        "options" : {
            "awslogs-group" : "${log_group_name_web}",
            "awslogs-region" : "ap-northeast-1",
            "awslogs-stream-prefix" : "web"
        }
        }
    }
]

タスク定義

タスク定義の設定を行います
template_fileには先ほど作成したtaskdef.json.tpl、varsには環境変数化したログとECRのパスを指定します
また、後述するタスクロールとタスク実行ロールも指定します

data "template_file" "app_container_definitions" {
  template = file("./templates/ecs/taskdef.json.tpl")

  vars = {
    log_group_name_app = aws_cloudwatch_log_group.app.name
    log_group_name_web = aws_cloudwatch_log_group.web.name
    ecr_image_app = var.ecr_image_app
    ecr_image_web = var.ecr_image_web
    
  }
}


# タスク定義
resource "aws_ecs_task_definition" "app" {
  family                   = "${local.prefix}-app"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512
  execution_role_arn = "arn:aws:iam::XXXXXXXXXXXX:role/tf-pg-dev-task-exec-role"
  task_role_arn =  "arn:aws:iam::XXXXXXXXXXXX:role/tf-pg-dev-task-role"
  container_definitions    = data.template_file.app_container_definitions.rendered

  volume {
    name = "tmp-data"
  }

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-task-def" })
  )
}

セキュリティグループ

ECSのセキュリティの設定を行います
HTTPのインバウンドアクセスに関しましては本来はALBからのアクセスのみ許可しないといけませんが未作成なので一旦全てのアクセスを許可します

プロトコル/サービス in/out ポート番号 CIDR
HTTP inbound 80 全てのアクセス
RDS(Postgres) outbound 5432 プライベートサブネットAとCのCIDRブロック内
HTTP outbound 80 全てのアクセス
HTTPS outbound 443 全てのアクセス
# ECSのセキュリテーグループ
resource "aws_security_group" "ecs_sg" {
  description = "Access for the ECS Service"
  name        = "${local.prefix}-ecs-sg"
  vpc_id      = aws_vpc.main.id

  # ECSからPublicな通信へのアウトバウンドアクセスを許可
  egress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }


  # ECSからPostgresへのアウトバウンドアクセスを許可
  egress {
    from_port = 5432
    to_port   = 5432
    protocol  = "tcp"
    cidr_blocks = [
      aws_subnet.private_a.cidr_block,
      aws_subnet.private_c.cidr_block,
    ]
  }

  # Publicな通信からNginxへのインバウンドアクセスを許可
  # 全ての通信をNginxを経由させたいのでECSの8000ポートへ直接アクセスさせない
  ingress {
    from_port = 80
    to_port   = 80
    protocol  = "tcp"
    cidr_blocks = [
      "0.0.0.0/0"
    ]
  }

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-sg" })
  )
}

ECSの設定について

ECSの設定のために以下を記載します

  • クラスター
  • タスク定義
  • タスク数
  • 起動タイプ(今回はFargate)
  • バージョン
resource "aws_ecs_service" "app" {
  name            = "${local.prefix}-app"
  cluster         = aws_ecs_cluster.main.name
  task_definition = aws_ecs_task_definition.app.family
  # 今回は検証用のためタスクを1つだけ実行させる
  desired_count    = 1
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  network_configuration {
    subnets = [
      aws_subnet.private_a.id,
      aws_subnet.private_c.id,
    ]
    security_groups  = [aws_security_group.ecs_sg.id]
  }

  tags = merge(
    local.common_tags,
    tomap({ "Name" = "${local.prefix}-ecs-service" })
  )

DjangoとNginxの設定

Terraformの設定を終えた後は

  • ECRリポジトリの作成
  • Dockerfileの作成、ビルド、プッシュ

を行います

ECRリポジトリの作成

まずはDjangoのECRリポジトリを作成します
パスはvariables.tfにも記載してあるとおり、今回はtf-pg/djangoにします
スクリーンショット 2023-07-17 8.11.02.png

今回はイメージスキャンを有効化します
リポジトリを作成ボタンを押したら作成完了です

スクリーンショット 2023-07-17 8.11.33.png

Nginxも同様にECRリポジトリを作成します
今回はtf-pg/nginxにします
スクリーンショット 2023-07-17 8.12.37.png

Dockerfileの作成

  • Django
  • Nginx

の2種類のDockerfileを作成します

Django

Terraformのタスク定義にも記載しましたがentrypoint.shを使ってコンテナを起動させます
また、コンテナのVolumeは/code/tmpに設定します
タスク定義に記載されている内容と統一しないとECSが起動しないので注意です

containers/django/Dockerfile
FROM --platform=linux/x86_64 python:3.11

# 公開するポートを明示的に定義
EXPOSE 8000

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

ENV APP_ROOT=/code

WORKDIR ${APP_ROOT}

COPY application/ ${APP_ROOT}/
RUN pip install --upgrade pip && pip install poetry
RUN poetry install --without dev

# コンテナ起動時に必ず実行したいコマンドを定義した entrypoint.sh をコピー
COPY ./containers/django/entrypoint.prd.sh /usr/local/bin/entrypoint.prd.sh
# 実行権限を付与
RUN chmod +x /usr/local/bin/entrypoint.prd.sh
ENTRYPOINT ["entrypoint.prd.sh"]

VOLUME ["${APP_ROOT}/tmp"]

続いてentrypoint.shを作成します
通常のDjangoの開発では開発用のサーバーのrunserverを使いますが
今回は本番用の想定で有名なアプリケーションサーバであるGunicornを使って起動させます

containers/django/entrypoint.sh
#!/bin/sh
set -eu

mkdir -p ${APP_ROOT}/tmp/gunicorn_sockets

# Execute migration
poetry run python manage.py migrate

# Run Django application
poetry run gunicorn project.wsgi:application --bind=unix://${APP_ROOT}/tmp/gunicorn_socket

exec "$@"

Nginx

NginxのDockerfileとnginx.confファイルを作成します
その際に注意しておくべきことが2つあります

  • プラットフォームの指定
    • M1MacでDockerfileをbuildする場合はArmのアーキテクチャになってしまいます
    • ECSのコンテナがx86_64で実行されるのでimageとECSのアーキテクチャが違うことによって実行されなくなってしまいます
    • そこで--platform=linux/x86_64と指定する必要があります
  • フォアグラウンドモードで実行させる
    • Nginxはデフォルトでバックグラウンド(daemon)モードで実行されます
    • CloudWatch内でログを見れるようにするにはバックグラウンドモードをoffにする必要があります
containers/nginx/Dockerfile
FROM --platform=linux/x86_64 nginx:stable-alpine

RUN rm -f /etc/nginx/conf.d/*
COPY ./containers/nginx/nginx.prd.conf /etc/nginx/conf.d/

CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf

Nginxの設定ファイルです

containers/nginx/nginx.conf
upstream gunicorn {
    # Unixドメインソケットを通じてGunicornにリクエストを転送する
    # NginxがリバースプロキシとしてGunicornと連携
    server unix:///code/tmp/gunicorn_socket;
}

server {
    listen 80;
    # Nginxのバージョン情報を非表示にする
    # サーバ情報を隠すことでセキュリティ上のリスクを軽減させる
    server_tokens off;

    # ファイルサイズの変更、デフォルト値は1M
    client_max_body_size 5M;

    # HTTP レスポンスヘッダの Content_Type に付与する文字コード
    charset utf-8;

    # ログ設定
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # API 通信
    location /api {
        # X-Real-IPヘッダにクライアントのIPアドレスを設定
        proxy_set_header X-Real-IP $remote_addr;
        # X-Forwarded-Forヘッダにリクエストを送ったクライアントまたはプロキシのIPアドレスの履歴(リスト)を設定
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # Hostヘッダにクライアントのホスト名を設定
        proxy_set_header Host $http_host;
        # タイムアウトの設定(3600s)
        proxy_read_timeout 3600;
        # 上記のヘッダの情報がGunicornに転送される
        proxy_pass http://gunicorn;
    }

    # ヘルスチェック
    location /api/health {
        empty_gif;
        access_log off;
        break;
    }

    # HTTP 通信をタイムアウトせずに待つ秒数
    keepalive_timeout 60;
}

DockerfileのbuildからECRへpushするまで

aws cliがDockerを使ってpushコマンドを実行できるよう認証トークンを以下のコマンドで取得します

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com

ログインに成功後、

  • Dockerfileのbuild
  • buildしたDockerfileのタグづけ
  • タグづけしたDockerfileをECRへpush

を順番に行います

docker build -t tf-pg/django -f containers/django/Dockerfile .
docker tag tf-pg/django:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/tf-pg/django:latest
docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/tf-pg/django:latest

Nginxも同様に行います

docker build -t tf-pg/nginx -f containers/nginx/Dockerfile .
docker tag tf-pg/nginx:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/tf-pg/django:latest
docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/tf-pg/nginx:latest

以下のように作成したDockerfileをECRリポジトリにpushできたら成功です

スクリーンショット 2023-07-17 8.23.45.png

スクリーンショット 2023-07-17 8.24.17.png

IAMRoleの作成と設定

ECSをデプロイする際は

  • タスクロール
  • タスク実行ロール

の2つが必要になります

タスクロールとタスク実行ロールの違い

  • タスクロール
    • コンテナの中のアプリケーションからAWSのサービスを利用する場合に設定するロール
    • 例えばコンテナ内にメール送信機能があればSES、画像をS3にアップロードする機能があればS3のポリシーをアタッチします
  • タスク実行ロール
    •  ECS(タスク)を実行させる場合に設定するロール
    •  例えばECR内のイメージをpullしたりCloudWatchにログを保存したりパラメータストア内の環境変数を使うときのポリシーをアタッチします

今回はSESやS3を使うロジックを作成してないのでタスク実行ロールのみ作成し、必要な
タスク実行ロールに

  • ECRとECSの実行
  • CloudWatch
  • パラメータストア

用のポリシーをアタッチします

タスク実行ロール用のポリシー

今回はAWS側ですでに用意しているポリシーである

  • AmazonECSTaskExecutionRolePolicy
  • CloudWatchAgentServerPolicy

とパラメータストア用のカスタムポリシー(名前はSSMForECSPolicyとします)を作成します

AmazonECSTaskExecutionRolePolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
CloudWatchAgentServerPolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:PutMetricData",
                "ec2:DescribeVolumes",
                "ec2:DescribeTags",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams",
                "logs:DescribeLogGroups",
                "logs:CreateLogStream",
                "logs:CreateLogGroup"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameter"
            ],
            "Resource": "arn:aws:ssm:*:*:parameter/AmazonCloudWatch-*"
        }
    ]
}

パラメータストアからSecureStringとして登録されている値も含めて取得するには

  • ssm:GetParameters

以外に

  • secretsmanager:GetSecretValue
  • kms:Decrypt(Secure Stringを復号化する)

も設定する必要があります

SSMForECSPolicy
{
    "Statement": [
        {
            "Action": [
                "ssm:GetParameters",
                "secretsmanager:GetSecretValue",
                "kms:Decrypt"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/*"
            ]
        }
    ],
    "Version": "2012-10-17"
}

信頼されたエンティティもSSMForECSPolicyに忘れずにアタッチしましょう

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

tagの作成

こちらに関しては任意です
ハードコーディングしてもいいのですが今回はmain.tfで定義した変数を使用します
mergeを使ってmain.tfにあるlocal.common_tags変数に任意の変数を追加します
共通部分は繰り返し使用でき、なおかつ独自の変数を追加できるのでおすすめです

variables.tf
# ------------------------------
# Variables
# ------------------------------

# プリフィックスを設定
variable "prefix" {
  default = "tf-pg"
}

# プロジェクトを識別する一意の識別子を設定
variable "project" {
  default = "terraform-playground"
}

# プロジェクトのオーナーを設定
variable "owner" {
  default = "shun198"
}
main.tf
# ------------------------------
# Locals
# ------------------------------
locals {
  # variables.tfから変数を取得
  # terraformのworkspaceの一覧から該当するworkspace(dev,stg,prdなど)を取得
  prefix = "${var.prefix}-${terraform.workspace}"
  common_tags = {
    Environmnet = terraform.workspace
    Project     = var.project
    Owner       = var.owner
    ManagedBy   = "Terraform"
  }
}

実際に作成してみよう!

フォーマットの修正、validateやplanによる確認が終わったら以下のコマンドで適用します

terraform apply -auto-approve

作成されたか確認してみよう!

以下のようにクラスターが作成され、クラスター内のタスクが実行されていたら成功です
スクリーンショット 2023-08-26 12.36.47.png

Gunicornの起動とMigrationが正常に実行されていることも確認できました

スクリーンショット 2023-08-26 11.51.24.png
スクリーンショット 2023-08-26 12.37.40.png

リソースの削除

使用しないリソースは以下のコマンドで削除しましょう

terraform destroy

まとめ

  • ECS
  • Dockerfile
  • Django
  • Nginx
  • パラメータストア
  • AWS CLI
  • IAMロールの適切な権限の設定
  • ネットワーク
  • Terraform

とある程度の知識の深さを幅広く要求されるのでかなり難しい内容になっているかと思います
今後一人でインフラを構築するのであれば避けては通れない箇所なので難しそうであれば数日かけて読んでいただけると幸いです

参考

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?