What?
やりたいこと。
- AWS Fargateを使って、クラスタを作る
- コンテナのログは、Amazon CloudWatch Logsに送る
- 以上のことを、Terraformで実現する
というのを書いていきます。
環境
利用するTerraformと、AWS Providerのバージョンです。
$ terraform version
Terraform v0.12.25
+ provider.aws v2.63.0
構築する内容
まっさらなところから、VPCを作ってAWS Fargateクラスタを構築、簡単な動作確認を行うところまでやります。
- シングルAZ
- 動作させるコンテナはnginx
- ALBはHTTP
Terraformでの実装内容は、すべてmain.tf
に書いています。
TerraformとAWS Providerのバージョン指定
terraform {
required_version = "= 0.12.25"
}
provider "aws" {
version = "2.63.0"
}
AWSのクレデンシャルは、環境変数で設定。
export AWS_ACCESS_KEY_ID=.....
export AWS_SECRET_ACCESS_KEY=.....
export AWS_DEFAULT_REGION=.....
この状態で、進めていきましょう。
VPC〜ALBまで
このあたりは、今回のお題であるFargateやCloudWatch Logsの前提となる環境を作っていくのでコミュニティモジュールを積極的に使って、簡単に。
VPC
VPCは、こちらのコミュニティモジュールを使用して定義。
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.33.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-1a", "ap-northeast-1c"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
map_public_ip_on_launch = true
enable_nat_gateway = true
single_nat_gateway = true
one_nat_gateway_per_az = false
}
シングルAZにするので、NAT Gatewayはひとつ。
セキュリティグループ
セキュリティグループは、ALBとFargateに割り当てるものをそれぞれ定義します。コミュニティモジュールは、こちらを利用。
AWS EC2-VPC Security Group Terraform module
module "load_balancer_sg" {
source = "terraform-aws-modules/security-group/aws//modules/http-80"
version = "3.10.0"
name = "load-balancer-sg"
vpc_id = module.vpc.vpc_id
ingress_cidr_blocks = ["0.0.0.0/0"]
}
module "nginx_cluster_sg" {
source = "terraform-aws-modules/security-group/aws//modules/http-80"
version = "3.10.0"
name = "nginx-cluster-sg"
vpc_id = module.vpc.vpc_id
ingress_cidr_blocks = ["10.0.0.0/16"]
}
vpc_id
に関しては、先ほどのVPCを作った時のモジュールのOutputを利用します。
vpc_id = module.vpc.vpc_id
ALB
HTTPを受け付ける、ALBを定義します。リスナーや、ターゲットグループも同時に定義することになります。コミュニティモジュールは、こちらを利用。
AWS Application and Network Load Balancer (ALB & NLB) Terraform module
module "load_balancer" {
source = "terraform-aws-modules/alb/aws"
version = "5.6.0"
name = "nginx"
vpc_id = module.vpc.vpc_id
load_balancer_type = "application"
internal = false
subnets = module.vpc.public_subnets
security_groups = [module.load_balancer_sg.this_security_group_id]
target_groups = [
{
backend_protocol = "HTTP"
backend_port = 80
target_type = "ip"
health_check = {
interval = 20
}
}
]
http_tcp_listeners = [
{
port = 80
protocol = "HTTP"
}
]
}
確認の都合上、ヘルスチェックの時間はちょっと短めにしておきました。
VPCで作成したパブリックサブネットと、ALB用のセキュリティグループの情報を使っています。
ここまでの結果で、Fargate構築の際に利用するもの
ここまで作成したVPCやセキュリティグループ、ALBの情報のうち、この後のFargateの構築で使うものをローカル変数に切り出しておきます。
locals {
vpc_id = module.vpc.vpc_id
private_subnets = [module.vpc.private_subnets[0]]
ecs_service_security_groups = [module.nginx_cluster_sg.this_security_group_id]
load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}
単純に、こうやって分けた方が記事を読む時にわかりやすいかな、と。
CloudWatch Logs
CloudWatch Logs側は、ロググループを定義。
resource "aws_cloudwatch_log_group" "nginx_cluster_log_group" {
name = "nginx-cluster-log-group"
}
IAMロール
コンテナからCloudWatch Logsにログを送る際に、権限が必要になります。このため、IAMロールを定義しましょう。
今回は、AmazonECSTaskExecutionRolePolicy
というポリシーをそのまま利用します。
AmazonECSTaskExecutionRolePolicy
でどんなことができるか、見てみましょう。
arnを調べます。
$ aws iam list-policies | grep ECS
"PolicyName": "AWSCodeDeployRoleForECS",
"Arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS",
"PolicyName": "AmazonECSServiceRolePolicy",
"Arn": "arn:aws:iam::aws:policy/aws-service-role/AmazonECSServiceRolePolicy",
"PolicyName": "AWSCodeDeployRoleForECSLimited",
"Arn": "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECSLimited",
"PolicyName": "AmazonECS_FullAccess",
"Arn": "arn:aws:iam::aws:policy/AmazonECS_FullAccess",
"PolicyName": "AWSApplicationAutoscalingECSServicePolicy",
"Arn": "arn:aws:iam::aws:policy/aws-service-role/AWSApplicationAutoscalingECSServicePolicy",
"PolicyName": "AmazonECSTaskExecutionRolePolicy",
"Arn": "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
$ aws iam get-policy --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
{
"Policy": {
"PolicyName": "AmazonECSTaskExecutionRolePolicy",
"PolicyId": "ANPAJG4T4G4PV56DE72PY",
"Arn": "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
"Path": "/service-role/",
"DefaultVersionId": "v1",
"AttachmentCount": 0,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"Description": "Provides access to other AWS service resources that are required to run Amazon ECS tasks",
"CreateDate": "2017-11-16T18:48:22+00:00",
"UpdateDate": "2017-11-16T18:48:22+00:00"
}
}
確認。
$ aws iam get-policy-version --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy --version-id v1
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
},
"VersionId": "v1",
"IsDefaultVersion": true,
"CreateDate": "2017-11-16T18:48:22+00:00"
}
}
まあ、同じ内容はドキュメントにも書かれているのですが、あらためて見た感じですね。
今回必要なのは、logs:CreateLogStream
とlogs:PutLogEvents
です。
で、定義したのがこちら。
data "aws_iam_policy_document" "assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role" "ecs_task" {
name = "MyEcsTaskRole"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
resource "aws_iam_role_policy_attachment" "ecs_task" {
role = aws_iam_role.ecs_task.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
Assume Roleについても、ドキュメントに書かれています(信頼関係という表現になっていますが)。
CloudWatch Logsに関するロールをタスクに付与しなかった場合、コンテナの作成時にこんなエラーを見ることになります。
Error: ClientException: Fargate requires task definition to have execution role ARN to support log driver awslogs.
Fargateクラスタ
最後に、Fargateクラスタの定義を行います。
- クラスタ定義
- タスク定義
- サービス定義
の3つを行います。
resource "aws_ecs_cluster" "nginx" {
name = "nginx-cluster"
}
resource "aws_ecs_task_definition" "nginx" {
family = "nginx-task-definition"
cpu = "256"
memory = "512"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
execution_role_arn = aws_iam_role.ecs_task.arn
container_definitions = <<JSON
[
{
"name": "nginx",
"image": "nginx:1.17.10",
"essential": true,
"portMappings": [
{
"protocol": "tcp",
"containerPort": 80
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-group": "nginx-cluster-log-group",
"awslogs-stream-prefix": "nginx-container-log-stream"
}
}
}
]
JSON
}
resource "aws_ecs_service" "nginx" {
name = "nginx-service"
cluster = aws_ecs_cluster.nginx.arn
task_definition = aws_ecs_task_definition.nginx.arn
desired_count = 3
launch_type = "FARGATE"
platform_version = "1.4.0"
network_configuration {
assign_public_ip = false
security_groups = local.ecs_service_security_groups
subnets = local.private_subnets
}
load_balancer {
target_group_arn = local.load_balancer_target_group_arn
container_name = "nginx"
container_port = 80
}
}
先ほど定義したIAMロールは、タスク定義で使います。
resource "aws_ecs_task_definition" "nginx" {
...
execution_role_arn = aws_iam_role.ecs_task.arn
...
また、コンテナがCloudWatch Logsにログを送信するという設定は、コンテナ定義のlogConfiguration
で行います。awslogs-group
に、ログ送信対象となるCloudWatch Logsのロググループを指定しましょう。
container_definitions = <<JSON
[
{
"name": "nginx",
"image": "nginx:1.17.10",
"essential": true,
"portMappings": [
{
"protocol": "tcp",
"containerPort": 80
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-group": "nginx-cluster-log-group",
"awslogs-stream-prefix": "nginx-container-log-stream"
}
}
}
]
JSON
こちらを参考に。
あとは、このタスク定義や、ALB、セキュリティグループの定義を設定しておしまいです。
resource "aws_ecs_service" "nginx" {
name = "nginx-service"
cluster = aws_ecs_cluster.nginx.arn
task_definition = aws_ecs_task_definition.nginx.arn
desired_count = 3
launch_type = "FARGATE"
platform_version = "1.4.0"
network_configuration {
assign_public_ip = false
security_groups = local.ecs_service_security_groups
subnets = local.private_subnets
}
load_balancer {
target_group_arn = local.load_balancer_target_group_arn
container_name = "nginx"
container_port = 80
}
}
動作確認
terraform apply
して
$ terraform apply -auto-approve
構築されたALBの情報から、DNS名を取得します。
$ aws elbv2 describe-load-balancers --names nginx
取得したDNSName
から、curl
で確認。
$ curl [DNSName]
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
この時の、CloudWatch Logsをtail
してみます。
$ aws logs tail nginx-cluster-log-group --follow
ログがCloudWatch Logsに登録されているのが確認できました。
2020-05-24T09:47:46.409000+00:00 nginx-container-log-stream/nginx/194c1f45-f056-4e4c-8cbf-8306c8ca52b0 10.0.101.4 - - [24/May/2020:09:47:46 +0000] "GET / HTTP/1.1" 200 612 "-" "ELB-HealthChecker/2.0" "-"
2020-05-24T09:47:46.409000+00:00 nginx-container-log-stream/nginx/a89ccf53-31a2-4a24-bb55-e643b30ea3a4 10.0.101.4 - - [24/May/2020:09:47:46 +0000] "GET / HTTP/1.1" 200 612 "-" "ELB-HealthChecker/2.0" "-"
2020-05-24T09:47:54.271000+00:00 nginx-container-log-stream/nginx/00f8ba07-8ac4-4885-854b-88b79bdd0789 10.0.101.4 - - [24/May/2020:09:47:54 +0000] "GET / HTTP/1.1" 200 612 "-" "ELB-HealthChecker/2.0" "-"
2020-05-24T09:48:00.197000+00:00 nginx-container-log-stream/nginx/00f8ba07-8ac4-4885-854b-88b79bdd0789 10.0.101.4 - - [24/May/2020:09:48:00 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.58.0" "xxx.xxx.xxx.xxx"
OKですね。