ECSでタスク内部でNginx-->php-fpmとのコンテナ間通信を簡単にTerraformで実現する
はじめに
以前、ECSでNginxコンテナを起動させる記事を作成しています。
これらを基に、1つのタスク内部で Nginxコンテナ->php-fpmコンテナのコンテナ間の通信を実現します。
なるべく複雑な設定を省いて、簡単な設定のみで実現しようと試みました。
GOAL状態
Nginxにリクエストすると、<?php phpinfo();" >を返却する単純な処理を実現します
AWS構成
パブリックサブネットにロードバランサーを配置して、ECSタスクはプライベートサブネットに配置します。
プライベートサブネットに配置されたタスクがECRへイメージを取得しにいく必要があるので、NAT Gatewayも設置しています。
Internet
|
|
[Application Load Balancer]
(HTTP:80, HTTPS:443)
|
|
+---------------------------+---------------------------+
| | |
[Public Subnet 1a] [Public Subnet 1c] [Elastic IP]
10.0.1.0/24 10.0.2.0/24 |
| | [NAT Gateway]
| | |
[Private Subnet 1a] [Private Subnet 1c] |
10.0.10.0/24 10.0.11.0/24 |
| | |
+---------------------------+---------------------------+
|
[ECS Cluster]
|
[ECS Service]
|
+---------------------------+
| |
[Nginx Container] [API Container]
(Port 80) (Port 9000)
ネットワークモード=awsvpcのコンテナ間通信について
ECSのネットワークの仕組みを公式ドキュメントから勉強していきます。
ECSのネットワークモード
今回はawsvpc モードを利用します。
ネットワークモード = awsvpc
awsvpc モードは、ECSタスクに固有のElastic Network Interface(ENI)を割り当て、タスクごとに専用のIPアドレスを持たせます。
特徴:
- 各タスクに独自のENI が割り当てられ、タスク自体がホストのように振る舞います。
- タスクはVPC内の他のリソース(例: RDS、EC2、ELB)と直接通信できるようになります。
- ENIごとにセキュリティグループを割り当てることができるため、きめ細かなネットワーク制御が可能です。
コンテナ間通信に関して重要なことが記載されています。
同じタスクに属するコンテナが、localhost インターフェイス経由で通信できるようになります。
Nginxコンテナ・php-fpmコンテナのコンテナ間通信の設定をしたイメージを作成
- ECSのコンテナで利用するイメージを作成
- TerraformでECS環境を構築
Localでのファイル構成
Localでのファイル構成はこのようになっています。
これら4つのファイルを利用して、Nginxコンテナ用のイメージと、php-fpm用のイメージ(api)を作成していきます。
L docker
L api
L php.ini
L Dockerfile
L nginx
L default.conf
L Dockerfile
php-fpmコンテナ(apiコンテナ)を作成
docker/api/php.ini
必要最小限の構成
[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"
memory_limit = 256M
docker/api/Dockerfile
/var/www/public/index.phpを作成し、index.phpの中身にphpinfo()を出力しています。
実際にアクセスした際に出力されるのはphpinfo()の内容です。
FROM php:8.2-fpm
WORKDIR /var/www
COPY ./docker/api/php.ini /usr/local/etc/php/
RUN apt-get update && apt-get -y upgrade
RUN apt-get install -y zip \
unzip \
vim
WORKDIR /var/www/src/public
RUN touch index.php
RUN chmod 777 index.php
RUN echo "<?php phpinfo();" > index.php
ビルドして、ECRへPUSH
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <acount-id>.dkr.ecr.ap-northeast-1.amazonaws.com
docker build --platform linux/amd64 -f docker/api/Dockerfile -t ecs-navi-api .
docker tag ecs-navi-nginx:latest <acount-id>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-navi-api:latest
docker push <acount-id>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-navi-api:latest
Nginxコンテナを作成
docker/nginx/default.conf
一番重要なポイントです。
nginxのlocationに入ってきたら、fastcgi_pass 127.0.0.1:9000; に設定します。
これで、localhost環境の9000ポート(php-fpmのapiコンテナ)へリクエストします。
server {
listen 80;
index index.php index.html;
root /var/www/src/public;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
docker/nginx/Dockerfile
niginxの最小限構成
FROM nginx:stable-alpine
COPY ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf
ビルドして、ECRへPUSH
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <acount-id>.dkr.ecr.ap-northeast-1.amazonaws.com
docker build --platform linux/amd64 -f docker/nginx/Dockerfile -t ecs-navi-nginx .
docker tag ecs-navi-nginx:latest <acount-id>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-navi-nginx:latest
docker push <acount-id>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-navi-nginx:latest
Terraformでリソースを作成する
ベースとなる部分はNginxのコンテナ構成のときとそれほど変わりません。
※主な変更点- プライベートネットワークが追加
- apiのコンテナが追加
- タスクロール・実行ロールを追加
NW部分
// VPC
resource "aws_vpc" "ecs_navi_vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.service_name}-vpc"
}
}
// Subnet
resource "aws_subnet" "ecs_navi_subnet_public_1a" {
vpc_id = aws_vpc.ecs_navi_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
tags = {
Name = "${var.service_name}-subnet-public-1a"
}
}
resource "aws_subnet" "ecs_navi_subnet_public_1c" {
vpc_id = aws_vpc.ecs_navi_vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = true
tags = {
Name = "${var.service_name}-subnet-public-1c"
}
}
resource "aws_subnet" "ecs_navi_subnet_private_1a" {
vpc_id = aws_vpc.ecs_navi_vpc.id
cidr_block = "10.0.10.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = false
tags = {
Name = "${var.service_name}-subnet-private-1a"
}
}
resource "aws_subnet" "ecs_navi_subnet_private_1c" {
vpc_id = aws_vpc.ecs_navi_vpc.id
cidr_block = "10.0.11.0/24"
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = false
tags = {
Name = "${var.service_name}-subnet-private-1c"
}
}
// Internet Gateway
resource "aws_internet_gateway" "ecs_navi_igw" {
vpc_id = aws_vpc.ecs_navi_vpc.id
}
// Route Table
resource "aws_route_table" "ecs_navi_route_table" {
vpc_id = aws_vpc.ecs_navi_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.ecs_navi_igw.id
}
}
// Route Table Association
resource "aws_route_table_association" "ecs_navi_subnet_public_1a_association" {
subnet_id = aws_subnet.ecs_navi_subnet_public_1a.id
route_table_id = aws_route_table.ecs_navi_route_table.id
}
resource "aws_route_table_association" "ecs_navi_subnet_public_1c_association" {
subnet_id = aws_subnet.ecs_navi_subnet_public_1c.id
route_table_id = aws_route_table.ecs_navi_route_table.id
}
// eip
resource "aws_eip" "ecs_navi_eip" {
domain = "vpc"
}
// Nat Gateway
resource "aws_nat_gateway" "ecs_navi_nat_gateway" {
subnet_id = aws_subnet.ecs_navi_subnet_public_1a.id
allocation_id = aws_eip.ecs_navi_eip.id
}
// Route Table
resource "aws_route_table" "ecs_navi_route_table_private" {
vpc_id = aws_vpc.ecs_navi_vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.ecs_navi_nat_gateway.id
}
}
// Route Table Association
resource "aws_route_table_association" "ecs_navi_subnet_private_1a_association" {
subnet_id = aws_subnet.ecs_navi_subnet_private_1a.id
route_table_id = aws_route_table.ecs_navi_route_table_private.id
}
resource "aws_route_table_association" "ecs_navi_subnet_private_1c_association" {
subnet_id = aws_subnet.ecs_navi_subnet_private_1c.id
route_table_id = aws_route_table.ecs_navi_route_table_private.id
}
IAMロール
// IAM
resource "aws_iam_role" "ecs_navi_ecs_task_execution_role" {
name = "${var.service_name}-ecs-task-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = {
Name = "${var.service_name}-ecs-task-execution-role"
}
}
resource "aws_iam_role_policy" "ecs_navi_ecs_task_execution_policy" {
name = "${var.service_name}-ecs-task-execution-policy"
role = aws_iam_role.ecs_navi_ecs_task_execution_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"ecs:DescribeTasks",
"ecs:UpdateContainerInstancesState",
"ecs:SubmitContainerStateChange"
]
Resource = "*"
}
]
})
}
セキュリティグループ
ロードバランサーのセキュリティグループ
resource "aws_security_group" "ecs_navi_sg" {
vpc_id = aws_vpc.ecs_navi_vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.service_name}-sg"
}
}
タスクのセキュリティグループ
外部からは80ポート(Nginxで開いているポートのみで受けつるける)
resource "aws_security_group" "ecs_navi_backend_sg" {
vpc_id = aws_vpc.ecs_navi_vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.ecs_navi_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
ロードバランサー・ターゲットグループ関連
// ロードバランサー
resource "aws_lb" "ecs_navi_lb" {
name = "${var.service_name}-lb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.ecs_navi_sg.id]
subnets = [aws_subnet.ecs_navi_subnet_public_1a.id, aws_subnet.ecs_navi_subnet_public_1c.id]
enable_deletion_protection = false
}
resource "aws_lb_target_group" "backend_tg" {
name = "${var.service_name}-backend-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.ecs_navi_vpc.id
target_type = "ip"
health_check {
path = "/"
interval = 30
timeout = 5
healthy_threshold = 5
unhealthy_threshold = 2
matcher = "200"
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.ecs_navi_lb.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.backend_tg.arn
}
}
// https のリスナー
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.ecs_navi_lb.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = aws_acm_certificate.ecs_navi_cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.backend_tg.arn
}
}
ECSの設定
クラスターとサービス
// ECS Cluster
resource "aws_ecs_cluster" "ecs_navi_cluster" {
name = "${var.service_name}-cluster"
}
// ECS Service
resource "aws_ecs_service" "backend_service" {
name = "${var.service_name}-backend-service"
cluster = aws_ecs_cluster.ecs_navi_cluster.id
task_definition = aws_ecs_task_definition.ecs_navi_nginx_task_definition.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.ecs_navi_subnet_private_1a.id, aws_subnet.ecs_navi_subnet_private_1c.id]
security_groups = [aws_security_group.ecs_navi_backend_sg.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.backend_tg.arn
container_name = "nginx"
container_port = 80
}
}
タスク定義
Nginxコンテナ、apiコンテナ(php-fpm)の2つのコンテナを1つのタスク内部で起動させます。
外部からのリクエストはnginxが受け付けるので、ENIとのポートフォワーディングはNginxの80ポートだけでOKです。
apiコンテナ(php-fpm)はデフォルトで9000ポートで待機しています。(localでコンテナ起動してみると分かります。)
// ECS タスク定義
resource "aws_ecs_task_definition" "ecs_navi_nginx_task_definition" {
family = "${var.service_name}-nginx-task"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 256
memory = 512
container_definitions = jsonencode([
{
name = "nginx"
image = "<account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-navi-nginx:latest"
portMappings = [
{
containerPort = 80
hostPort = 80
}
]
},
{
name = "api"
image = "<account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-navi-api:latest"
}
])
execution_role_arn = aws_iam_role.ecs_navi_ecs_task_execution_role.arn
}
終わりに
今回もサービスのDNS名を取得して、ブラウザにリクエストしてください。
phpinfo(); の情報が表示されたら通信OKです。
Discussion