AWS初心者がECS (Fargate) を使ってアプリ環境構築をしてみた話
AWS初心者がECS (Fargate) を使ってアプリ環境構築をしてみた話:
みなさんはじめまして、dev.aokiと申します。あっQiitaはTwitterアカウントか!まぁいっか。
IT系でふらふらと仕事をしておりまして、今年9月からオプトに入社しました。
入社前まではSIのプロジェクトやWindowsデスクトップアプリケーションの開発をしておりまして、
クラウドサービスを使ったWebアプリケーションを本格的にやるのは今回初めてでした。
そんなわけで色々と学びがありましたが、
特にネット上にやりたかったことドンピシャな情報が少なく自分が苦労した部分についてまとめて、
今後類似の構成で環境構築したい方の役に少しでも立てれば幸いかなと思い、記録がてら記事を書かせて頂いております。
Qiitaは閲覧専門だったので、書く側になるのはちょっとばかし緊張しております。
内容やら書き方やらに不備がございましたらご指摘いただけると幸いです。
どうぞよろしくお願いいたします。
この記事では、AWSについてほぼズブの素人の私が
担当プロダクトの環境構築をして学んだことをまとめています。
こんな感じの環境構成を構築します。
担当プロダクトがSPAということで、フロントサーバ->APIサーバというアクセスではなく、
並立するような形になっています。
GitHubにTerraformによる環境構築コードを上げてあります。
こちら
フロントエンド、API、データベースに分かれている、いわゆる三層構成ですね。
弊社のプロダクトはSPA化が進んでいるので、APIについてはフロントのサーバからではなく
ユーザからアクセスできる必要があるのでこんな感じの図になっています。
フロントからしかAPIをアクセスしない場合は、セキュリティグループで制御した方がいいですね。
今回の記事で特に取り上げたいのはECSとFargateです。
ECSはともかく、Fargateは東京リージョンに来てまだ日が浅いので(※執筆日は2018年12月)
ネット上にまとまった記事が少なく、なかなか苦戦しました。
Amazon Elastic Container Service とは
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.html
Amazon ECS の AWS Fargate
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/AWS_Fargate.html
ECSのうれしみは、個人的にはスケールしやすいことではないかと思います。
物理サーバは論外にしても、EC2インスタンスにしたって気軽にボコボコ増やせばいいってもんではなく
色々面倒ごとがあるかと思います。
その点、ECSの場合は立ち上げるインスタンス数を設定するだけで同一内容のインスタンスが
ガガガーっと増えてくれるので、急なワークロード増加にも対応しやすいと思います。
で、Fargateだと更に進み、ホストマシンの設定等に心を砕く必要がないです。
コンテナだけで動いてくれるので、開発環境でコンテナ化してあれば
ローカルと同じように動作するはずです、理屈としては。
いやー、便利な時代になりましたね……インフラそんなに得意じゃないんですが
そんなに抵抗なく進められそうです。
今回は自分のタスクで担当していた以下の部分について記述します。
それでは私がやった構築順序に沿って進めていきましょう。
まずAWSに接続するための情報を記述します。
Terraformの設定です。
特筆すべきは
何も書かなければローカルに
ローカルに保存されます。
個人開発ならこれで構いませんが、チームでやるとなると連携が面倒です。
今回の場合は事前にS3バケットを用意しておき、そこに保存するように設定しました。
これで他のメンバーとも設定が共有できることになります、よかったですね。
Backend
https://www.terraform.io/docs/backends/index.html
こちらはAWSとの接続設定です。
アクセスキーとシークレットキーを直接書くこともできますが、
GitHubなどでコードを管理する場合は直接書きたくないですよね。
profile名を指定し、ローカルでのみ保持することにしています。
ちなみに
変数は
この状態で
ようやくスタートラインですね、これだけでも割と苦労しました私。。。。
さて、兎にも角にも足回りがそろわないと話になりません。
サクサク作りましょう。
今回はVPCを1つと、公開用サブネットと非公開用サブネットをそれぞれAZをap-northeast-a/cに1つずつで計4つ作成しました。
このあたりは構成図書いた時点で悩む必要もなしですね。
サブネットのCIDR定義をしているところにある
指定したCIDRのサブネットを自動計算してくれる機能です。
これを利用すれば、VPCのCIDRを変更したときにサブネットも自動追従してくれるようになります、便利ですね。
https://www.terraform.io/docs/configuration/interpolation.html#cidrsubnet-iprange-newbits-netnum-
余談ですが、
現在のワークスペース名を取得しています。
ワークスペースを使うと、開発用/ステージング用/本番用みたいな感じで同じコードを使いまわしつつ
別環境として管理することができて便利です。
https://www.terraform.io/docs/state/workspaces.html
先ほど、公開/非公開のサブネットと言いましたが、作成した時点でそのあたりの設定をしているわけではありません。
ルーティングをしてあげて初めて外の世界に行けるかどうかが決定します。
というわけでルートテーブルの設定をしましょう。
外のネットワークへの出口となる、インターネットゲートウェイを作成しています。
はい、ここですね。
外向けのルートテーブルと、プライベートのみのルートテーブルを作成して、各サブネットを紐づけています。
厳密には、デフォルトルートテーブルという、VPCが作成されると自動的に一つ作られるテーブルをプライベートとして扱い、
特別に外に出られるルートテーブルを別途作成しています。
新たにサブネットを作成されたとき、そのサブネットはデフォルトルートテーブルに暗黙的に紐づけられるため、
何の気なしに作った新インスタンスが勝手に公開されないように、デフォルトの方を非公開にしています。
ちなみに、
ルート設定がされます。
ルーティングで各サブネットの公開/非公開は設定できましたが、
セキュリティグループを使うともう少し細かくアクセスの制限をかけられます。
という訳でこちらも設定していきましょう。
冒頭でも言いましたが、今回は以下のようなアクセスを許可するようにしています。
矢印が許可されるアクセスだと思って見てください。
セキュリティグループのIn/Out設定はサブネット単位またはセキュリティグループ単位で設定できます。
今回はセキュリティグループ単位で設定するようにしています。
ルールについては
後からルールに変更があった場合に依存するインスタンスも煽りを受けて作り直しが発生したりします。
このため今回はルールごとに定義をし、付け外しの影響を受けないように作っています。
さて。長くなりましたが、これでようやく足回りができました。
ようやくインスタンス作りに着手しましょう。
ECSで動作するコンテナインスタンスは動的にスケーリングできます。
将来的に負荷が高まった場合にはロードバランスをしたいということもありそうなので、
事前に仕組みを作っておこうと思います。
今回はフロントエンド用にHTTP 80番ポートへのアクセス、
API用にHTTP 8080番ポートへのアクセスの2つをターゲットグループとして用意しました。
ECSを使うなら、サービスディスカバリを使うという手もあります。
諸々の事情で今回は利用できませんでしたが、参考までにリンクを貼っておきます。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-discovery.html
コンテナベースでのアプリだと、サーバ内にデータを確保しておくわけにいかないので
永続データの確保が必要になりますね。
というわけで今回はRDSを利用する想定で立てておきます。
あんまりややこしいことはないですね。
ユーザ名やパスワードを変数化してるぐらいでしょうか。
はい、ずいぶん後ろになってしまいました。
本題のECSの設定です。
こちらはFargateでコンテナを立ち上げるためのDockerイメージを格納するリポジトリです。
DockerHubを使うこともできますが、独自サービスを使うなら同じAWS上のこちらを使うのがいいかなと思います。
DockerHubのプライベートリポジトリからイメージをPullするのは認証とか細々要りますし。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/private-auth.html
定義するだけなので簡単ですね。
作成されたらコンテナイメージをプッシュする必要があります。
プッシュされていないと後段処理となるサービス立ち上げがうまくできなくなります。
これを避けるため、Provisionerを使って作成と同時にバージョン1となるイメージをプッシュしておきましょう。
私は
地味にハマりポイントだったのがこちら。
ECS上で動かすためのIAMロールを作ります。こちらはJSON形式で記述します。
この定義を用いてIAM Roleを作成します。
くっつけるポリシーは今回はAWSが元々提供してくれている
というものを使っています。
タスク定義の項目でも話しますが、タスク自体のロールと、タスク実行ロールというものが分かれています。
ここで作っているのは、タスク実行ロールのためのロールで、タスクの管理のために必要な権限を与えています。
ECS上で動作するサービスやタスクのグループです。
EC2インスタンスやFargateのコンテナを立ち上げるための土台みたいなイメージでとらえてよさそうですね。
ちなみにタイプがEC2のものもFargateのものも共存できます。
わかりやすい名前でサクッと作りましょう。
さて、今回のメインですね。
コンテナイメージを動かすサービスのためのタスクを定義します。
こちらもJSON形式です。コンソール上でも同じものが見れますね。
やはり別ファイルに出してしまいましょう。
お作法はそちらに従えばだいたいOKです。
私はあまり知見がなかったため、実行時コマンドの上書きをする
ちなみにコマンドにクォートがあるようなケースは要注意です。
シングルクォートて囲った文字列を囲ったまま渡したらエラーになりました。つら。
いよいよタスク定義を作成します。
(API分は省略)
はい、出ましたIAMロール。
例えばAPIはS3を触るよ、みたいなことがあればその許可が必要になります。
一方の
つまり、ECSでタスクを作ったり壊したり、コンテナ数をオートスケールさせたりといったことをするロールですので、
ECSに関する許可を諸々持たなくてはいけません。私はここでかなり長いことハマりました。
なーにがハマるって、作ること自体は普通にできるんですが、後段のサービスまで行ってから
「アルェー、なんでタスクが上がってこないんだ?」みたいに発覚するからです。
しかもログも出ないもんだからなかなか苦しむ苦しむ……愚痴っぽくなってきました。
またもう一つポイントなのが、
こちらも作成時は普通に通って、動かしてから発覚するタイプのエラーです。ご注意を。
無事タスク定義が作れたら、サービスの立ち上げです。
(API分は省略)
これで正しく設定されていれば、コンソール上で元気に動くコンテナ君の姿が見られることでしょう。おめでとうございます。
ハマりポイントが結構ありました。
WebコンソールだとUI側でよしなに可能な設定の絞り込み等してくれるのですが、Terraformではそこまでの親切はありません。
しかしそれもかなり親切な方で、ECSに関しては 実際にコンテナが動き出し、ヘルスチェックが通るまでは安心できない
というのが体感的なところです。
ECSで特にハマったポイントを挙げて見ます。
愚痴っぽいことを書いていますが、実際にはこのサービスは気に入っています。
立ち上げの苦労はさておき、運用はずいぶん楽です。スケールしやすいし、ダウンタイムも短くできますからね。
そして何より環境がコード化されていることの喜びが大きいです。バージョン管理できるってすばらしい。
更新日付が鬼のように古い謎のインフラドキュメントを共有フォルダから発見して、実体との差分を細々チェックしてからようやくインフラ変更作業に取り掛かるみたいなつらみはないはずです、きっと
今回は以上です。
自分もまだAWSまともに触り始めて2,3か月ということで、日々覚えることが新鮮でとても楽しく仕事しています。
この記事も誤りやより良い方法はきっとあることと思いますので、その際はお気軽に編集リクエストをください。
よろしくお願いいたします。
ごあいさつ
みなさんはじめまして、dev.aokiと申します。あっQiitaはTwitterアカウントか!まぁいっか。IT系でふらふらと仕事をしておりまして、今年9月からオプトに入社しました。
入社前まではSIのプロジェクトやWindowsデスクトップアプリケーションの開発をしておりまして、
クラウドサービスを使ったWebアプリケーションを本格的にやるのは今回初めてでした。
そんなわけで色々と学びがありましたが、
特にネット上にやりたかったことドンピシャな情報が少なく自分が苦労した部分についてまとめて、
今後類似の構成で環境構築したい方の役に少しでも立てれば幸いかなと思い、記録がてら記事を書かせて頂いております。
Qiitaは閲覧専門だったので、書く側になるのはちょっとばかし緊張しております。
内容やら書き方やらに不備がございましたらご指摘いただけると幸いです。
どうぞよろしくお願いいたします。
この記事について
この記事では、AWSについてほぼズブの素人の私が担当プロダクトの環境構築をして学んだことをまとめています。
構築環境の構成
こんな感じの環境構成を構築します。担当プロダクトがSPAということで、フロントサーバ->APIサーバというアクセスではなく、
並立するような形になっています。
GitHubにTerraformによる環境構築コードを上げてあります。
こちら
フロントエンド、API、データベースに分かれている、いわゆる三層構成ですね。
弊社のプロダクトはSPA化が進んでいるので、APIについてはフロントのサーバからではなく
ユーザからアクセスできる必要があるのでこんな感じの図になっています。
フロントからしかAPIをアクセスしない場合は、セキュリティグループで制御した方がいいですね。
今回の記事で特に取り上げたいのはECSとFargateです。
ECSはともかく、Fargateは東京リージョンに来てまだ日が浅いので(※執筆日は2018年12月)
ネット上にまとまった記事が少なく、なかなか苦戦しました。
公式のドキュメント
Amazon Elastic Container Service とはhttps://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.html
Amazon ECS の AWS Fargate
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/AWS_Fargate.html
ECSのうれしみ
ECSのうれしみは、個人的にはスケールしやすいことではないかと思います。物理サーバは論外にしても、EC2インスタンスにしたって気軽にボコボコ増やせばいいってもんではなく
色々面倒ごとがあるかと思います。
その点、ECSの場合は立ち上げるインスタンス数を設定するだけで同一内容のインスタンスが
ガガガーっと増えてくれるので、急なワークロード増加にも対応しやすいと思います。
Fargateのうれしみ
で、Fargateだと更に進み、ホストマシンの設定等に心を砕く必要がないです。コンテナだけで動いてくれるので、開発環境でコンテナ化してあれば
ローカルと同じように動作するはずです、理屈としては。
いやー、便利な時代になりましたね……インフラそんなに得意じゃないんですが
そんなに抵抗なく進められそうです。
記事の範囲
今回は自分のタスクで担当していた以下の部分について記述します。- 目的を実現するためのAWSサービス構成
- 各サービスの設定内容
- 今回の構成をデプロイするためのTerraform記述
- AWS各サービスの説明
- 公式ドキュメント( https://docs.aws.amazon.com/index.html#lang/ja_jp )やネット上の各記事で私より詳しく解説されています
- Dockerコンテナの作り方
- 白状すると私もまだ熟知してないorz...
- Terraformの全般的な使い方
- Terraformは公式ドキュメントを読むのがとにかく最短でした( https://www.terraform.io/ )
構築手順ごとに説明
それでは私がやった構築順序に沿って進めていきましょう。
接続設定
まずAWSに接続するための情報を記述します。main.tf
terraform { required_version = ">= 0.11.0" backend "s3" { region = "ap-northeast-1" profile = "hogehoge" bucket = "fugafuga" key = "piyopiyo/terraform.tfstate" } }
特筆すべきは
backend
の部分でしょうか。何も書かなければローカルに
tfstate
というTerraformの管理するリソース状態を保存するファイルがローカルに保存されます。
個人開発ならこれで構いませんが、チームでやるとなると連携が面倒です。
今回の場合は事前にS3バケットを用意しておき、そこに保存するように設定しました。
これで他のメンバーとも設定が共有できることになります、よかったですね。
Backend
https://www.terraform.io/docs/backends/index.html
provider.tf
provider "aws" { region = "${var.region}" profile = "${var.profile}" }
アクセスキーとシークレットキーを直接書くこともできますが、
GitHubなどでコードを管理する場合は直接書きたくないですよね。
profile名を指定し、ローカルでのみ保持することにしています。
ちなみに
${var.hogehoge}
という書き方はTerraformの変数を呼んでいます。変数は
terraform.tfvars
というファイルで管理しています。この状態で
terraform init
とコマンドを実行すると、Terraformでの作業ができる状態になります。ようやくスタートラインですね、これだけでも割と苦労しました私。。。。
ネットワーク関連
さて、兎にも角にも足回りがそろわないと話になりません。サクサク作りましょう。
VPC/サブネット
vpc.tf
resource "aws_vpc" "vpc" { cidr_block = "10.1.0.0/16" enable_dns_support = "true" enable_dns_hostnames = "true" tags = { "Name" = "${var.product}-${terraform.workspace}-vpc" "Product" = "${var.product}" "Env" = "${terraform.workspace}" } }
subnet.tf
# Public-A resource "aws_subnet" "subnet-public-a" { vpc_id = "${aws_vpc.vpc.id}" cidr_block = "${cidrsubnet(aws_vpc.vpc.cidr_block,8,1)}" availability_zone = "ap-northeast-1a" tags { Name = "${var.product}-${terraform.workspace}-subnet-public-a" Product = "${var.product}" Env = "${terraform.workspace}" } } # Public-C resource "aws_subnet" "subnet-public-c" { vpc_id = "${aws_vpc.vpc.id}" cidr_block = "${cidrsubnet(aws_vpc.vpc.cidr_block,8,2)}" availability_zone = "ap-northeast-1c" tags { Name = "${var.product}-${terraform.workspace}-subnet-public-c" Product = "${var.product}" Env = "${terraform.workspace}" } } # Private-A resource "aws_subnet" "subnet-private-a" { vpc_id = "${aws_vpc.vpc.id}" cidr_block = "${cidrsubnet(aws_vpc.vpc.cidr_block,8,65)}" availability_zone = "ap-northeast-1a" tags { Name = "${var.product}-${terraform.workspace}-subnet-private-a" Product = "${var.product}" Env = "${terraform.workspace}" } } # Private-C resource "aws_subnet" "subnet-private-c" { vpc_id = "${aws_vpc.vpc.id}" cidr_block = "${cidrsubnet(aws_vpc.vpc.cidr_block,8,66)}" availability_zone = "ap-northeast-1c" tags { Name = "${var.product}-${terraform.workspace}-subnet-private-c" Product = "${var.product}" Env = "${terraform.workspace}" }
このあたりは構成図書いた時点で悩む必要もなしですね。
サブネットのCIDR定義をしているところにある
cidrsubnet
っていうのがなかなか便利でして、指定したCIDRのサブネットを自動計算してくれる機能です。
これを利用すれば、VPCのCIDRを変更したときにサブネットも自動追従してくれるようになります、便利ですね。
https://www.terraform.io/docs/configuration/interpolation.html#cidrsubnet-iprange-newbits-netnum-
余談ですが、
${terraform.workspace}
というのは、Terraformのワークスペース機能を用いて、現在のワークスペース名を取得しています。
ワークスペースを使うと、開発用/ステージング用/本番用みたいな感じで同じコードを使いまわしつつ
別環境として管理することができて便利です。
https://www.terraform.io/docs/state/workspaces.html
ルーティング
先ほど、公開/非公開のサブネットと言いましたが、作成した時点でそのあたりの設定をしているわけではありません。ルーティングをしてあげて初めて外の世界に行けるかどうかが決定します。
というわけでルートテーブルの設定をしましょう。
gateway.tf
resource "aws_internet_gateway" "main-gw" { vpc_id = "${aws_vpc.vpc.id}" tags { Name = "${var.product}-${terraform.workspace}-main-gw" Product = "${var.product}" Env = "${terraform.workspace}" } }
routes.tf
# Default(Private) resource "aws_default_route_table" "default-route-table" { default_route_table_id = "${aws_vpc.vpc.default_route_table_id}" tags { Name = "${var.product}-${terraform.workspace}-default-route-table" Product = "${var.product}" Env = "${terraform.workspace}" } } resource "aws_route_table_association" "route-private-a" { route_table_id = "${aws_default_route_table.default-route-table.id}" subnet_id = "${aws_subnet.subnet-private-a.id}" } resource "aws_route_table_association" "route-private-c" { route_table_id = "${aws_default_route_table.default-route-table.id}" subnet_id = "${aws_subnet.subnet-private-c.id}" } # Public resource "aws_route_table" "public-route-table" { vpc_id = "${aws_vpc.vpc.id}" route { cidr_block = "0.0.0.0/0" gateway_id = "${aws_internet_gateway.main-gw.id}" } tags { Name = "${var.product}-${terraform.workspace}-public-route-table" Product = "${var.product}" Env = "${terraform.workspace}" } } resource "aws_route_table_association" "route-public-a" { route_table_id = "${aws_route_table.public-route-table.id}" subnet_id = "${aws_subnet.subnet-public-a.id}" } resource "aws_route_table_association" "route-public-c" { route_table_id = "${aws_route_table.public-route-table.id}" subnet_id = "${aws_subnet.subnet-public-c.id}" }
外向けのルートテーブルと、プライベートのみのルートテーブルを作成して、各サブネットを紐づけています。
厳密には、デフォルトルートテーブルという、VPCが作成されると自動的に一つ作られるテーブルをプライベートとして扱い、
特別に外に出られるルートテーブルを別途作成しています。
新たにサブネットを作成されたとき、そのサブネットはデフォルトルートテーブルに暗黙的に紐づけられるため、
何の気なしに作った新インスタンスが勝手に公開されないように、デフォルトの方を非公開にしています。
ちなみに、
route
に何もつけないでおくと、デフォルトでVPC内に限りすべての通信ができるというようなルート設定がされます。
セキュリティグループ
ルーティングで各サブネットの公開/非公開は設定できましたが、セキュリティグループを使うともう少し細かくアクセスの制限をかけられます。
という訳でこちらも設定していきましょう。
security-groups.tf
# Default resource "aws_default_security_group" "security-group-default" { vpc_id = "${aws_vpc.vpc.id}" tags { Name = "${var.product}-${terraform.workspace}-security-group-default" Product = "${var.product}" Env = "${terraform.workspace}" } } # for ALB resource "aws_security_group" "security-group-alb" { name = "${var.product}-${terraform.workspace}-security-group-alb" vpc_id = "${aws_vpc.vpc.id}" tags { Name = "${var.product}-${terraform.workspace}-security-group-alb" Product = "${var.product}" Env = "${terraform.workspace}" } } # In: All HTTP(port 80) resource "aws_security_group_rule" "security-group-alb-in-rule-http80" { security_group_id = "${aws_security_group.security-group-alb.id}" type = "ingress" from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # In: All HTTP(port 8080) resource "aws_security_group_rule" "security-group-alb-in-rule-http8080" { security_group_id = "${aws_security_group.security-group-alb.id}" type = "ingress" from_port = 8080 to_port = 8080 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # In: All HTTPS resource "aws_security_group_rule" "security-group-alb-in-rule-https443" { security_group_id = "${aws_security_group.security-group-alb.id}" type = "ingress" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # Out: ALL OK resource "aws_security_group_rule" "security-group-alb-out-rule-all" { security_group_id = "${aws_security_group.security-group-alb.id}" type = "egress" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } # for Frontend resource "aws_security_group" "security-group-front" { name = "${var.product}-${terraform.workspace}-security-group-front" vpc_id = "${aws_vpc.vpc.id}" tags { Name = "${var.product}-${terraform.workspace}-security-group-front" Product = "${var.product}" Env = "${terraform.workspace}" } } # In: ALB resource "aws_security_group_rule" "security-group-front-in-rule-alb" { security_group_id = "${aws_security_group.security-group-front.id}" type = "ingress" from_port = 0 to_port = 0 protocol = "-1" source_security_group_id = "${aws_security_group.security-group-alb.id}" } # Out: ALL OK resource "aws_security_group_rule" "security-group-front-out-rule-all" { security_group_id = "${aws_security_group.security-group-front.id}" type = "egress" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } # for API resource "aws_security_group" "security-group-api" { name = "${var.product}-${terraform.workspace}-security-group-api" vpc_id = "${aws_vpc.vpc.id}" tags { Name = "${var.product}-${terraform.workspace}-security-group-api" Product = "${var.product}" Env = "${terraform.workspace}" } } # In: ALB resource "aws_security_group_rule" "security-group-api-in-rule-alb" { security_group_id = "${aws_security_group.security-group-api.id}" type = "ingress" from_port = 0 to_port = 0 protocol = "-1" source_security_group_id = "${aws_security_group.security-group-alb.id}" } # Out: ALL OK resource "aws_security_group_rule" "security-group-api-out-rule-all" { security_group_id = "${aws_security_group.security-group-api.id}" type = "egress" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } # for RDS resource "aws_security_group" "security-group-rds" { name = "${var.product}-${terraform.workspace}-security-group-rds" vpc_id = "${aws_vpc.vpc.id}" tags { Name = "${var.product}-${terraform.workspace}-security-group-rds" Product = "${var.product}" Env = "${terraform.workspace}" } } # In: API / MariaDB resource "aws_security_group_rule" "security-group-rds-in-rule-api" { security_group_id = "${aws_security_group.security-group-rds.id}" type = "ingress" from_port = 3306 to_port = 3306 protocol = "tcp" source_security_group_id = "${aws_security_group.security-group-api.id}" } # Out: ALL OK resource "aws_security_group_rule" "security-group-rds-out-rule-all" { security_group_id = "${aws_security_group.security-group-rds.id}" type = "egress" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }
矢印が許可されるアクセスだと思って見てください。
セキュリティグループのIn/Out設定はサブネット単位またはセキュリティグループ単位で設定できます。
今回はセキュリティグループ単位で設定するようにしています。
ルールについては
aws_security_group
内に記述することもできるのですが、後からルールに変更があった場合に依存するインスタンスも煽りを受けて作り直しが発生したりします。
このため今回はルールごとに定義をし、付け外しの影響を受けないように作っています。
インスタンス
さて。長くなりましたが、これでようやく足回りができました。ようやくインスタンス作りに着手しましょう。
ALB
ECSで動作するコンテナインスタンスは動的にスケーリングできます。将来的に負荷が高まった場合にはロードバランスをしたいということもありそうなので、
事前に仕組みを作っておこうと思います。
alb.tf
# ALB resource "aws_alb" "alb" { name = "${var.product}-${terraform.workspace}-alb" internal = false load_balancer_type = "application" security_groups = ["${aws_security_group.security-group-alb.id}"] subnets = ["${aws_subnet.subnet-public-a.id}", "${aws_subnet.subnet-public-c.id}"] tags { Name = "${var.product}-${terraform.workspace}-alb" Product = "${var.product}" Env = "${terraform.workspace}" } } # ALB target group resource "aws_alb_target_group" "alb-target-group-front" { name = "${var.product}-${terraform.workspace}-alb-tg-front" port = 80 protocol = "HTTP" vpc_id = "${aws_vpc.vpc.id}" target_type = "ip" health_check { interval = 60 path = "/" protocol = "HTTP" timeout = 20 unhealthy_threshold = 4 matcher = 200 } tags { Name = "${var.product}-${terraform.workspace}-alb-target-group-front" Product = "${var.product}" Env = "${terraform.workspace}" } } resource "aws_alb_target_group" "alb-target-group-api" { name = "${var.product}-${terraform.workspace}-alb-tg-api" port = 8080 protocol = "HTTP" vpc_id = "${aws_vpc.vpc.id}" target_type = "ip" health_check { interval = 60 path = "/api/authentication" protocol = "HTTP" timeout = 20 unhealthy_threshold = 4 matcher = 200 } tags { Name = "${var.product}-${terraform.workspace}-alb-target-group-api" Product = "${var.product}" Env = "${terraform.workspace}" } } # ALB Listener resource "aws_alb_listener" "alb-listener-front" { load_balancer_arn = "${aws_alb.alb.arn}" port = "80" protocol = "HTTP" default_action { target_group_arn = "${aws_alb_target_group.alb-target-group-front.arn}" type = "forward" } } resource "aws_alb_listener" "alb-listener-api" { load_balancer_arn = "${aws_alb.alb.arn}" port = "8080" protocol = "HTTP" default_action { target_group_arn = "${aws_alb_target_group.alb-target-group-api.arn}" type = "forward" } }
API用にHTTP 8080番ポートへのアクセスの2つをターゲットグループとして用意しました。
ECSを使うなら、サービスディスカバリを使うという手もあります。
諸々の事情で今回は利用できませんでしたが、参考までにリンクを貼っておきます。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-discovery.html
RDS
コンテナベースでのアプリだと、サーバ内にデータを確保しておくわけにいかないので永続データの確保が必要になりますね。
というわけで今回はRDSを利用する想定で立てておきます。
rds.tf
# Subnet group resource "aws_db_subnet_group" "rds-subnet-group" { name = "${var.product}-${terraform.workspace}-rds-subnet-group" subnet_ids = ["${aws_subnet.subnet-private-a.id}", "${aws_subnet.subnet-private-c.id}"] } # Parameter group resource "aws_db_parameter_group" "rds-params" { name = "${var.product}-${terraform.workspace}-rds-params" family = "mariadb10.2" } # Write DB resource "aws_db_instance" "rds" { identifier = "${var.product}-${terraform.workspace}-rds" allocated_storage = "${var.db_storage}" storage_type = "gp2" engine = "mariadb" engine_version = "10.2.15" instance_class = "db.t2.micro" name = "${var.db_name}" username = "${var.db_user}" password = "${var.db_pass}" parameter_group_name = "${aws_db_parameter_group.rds-params.name}" db_subnet_group_name = "${aws_db_subnet_group.rds-subnet-group.name}" vpc_security_group_ids = ["${aws_security_group.security-group-rds.id}"] availability_zone = "ap-northeast-1a" multi_az = false backup_retention_period = 1 skip_final_snapshot = true tags { Name = "${var.product}-${terraform.workspace}-rds" Product = "${var.product}" Env = "${terraform.workspace}" } }
ユーザ名やパスワードを変数化してるぐらいでしょうか。
ECS
はい、ずいぶん後ろになってしまいました。本題のECSの設定です。
ECR
こちらはFargateでコンテナを立ち上げるためのDockerイメージを格納するリポジトリです。DockerHubを使うこともできますが、独自サービスを使うなら同じAWS上のこちらを使うのがいいかなと思います。
DockerHubのプライベートリポジトリからイメージをPullするのは認証とか細々要りますし。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/private-auth.html
ecr.tf
# Front resource "aws_ecr_repository" "repository-front" { name = "${var.product}-front" provisioner "local-exec" { command = "build-image.sh front ${self.repository_url} ${self.name}" } } # API resource "aws_ecr_repository" "repository-api" { name = "${var.product}-api" provisioner "local-exec" { command = "build-image.sh api ${self.repository_url} ${self.name}" } }
作成されたらコンテナイメージをプッシュする必要があります。
プッシュされていないと後段処理となるサービス立ち上げがうまくできなくなります。
これを避けるため、Provisionerを使って作成と同時にバージョン1となるイメージをプッシュしておきましょう。
私は
build-image.sh
というスクリプトを作成して実行させるようにしました。
IAM ロール
地味にハマりポイントだったのがこちら。ECS上で動かすためのIAMロールを作ります。こちらはJSON形式で記述します。
.tf
ファイルにベタで書く方法もありますが、見づらいので別ファイルに出してしまいましょう。task-role.json
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
くっつけるポリシーは今回はAWSが元々提供してくれている
AmazonECSTaskExecutionRolePolicy
というものを使っています。
iam.tf
# Task role data "template_file" "task-role-template" { template = "${file("${path.root}/iam/roles/task-role.json")}" } resource "aws_iam_role" "task-role" { name = "ecsTaskExecutionRole" assume_role_policy = "${data.template_file.task-role-template.rendered}" } resource "aws_iam_role_policy_attachment" "task-role-attachment" { role = "${aws_iam_role.task-role.name}" policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" }
ここで作っているのは、タスク実行ロールのためのロールで、タスクの管理のために必要な権限を与えています。
ECSクラスター
ECS上で動作するサービスやタスクのグループです。EC2インスタンスやFargateのコンテナを立ち上げるための土台みたいなイメージでとらえてよさそうですね。
ちなみにタイプがEC2のものもFargateのものも共存できます。
わかりやすい名前でサクッと作りましょう。
ecs.tf
resource "aws_ecs_cluster" "ecs-cluster" { name = "${var.product}-${terraform.workspace}-ecs-cluster" }
タスク定義
さて、今回のメインですね。コンテナイメージを動かすサービスのためのタスクを定義します。
こちらもJSON形式です。コンソール上でも同じものが見れますね。
やはり別ファイルに出してしまいましょう。
task-definitions/front.json
[ { "name" : "${product}-${env}-front-container", "portMappings": [ { "hostPort": 80, "protocol": "tcp", "containerPort": 80 } ], "cpu": ${cpu}, "memory": ${memory}, "memoryReservation": ${memory}, "image": "${image-url}:latest", "command": ["exec", "nginx", "-g", "daemon off;"], "environment": [ { "name": "HOGE", "value": "${hoge}" } ], "healthCheck": { "retries": 10, "command": [ "CMD-SHELL", "curl http://localhost/ || exit 1" ], "timeout": 20, "interval": 60, "startPeriod": 120 }, "essential": true } ]
docker-compose.yml
で見たことあるような項目が多いですよね。お作法はそちらに従えばだいたいOKです。
私はあまり知見がなかったため、実行時コマンドの上書きをする
command
で配列に入れるところでハマったりしました。ちなみにコマンドにクォートがあるようなケースは要注意です。
シングルクォートて囲った文字列を囲ったまま渡したらエラーになりました。つら。
いよいよタスク定義を作成します。
task-definition.tf
# Frontend data "template_file" "front-container-template" { template = "${file("${path.root}/task-definitions/front.json")}" vars { product = "${var.product}" env = "${terraform.workspace}" image-url = "${aws_ecr_repository.repository-front.repository_url}" cpu = "${var.front_cpu}" memory = "${var.front_memory}" hoge = "Nyan-path" } } resource "aws_ecs_task_definition" "front-task-definition" { family = "${var.product}-${terraform.workspace}-front-task" container_definitions = "${data.template_file.front-container-template.rendered}" task_role_arn = "${aws_iam_role.task-role.arn}" execution_role_arn = "${aws_iam_role.task-role.arn}" network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] cpu = "${var.front_cpu}" memory = "${var.front_memory}" }
はい、出ましたIAMロール。
task_role_arn
で指定するのは、実行されるタスクのロール。例えばAPIはS3を触るよ、みたいなことがあればその許可が必要になります。
一方の
execution_role_arn
で指定するのは、これらタスクを管理するロールになります。つまり、ECSでタスクを作ったり壊したり、コンテナ数をオートスケールさせたりといったことをするロールですので、
ECSに関する許可を諸々持たなくてはいけません。私はここでかなり長いことハマりました。
なーにがハマるって、作ること自体は普通にできるんですが、後段のサービスまで行ってから
「アルェー、なんでタスクが上がってこないんだ?」みたいに発覚するからです。
しかもログも出ないもんだからなかなか苦しむ苦しむ……愚痴っぽくなってきました。
またもう一つポイントなのが、
network_mode
はFargateの場合、 awscpc
しか選べません。こちらも作成時は普通に通って、動かしてから発覚するタイプのエラーです。ご注意を。
ECSサービス
無事タスク定義が作れたら、サービスの立ち上げです。ecs.tf
# Frontend resource "aws_ecs_service" "front-service" { name = "${var.product}-${terraform.workspace}-front-service" cluster = "${aws_ecs_cluster.ecs-cluster.id}" task_definition = "${aws_ecs_task_definition.front-task-definition.arn}" launch_type = "FARGATE" desired_count = 1 deployment_minimum_healthy_percent = 100 deployment_maximum_percent = 200 health_check_grace_period_seconds = 60 network_configuration { subnets = ["${aws_subnet.subnet-public-a.id}", "${aws_subnet.subnet-public-c.id}"] security_groups = ["${aws_security_group.security-group-front.id}"] assign_public_ip = "true" } load_balancer { target_group_arn = "${aws_alb_target_group.alb-target-group-front.arn}" container_name = "${var.product}-${terraform.workspace}-front-container" container_port = 80 } }
launch_type
をFARGATEにして諸々設定してあげれば無事作成完了です。これで正しく設定されていれば、コンソール上で元気に動くコンテナ君の姿が見られることでしょう。おめでとうございます。
やってみての感想
ハマりポイントが結構ありました。WebコンソールだとUI側でよしなに可能な設定の絞り込み等してくれるのですが、Terraformではそこまでの親切はありません。
plan
のときにエラーが出るのは文法レベルまでで、apply
してみたらエラーみたいなことも多々あります。しかしそれもかなり親切な方で、ECSに関しては 実際にコンテナが動き出し、ヘルスチェックが通るまでは安心できない
というのが体感的なところです。
ECSで特にハマったポイントを挙げて見ます。
- ネットワークモードがawsvpc固定であること
- ↑の合わせ技で、コンテナポートへのポートマッピングがawsvpcでは使えないこと
- IAMロールでそもそもタスクが立ち上がらないこと
- コンテナそのものが上手く立ち上がらないこと (これはECSとは無関係だが、どっちの問題かしばらくわからなかった)
- commandで渡す値がうまく渡らなかったこと (配列しんどい)
- 無事立ち上がったと思ったものの束の間、ヘルスチェックに殺されてSTOPPEDのタスクが大量に溜まること
- ちなみにヘルスチェックに殺されても、終了コードが雑に書かれて終了です、おだいじに。
- (今回は使わなかったが)サービスディスカバリのヘルスチェックがクセ強すぎなこと
- 謎ですが、Route53の
health_check_custom_config
というプロパティに不必要でも何か設定しないと、コンテナ側の設定と同じヘルスチェックをしてくれず殺されるということがわかりました
- 謎ですが、Route53の
愚痴っぽいことを書いていますが、実際にはこのサービスは気に入っています。
立ち上げの苦労はさておき、運用はずいぶん楽です。スケールしやすいし、ダウンタイムも短くできますからね。
そして何より環境がコード化されていることの喜びが大きいです。バージョン管理できるってすばらしい。
今回は以上です。
自分もまだAWSまともに触り始めて2,3か月ということで、日々覚えることが新鮮でとても楽しく仕事しています。
この記事も誤りやより良い方法はきっとあることと思いますので、その際はお気軽に編集リクエストをください。
よろしくお願いいたします。
コメント
コメントを投稿