TerraformでFargateのSidecarを実現しよう
須藤 悠
こんにちは!リクルートライフスタイルの共通クラウド基盤でリードクラウドアーキテクトをしている須藤です。
この記事は AWS Advent Calendar 2018 の2日目の記事です!( リクルートライフスタイル Advent Calendar 2018 2日目の記事でもあります)
リクルートライフスタイルの共通クラウド基盤では、サービスごとにアカウントを払い出して、サービス開発者がその環境を構築する、というスタイルです。クラウドアーキテクトチームはCCoE(Cloud Center of Excellence)であり、TerraformやFargateを使いたい、という開発者に対しては、ペアプログラミングやアーキテクチャレビューなど、規範的・助言的な活動を通して成長しあっていく、という活動をしています。
当然、re:Invent 2018で発表された トランジットゲートウェイ や AWS Resource Access Managerのクロスアカウントリソース共有 なども、適用することで幸せになるユースケースがあれば積極的に使っていきたいと思っています!
Terraform で構築するサービスの構成
まずは、Terraformで構築する新しいサービス myservice
の構成図を紹介します。
新しいAWSアイコンを使って作図してみましたが、まだちょっと慣れませんね。
2つのAZにそれぞれ3つのサブネットがあり、ALBを配置するパブリックサブネット、Webアプリケーションを動かすためのサブネット、データベースを置くためのサブネットに分かれています。Fargateで動作するTaskは、Webアプリケーションサブネットに配置します。また、これらのTaskはSidecar構成で、それぞれDataDog Agentのコンテナとセットで起動します。
DataDog AgentのSidecarを、どうTerraformで構成すれば良いのか?TerraformでFargateを利用したサービスやTask Definitionを記述するときに注意すべきはどこなのか?やったことがないと、試行錯誤してしまいそうなところです。順に見ていきましょう。
DataDog を利用するための構成
DataDog AgentでFargateのコンテナを監視するので、Webアプリケーション本体とDataDog AgentのSidecarをセットで扱うために、1つのTask Definitionで定義しておきます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
[
{
"name": "myservice",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "${awslogs-group}",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "fargate"
}
},
"portMappings": [
{
"hostPort": 3000,
"protocol": "tcp",
"containerPort": 3000
}
],
"environment": [
{
"name": "DATABASE_HOST",
"value": "${db-endpoint}"
},
{
"name": "DATABASE_PASSWORD",
"value": "${db-password}"
},
{
"name": "DATABASE_USER",
"value": "${db-user}"
},
{
"name": "NODE_ENV",
"value": "production"
},
{
"name": "RAILS_ENV",
"value": "production"
},
{
"name": "RAILS_LOG_TO_STDOUT",
"value": "true"
}
],
"image": "${image-repository}",
"essential": true,
"dockerLabels": {
"com.datadoghq.ad.instances": "[{\"host\": \"%%host%%\", \"port\": 3000}]",
"com.datadoghq.ad.check_names": "[\"myservice\"]",
"com.datadoghq.ad.init_configs": "[{}]"
}
},
{
"name": "datadog-agent",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "${awslogs-group}",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "fargate"
}
},
"portMappings": [
{
"hostPort": 8126,
"protocol": "tcp",
"containerPort": 8126
}
],
"environment": [
{
"name": "DD_API_KEY",
"value": "${dd-agent-key}"
},
{
"name": "DD_APM_ENABLED",
"value": "true"
},
{
"name": "ECS_FARGATE",
"value": "true"
}
],
"image": "datadog/agent:latest",
"essential": true
}
]
後半に、 datadog/agent:latest
Dockerイメージを利用する datadog-agent
を定義しています。 portMappings
で 8126
ポートを開けているのは、Application Performance Monitoring(APM)を利用するためです。APMを利用する場合には、アプリケーション側でも設定が必要なのでご注意を。 Java、Python、Ruby、Go、Node.jsのサンプルが記載されている ので、悩むことはないでしょう。
また、ここでいくつかの環境変数を渡しています。
DD_API_KEY
: DataDogのAPIキーDD_APM_ENABLED
: DataDog trace-agentを有効化してAPMを利用するかどうかECS_FARGATE
: Fargateを利用する場合true
これらとは別に、Webアプリケーション側の dockerLabels
でもいくつかの情報を渡しています。DataDogのAutodiscoveryのドキュメントを見れば詳細を知ることができますが、ここでは %%host%%
%%port%%
myservice
の3つが設定されていれば良いでしょう。
このWebアプリケーション myservice
はRuby on Railsで作っているので、 containerPort
とDataDogに渡しているポート情報を 3000
としています。また、Fargateは現在 awsvpc
ネットワークモードのみに対応しているので、
awsvpc ネットワークモードを使用するタスク定義では、containerPort のみを指定する必要があります。hostPort は、空白のままにするか、containerPort と同じ値にする必要があります。
という制約を受けます。このため、 hostPort
も 3000
としています。
その他のパラメータについても、詳細はTask Definitionのパラメータのドキュメントをご覧いただければだいたい分かります。
あとは、こうして記述したjsonテンプレートにTerraformから必要な変数をバインドしてあげればOKですね!
1
2
3
4
5
6
7
8
9
10
11
12
data "template_file" "myservice_task_definition_json" {
template = "${file("task-definitions/myservice.json")}"
vars {
awslogs-group = "${aws_cloudwatch_log_group.myservice.name}"
db-endpoint = "${aws_rds_cluster.myservice.endpoint}"
db-user = "${aws_rds_cluster.myservice.master_username}"
db-password = "${var.db_password}"
image-repository = "${var.ecr_repository_uri}"
dd-agent-key = "${var.dd_agent_key}"
}
}
Fargate 特有の記述
ECSのFargate Modeを利用する場合に、TerraformのリソースでFargate特有の記述が必要になるのは
Task Definition( aws_ecs_task_definition
)とService( aws_ecs_service
)、そしてALBのTarget Group( aws_lb_target_group
)リソースです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
resource "aws_ecs_task_definition" "task_definition" {
family = "myservice"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "${var.myservice_cpu}"
memory = "${var.myservice_memory}"
container_definitions = "${data.template_file.myservice_task_definition_json.rendered}"
task_role_arn = "${var.myservice_task_role_arn}"
execution_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ecsTaskExecutionRole"
}
resource "aws_ecs_service" "service" {
name = "myservice"
cluster = "${aws_ecs_cluster.cluster.id}"
launch_type = "FARGATE"
task_definition = "${aws_ecs_task_definition.task_definition.arn}"
desired_count = 1
health_check_grace_period_seconds = 120
lifecycle {
ignore_changes = ["desired_count"]
}
load_balancer {
target_group_arn = "${aws_lb_target_group.target_group.arn}"
container_name = "myservice"
container_port = 3000
}
network_configuration {
subnets = ["${data.terraform_remote_state.base.vpc01_webapp_subnet_ids.az1}", "${data.terraform_remote_state.base.vpc01_webapp_subnet_ids.az2}"]
security_groups = ["${aws_security_group.ip_restrictions.id}"]
}
}
resource "aws_lb_target_group" "target_group" {
name = "myservice"
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = "${data.terraform_remote_state.base.vpc01_vpc.id}"
health_check {
healthy_threshold = 3
unhealthy_threshold = 2
timeout = 5
interval = 10
matcher = "200,304"
path = "/health_check"
}
}
requires_compatibilities
と launch_type
にそれぞれ FARGATE
の記述をします。また、前述の通り network_mode
が awsvpc
のみとなることにも注意しましょう。
requires_compatibilities
には ["FARGATE","EC2"]
のように2つの値を指定することもできます。基本的にはFargateを使うけれども、デバッグ用途でEC2モードでも動かしたいとか、開発用にSpotFleetの安価なクラスタ上で動かしたい、といった場合には活用できるでしょう。わたしたちはFargate一筋ですが!
network_configuration
の中で assign_public_ip = true
とすれば個々のTaskが利用するENIにパブリックIPを割り当てることは可能ですが、 load_balancer
でALBの target_group_arn
を指定している場合、通常 パブリックIPは不要 です。
ALBを利用している場合、クライアントからのアクセスはALBのパブリックIPを経由してTargetGroup内の各TaskのENIに割り当てられたプライベートIPに流すのが一般的な利用方法です。Terraformやecs-deployを利用してFargateの構築をする場合、本当に assign_public_ip = true
や assignPublicIp=ENABLED
が必要か、きちんと検討しましょう。
aws_lb_target_group
で注意すべきなのは target_type = "ip"
となる点です。FargateはENIを利用しているので、サブネットのローカルIPをENIに割り当て、そのIPを利用してTarget Groupを更新します。
サービスをオートスケールさせる場合、 terraform apply
するときに初期値(上記の例では 1
)にリセットされないように、 desired_count
は lifecycle
の ignore_changes
で無視するよう設定しておくと安心ですね。(これはFargateに限らず、EC2モードのECSでも一緒ですね!)
さいごに
いかがでしたか?Terraform、Fargate、そしてSidecarでDataDogを使う、という方に届き、参考になれば幸いです。
わたしたち共通クラウド基盤のチームでは、 即戦力向けのクラウドアーキテクトポジションと、第二新卒やクラウド未経験者向けのクラウドアーキテクト(ポテンシャル)の2つのポジションを採用中 です!
一緒にリクルートライフスタイルの共通クラウド基盤を進化させていく仲間になりませんか?須藤はまだ入社5ヶ月ですが、年齢に関係なく期待に応えれば報われ、高いスキルを持った方々とより良いクラウド基盤について議論しながら日々の業務を回していく環境で働けるのは、本当に楽しいです。
この記事を読んでご興味を持っていただけた方は、上記リンクの中途採用ページからご連絡ください。お待ちしています!