前回まででイメージの自動生成ができましたが、実はまだAWS上でコンテナを動かしていないので、まだグッとこない感じがするかもです。
ということで待ちに待った ALB からの接続までもっていって、仕事してる感を演出しようではありませんか!
目次
(0) はじめに(1) Docker
(2) イメージ自動生成
(3) サービス起動 ← イマココ!
(4) デプロイ
(5) Auto Scaling
(6) 費用と性能
ECSの基本を知る
この記事では、ECS = Amazon Elastic Container Service で Fargate を用いてコンテナを稼働させます。詳しくは公式ドキュメントを参照…って一応言うんだけど、ぶっちゃけ、下地が無きゃ見てもわかんねーよ。いろいろ調べても、ECS だの Fargate だの EKS だの出てきて、どんどん変化もしていってて、どーせーっつーんじゃい!ってなって、答えはでない。銀の弾丸じゃないから目的に沿って選択してね♥の伝家の宝刀に斬られて終わりですわ。
なので、ここでは最低限の説明と、(多分)最簡単な構成をとるので、まずはそっから始めてみましょう。難しいことはあとやあと!
クラスタとサービスとタスクとは
クラスタがただの箱で、サービスが実質上のリソース管理母体で、
タスクがコンテナです。
最初はこれでOK。
ドキュメントを見るよりも、とりあえずリソースを作成してみて、管理画面にて、クラスタのタブ、サービスのタブ、タスクの詳細、と見ていったほうが理解が早いと思います。
Fargateとは
Fargateを知るには、その前の EC2構成を知る必要があります。ECS は Dockerコンテナが動くところなので、インスタンスとDockerデーモンが必要なわけですが、そのインスタンスと空きリソースの管理をクラスターが行っていたわけです。リソースが空いてるところにコンテナを起動してくれたり、時にはインスタンスにSSHで入って Dockerを直接見たり、とかが従来の運用だったようです。そこに Fargate が登場。ユーザーはインスタンスとか気にせず、コンテナのことだけ考えればよくなりました。インスタンスのスペックや台数、リソース・死活監視が不要になり、コンテナのスペックと台数だけが管理対象となりました。
じゃあそれ一択じゃんってなりそうなものですが、最初はお高かったり、起動が遅めだったりして、管理の手間よりも品質を求めるとEC2版を使わざるをえなかったりするなど、すったもんだあり選択の余地が出てしまうというわけです。
ただ、一度大幅な値下げが実行されたこともあり、やっぱ管理は楽だし、今後に期待age ということで、ここでは Fargate を選択しています。まぁ迷うなら Fargate、確たる自信をもって運用できる人は EC2 もアリ、というくらいに捉えておけばよいと思います。
DNSとSSL/TLS証明書を準備する
で、ECSの作成から始まるかと思いきや、残念でしたDNSと証明書です。ECSのサービスを実用的に使うには ALBが必要で、ALBを使うには時代的に証明書が必須で、証明書の利用とサービスの公開にはDNSが必要だからです。楽しい箇所はあとにとっておきましょうね。進行図
離れに重要カップル現れて~♪ドメイン取得とゾーン作成
この辺は主旨とそれるので適当にいきますが、まず既存ドメインのサブドメインをゾーンとする場合はドメイン取得は不要です。完全新規のドメインを使う場合は、どっかで買ってきてください。ドメインとゾーンが決まったら、まずRoute53管理画面で、手動でゾーンを作成してください。ここでは test.example.com とでもしておきましょうか。ゾーンを作成したら、NSレコードが4つできるので、ドメイン管理サイトの方でネームサーバーを登録して権限を移譲してきてください。それでRoute53でレコード管理できるようになります。
※なぜ手動でゾーンを作成したかド忘れしました。メモも残っておらず。。。
でも Terraform で作るのやめた気がするので何か理由はあったはず!
ゾーンのデータ取得
手動作成したゾーンを terraform で扱えるように data を取得します。
1 2 3 4 |
data "aws_route53_zone" "default" { name = "${local.dns_zone}" private_zone = false } |
ACMで証明書を作成
メインのゾーン(test.example.com)と、サブ(*.test.example.com)で証明書を作成します。旧式のメールだとポチポチが必要になるので、DNS認証で一括自動化してしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
resource "aws_acm_certificate" "default" { count = "${local.on_common && local.acm_enabled ? 1 : 0}" domain_name = "${local.dns_zone}" subject_alternative_names = "${local.acm_aliases}" validation_method = "DNS" tags { service = "${local.service_name}" } lifecycle { create_before_destroy = true } } |
DNSレコードを作成
ACMのDNS認証では、自身の管理下にあるゾーンにて、指定された値でレコードを作成できることで可とするので、そのようにレコードを作成します。どんな値かは作成後に確認してください。
1 2 3 4 5 6 7 8 9 |
resource "aws_route53_record" "default" { count = "${local.on_common && local.acm_enabled ? 1 : 0}" name = "${aws_acm_certificate.default.domain_validation_options.0.resource_record_name}" type = "${aws_acm_certificate.default.domain_validation_options.0.resource_record_type}" zone_id = "${data.aws_route53_zone.default.id}" records = ["${aws_acm_certificate.default.domain_validation_options.0.resource_record_value}"] ttl = 60 } |
DNS検証
DNSレコードと証明書を関連付けて検証OKを出してもらいます。これでACMを利用できます。
1 2 3 4 5 6 |
resource "aws_acm_certificate_validation" "default" { count = "${local.on_common && local.acm_enabled ? 1 : 0}" certificate_arn = "${aws_acm_certificate.default.arn}" validation_record_fqdns = ["${aws_route53_record.default.fqdn}"] } |
CloudFront用も作成しておく
これは補足ですが、東京(ap-northeast-1) で提供する場合、CloudFront の分の証明書は us-east-1 で別途作る必要があります。terraform の provider -> alias にて us-east-1 版を用意し、ACM関連のリソースで provider = “aws.global” などリージョンを切り替えて作成しましょう。route53 は元々Globalリソースなので指定の必要はありません。ALB作成
ECSのサービスから関連付ける ALB の TargetGroup が必要なので先に作成します。TargetGroup だけでいいかなと思いきや、ALB本体もないとサービス作成ができないようになっているので、ケチらず作ってしまうしかありませんよ、と。進行図
満更でもない雰囲気が出てきました。VPCの中にRoute53は変とかツッコミすんなよ、モテないぞ!こっちゃ狭い中ガンバっとるんじゃぃ!ALB作成
とりあえず本体を作成し、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
resource "aws_alb" "web" { count = "${local.on_service && local.alb_enabled_web ? 1 : 0}" name = "${local.env}-${local.service_name}-web" load_balancer_type = "application" internal = false enable_http2 = true security_groups = ["${aws_security_group.web_global.id}"] subnets = ["${aws_subnet.public.*.id}"] enable_deletion_protection = false tags = { Name = "${local.env}-${local.service_name}-web" service = "${local.service_name}" env = "${local.env}" } } |
Listener作成
ACM使って443番で待機します。default_action -> target_group_arn のところは、自前の設定で ECS と EC2 AutoscalingGroup を切り替えられるようにしてるだけなので気にしないでください。あと、blue という名前は、デプロイにおける Blue/Green のアレですが、切り替わりが発生するので ignore_changes で実値との差異を無視しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
resource "aws_alb_listener" "web" { count = "${local.on_service && local.alb_enabled_web ? 1 : 0}" load_balancer_arn = "${aws_alb.web.arn}" port = "443" protocol = "HTTPS" ssl_policy = "ELBSecurityPolicy-2016-08" certificate_arn = "${data.terraform_remote_state.common.aws_acm_certificate_validation_default_certificate_arn}" default_action { target_group_arn = "${local.on_ecs ? aws_alb_target_group.ecs-blue-web.arn : aws_alb_target_group.asg-web.arn}" type = "forward" } lifecycle { ignore_changes = ["default_action"] } } |
TargetGroup作成
あとで Blue/Green Deployment を行うので、2つの TargetGroup を用意します。変に1つにまとめて書くとかえって汚くなりそうだったので、普通に2つに分けて書きました。
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 |
resource "aws_alb_target_group" "ecs-blue-web" { count = "${local.on_service ? 1 : 0}" name = "${local.env}-${local.service_name}-web-ecs-blue" vpc_id = "${aws_vpc.default.id}" protocol = "HTTP" port = 80 deregistration_delay = 30 target_type = "ip" health_check { protocol = "HTTP" path = "${local.alb_healthcheck_path_web}" port = 80 interval = 10 timeout = 3 healthy_threshold = 2 unhealthy_threshold = 2 matcher = 200 } } resource "aws_alb_target_group" "ecs-green-web" { count = "${local.on_service ? 1 : 0}" name = "${local.env}-${local.service_name}-web-ecs-green" vpc_id = "${aws_vpc.default.id}" protocol = "HTTP" port = 80 deregistration_delay = 30 target_type = "ip" health_check { protocol = "HTTP" path = "${local.alb_healthcheck_path_web}" port = 80 interval = 10 timeout = 3 healthy_threshold = 2 unhealthy_threshold = 2 matcher = 200 } } |
ちなみに、EC2 AutoscalingGroupの方は使わないので載せませんが、target_type = “instance” となる点が異なります。
DNS CNAME作成
サービス提供の際に公開利用するFQDNを、ALBのFQDNを値としたCNAMEレコードにして追加します。昔よりだいぶ手作業が減ってきて良い時代です。
1 2 3 4 5 6 7 8 9 |
resource "aws_route53_record" "alb-web" { count = "${local.on_service && local.alb_enabled_web ? 1 : 0}" name = "${local.alb_domain_web}" type = "CNAME" zone_id = "${data.aws_route53_zone.default.id}" records = ["${aws_alb.web.dns_name}"] ttl = 60 } |
機密情報の登録
コンテナを起動する前に、ちょっとした実践的な仕込みをしておきます。例えばデータベースのパスワードですが、ハードコーディングしちゃならねぇデータはなにかしらあるわけです。その入力を AWS Systems Manager -> パラメータストア で済ませて、コンテナ内で取得させて環境変数として扱いましょう、という仕組みがあります。なので管理画面 AWS Systems Manager にいき、パラメータの作成を行います。今回は /app/DB_PASSWORD というキーで、安全な文字列として、キーIDをデフォルトの alias/aws/ssm で作成しておきます。値はなんでもいいです。ちゃんと表示されるかはアトのお楽しみです。
ECS作成
長くなってきましたが、ようやくECSにたどり着きました。クラスタとサービスを作成し、コンテナを起動するところまで頑張りましょう。進行図
ついにこの時がきました…… ECSが中央に陣取ることで、イメージとALBとで醸し出す一体感!これぞシステムのハーモニー☆クラスタ作成
たいした内容じゃなくて拍子抜けしますね。
1 2 3 4 5 |
resource "aws_ecs_cluster" "web" { count = "${local.on_ecs ? 1 : 0}" name = "${format("%s-%s-%s", local.env, local.service_name, "web")}" } |
IAM Role作成
タスク──つまりコンテナ自体が必要な権限を作成します。既存のポリシーに加えて、パラメータストアからのデータ取得も付与しています。
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 |
resource "aws_iam_role" "ecs-task" { count = "${local.on_common ? 1 : 0}" name = "ecsTaskExecutionRole" assume_role_policy = <<eof { "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } eof } resource "aws_iam_role_policy_attachment" "ecs-task" { count = "${local.on_common ? 1 : 0}" role = "${aws_iam_role.ecs-task.name}" policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } resource "aws_iam_role_policy" "ecs-task" { count = "${local.on_common ? 1 : 0}" name = "ecsTaskExecutionRolePolicy" role = "${aws_iam_role.ecs-task.id}" policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:GetParameters", "secretsmanager:GetSecretValue", "kms:Decrypt" ], "Resource": [ "arn:aws:ssm:${local.region}:${local.service_account_id}:parameter/app/*", "arn:aws:kms:${local.region}:${local.service_account_id}:alias/aws/ssm" ] } ] } EOF } |
タスク設定JSON作成
タスクの細かい設定のうち、containerDefinitions という部分の様々な設定をJSONにしてしまいます。それ以外の部分は terraform のリソースコードに直接書いていきます。
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 |
[ { "name" : "${basename(image)}", "image" : "${image}", "essential": true, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group" : "${log_group}", "awslogs-region" : "${region}", "awslogs-stream-prefix": "ecs" } }, "ulimits": [ { "name": "nofile", "softLimit": 1000000, "hardLimit": 1000000 } ], "environment" : [ { "name" : "ENV", "value": "${ENV}" }, { "name" : "DB_HOST", "value": "${DB_HOST}" } ], "secrets" : [ { "name" : "DB_PASSWORD", "valueFrom": "${DB_PASSWORD}" } ] } ] |
変数部分は terraform から渡されますので後述です。
environment が直に渡す環境変数で、
secrets がパラメータストアのキーを元に値を取得する項目です。
コンテナが secrets の取得に失敗すると、起動失敗となって何度も作り直しが発生するので、このへんは丁寧にやりましょう。ダメならダメで原因はタスクのログに出てるので確認します。
コンテナのタスク定義としては、この部分JSONを含む、もっと大きなJSONになるので、どのくらいの内容になるのかはあとで管理画面でタスク定義の詳細を確認するとよいです。
タスク定義を作成
で、上記JSONを使いつつタスク定義をします。template_file で変数に値を渡しつつレンダリング!
※template_file の vars 配下で list, hash が使えればスマートに書けるのに!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
data "template_file" "ecs_task_web" { template = "${file("ecs_task_web.json")}" vars { image = "${local.ecs_config_web["image"]}" region = "${local.ecs_config_web["region"]}" log_group = "${local.ecs_config_web["log_group"]}" ENV = "${local.env}" DB_HOST = "${lookup(local.ecs_value_web["environments"], "DB_HOST")}" DB_PASSWORD = "${lookup(local.ecs_value_web["secrets"], "DB_PASSWORD")}" } } |
ゴリッとタスク定義を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
resource "aws_ecs_task_definition" "web" { count = "${local.on_ecs ? 1 : 0}" family = "${format("%s-%s-%s", local.env, local.service_name, "web")}" container_definitions = "${data.template_file.ecs_task_web.rendered}" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" execution_role_arn = "${local.ecs_config_web["role_arn"]}" cpu = "${local.ecs_config_web["cpu"]}" memory = "${local.ecs_config_web["memory"]}" depends_on = [ "aws_cloudwatch_log_group.ecs_task_web" ] } |
FARGATE は awsvpc モードじゃないとダメとか、
CPU / Memory のセットはある程度決まっているとか、
いくつか決まりごとがあります。
CloudWatch Logsにデータも送られるので、期限を切るために作成します。
1 2 3 4 5 6 |
resource "aws_cloudwatch_log_group" "ecs_task_web" { count = "${local.on_ecs ? 1 : 0}" name = "${local.ecs_config_web["log_group"]}" retention_in_days = 7 } |
サービス作成
ここが中核になります。クラスタとタスク定義を関連付け、
VPC Networkの指定、
ALB TargetGroupの指定、
デプロイについて(はまた別記事にて)、
そして運用で TargetGroup や 台数が変わるので無視したり、task_definition を terraformから(APIから)変化させようとするとエラーになるから無視、といった小難しい内容になっています。
ここを仕留めれば終わったも同然です!いくぞ~
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 |
resource "aws_ecs_service" "web" { count = "${local.on_ecs_web ? 1 : 0}" name = "${format("%s-%s-%s", local.env, local.service_name, "web")}" cluster = "${aws_ecs_cluster.web.id}" task_definition = "${aws_ecs_task_definition.web.arn}" launch_type = "FARGATE" desired_count = 0 deployment_minimum_healthy_percent = "100" deployment_maximum_percent = "200" scheduling_strategy = "REPLICA" health_check_grace_period_seconds = 300 enable_ecs_managed_tags = true propagate_tags = "SERVICE" network_configuration { subnets = ["${slice(aws_subnet.public.*.id, 0, local.ecs_config_web["az_num"])}"] security_groups = ["${aws_security_group.web_private.id}"] assign_public_ip = "true" } load_balancer { target_group_arn = "${aws_alb_target_group.ecs-blue-web.id}" container_name = "${basename(local.ecs_config_web["image"])}" container_port = 80 } deployment_controller { type = "CODE_DEPLOY" } tags { service = "${local.service_name}" env = "${local.env}" } propagate_tags = "SERVICE" depends_on = ["aws_alb_listener.web"] lifecycle { ignore_changes = ["task_definition", "load_balancer", "desired_count"] } } |
どーだ!とりあえず、terraform apply が通ることを祈れぃ!!
動作確認
念願のコンテナを動かすぞ!タスクを起動
ECS管理画面で、クラスタ -> サービス に行き、「更新」から「タスクの数」を 1 に変更します。それ以外は特に変更しないで、眺めてスルーしていけばOKです。更新したら、クラスタまたはサービスのタブにある「タスク」に1つ追加され、ステータスが RUNNING になったことや、EC2 TargetGroup(blue) に入ったことを確認します。もしうまくいかなければ、サービス -> イベント のログや、タスク詳細のログを確認して対処します。
HTTP(S)の確認
ここまできて思い出したけど、WEB用のセキュリティグループを terraform コードで紹介してなかったな…… ま、それくらい、いっか。ここまできたらALBに接続するしかねぇ!
1 |
curl https://test.example.com/test/index -H "Host: test.example.com" |
ちなみに、タスク詳細を見たら IPアドレスも記載されてるので、http://172.16.0.1 みたいにしたら直接アクセスもできますぞ!
で、こんな感じに表示されたらOK(一部だけ記載)
1 2 3 4 5 6 7 8 9 10 11 |
<body> This is test page.<br> <br> CPU Type = model name : Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz <br> <br> List of environments and secrets.<br> ENV (environments) = production<br> DB_HOST (environments) = 1.2.3.4<br> DB_PASSWORD (secrets) = NaishodaYO<br> </body> |
タスク定義で設定した、環境変数とパラメータストアの値が出れば大勝利です。
WEBサーバーのログ
今回は nginx.conf でアクセスログとエラーログを、標準出力と標準エラー出力に出したので、タスク詳細のタブ「Logs」や CloudWatch Logs のログから、WEBサーバーとしてのログを確認できます。ファイルに残さずこうしたのは、コンテナの基本の1つであるログの扱いを意識したかったからです。
実戦ではどんなログを取り扱って、なにでストリームして、どこに保存するかという課題がある、ということでもあります。
基本的なことしかやっていないにも関わらず、リソースの種類が多いし絡みが複雑で、「ね、簡単でしょ?」とはいえない雰囲気ですね。ただ、一度組んでしまえば、コードコピペと設定編集だけして terraform apply 一発でここまで持っていけるようにできるので、最初に自分とこの環境に合わせてコード書くのだけ辛抱です。
次へまいります:デプロイ