どうも、小野です。 今回はAWS Fargateを使って、下図のような構成で作っていきます。
AWS Fargateは元々あるECSからEC2の管理を除外したサービスになります。 つまり、ECSをよりマネージドにしたものです。
Fargateについて知りたい方は公式サイトを確認してください。 aws.amazon.com
説明するにあたりAWSコンソールでもよいのですが、UIが頻繁に変わったりするので、今回はCLIベースで説明していきます。
※動作確認を行うターミナルは、コマンド実行時に変数を利用している関係上、最後まで閉じないようにしてください。
動作環境
- Mac maxOS Mojave
前提
- AWS CLIがインストール済み、かつバージョンが最新であること。
- aws configreが設定済みであること。
- Dockerコマンドが利用できること。
- jqコマンドが利用できること。
準備
ロールの作成
assume-role-policy.json
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<アカウントID>:root" }, "Action": "sts:AssumeRole" } }
ROLE_ARN=$(aws iam create-role --role-name fargate-helloworld-role --assume-role-policy-document file://./assume-role-policy.json | jq -r '.Role.Arn') aws iam attach-role-policy --role-name fargate-helloworld-role --policy-arn "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
コンテナイメージのリポジトリ作成
コンテナのリポジトリはDockerHubなどがありますが、今回はAWSのECRを使います。
HELLO_REPO_URI=$(aws ecr create-repository --repository-name helloworld/hello | jq -r '. repository. repositoryUri') WORLD_REPO_URI=$(aws ecr create-repository --repository-name helloworld/world | jq -r '. repository. repositoryUri') NGINX_REPO_URI=$(aws ecr create-repository --repository-name helloworld/nginx | jq -r '. repository. repositoryUri')
ECRにログインしておく
$(aws ecr get-login --no-include-email --region ap-northeast-1)
Webサーバのコンテナ作成
Webサーバにはnginxを使います。 nginxはALBからリクエストを受けて、サービスディスカバリを利用してアプリケーションにリクエストをします。
mkdir helloworld-nginx cd helloworld-nginx
nginxの設定
default.confを作成します。
server { listen 80; server_name localhost; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location ^~ /hello/ { # パスの先頭が「/hello/」の場合に helloアプリケーションにリクエスト proxy_pass http://hello.helloworld:8080/hello/; } location ^~ /world/ { # パスの先頭が「/world/」の場合に worldアプリケーションにリクエスト proxy_pass http://world.helloworld:8080/world/; } location / { root /usr/share/nginx/html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
「hello.helloworld」、「world.helloworld」はサービスディスカバリの機能によってDNS解決されます。
Dockerfileを作成
FROM nginx # 作成したdefault.confをnginxサーバに置きます ADD "default.conf" "/etc/nginx/conf.d/"
Dockerリポジトリにプッシュ
docker build -t helloworld-nginx . docker tag helloworld/nginx:latest ${NGINX_REPO_URI}:latest docker push ${NGINX_REPO_URI}:latest
アプリケーションのコンテナ作成
今回作成するアプリケーションは、JavaEE + Payara Microで作ります。 アプリケーションは、Hello、Worldの2つ作ります。
Helloサービス
@Path("hello") public class HelloResource { @GET @Produces(MediaType.TEXT_PLAIN) public String getText() { // Worldサービスのホストは環境変数から取得 String host = System.getenv("WORLD_HOST"); String url = "http://" + host + ":8080/world/webresources"; // Worldアプリケーションから文字列を受け取り、Helloと結合して返却する return "Hello " + ClientBuilder.newClient() .target(url) .path("world") .request() .get(String.class); } }
pom.xmlの編集
buildタグ内に追加
<build> <finalName>hello</finalName> ←追加
Dockerファイルを作成する。
FROM payara/micro # curlをインストールするため一時的にrootユーザに変更 USER root RUN apk add --no-cache curl USER payara ADD "target/hello.war" "/tmp/" ENTRYPOINT ["java", "-jar", "payara-micro.jar","--deploy","/tmp/hello.war"]
Dockerリポジトリにプッシュ
mvn clean package docker build -t helloworld-hello . docker tag helloworld/hello:latest ${HELLO_REPO_URI}:latest docker push ${HELLO_REPO_URI}:latest
Worldサービス
@Path("world") public class WorldResource { @GET @Produces(MediaType.TEXT_PLAIN) public String getText() { return "World!"; } }
pom.xmlの編集
<build> <finalName>world</finalName> ←追加
Dockerfileの作成
FROM payara/micro # curlをインストールするため一時的にrootユーザに変更 USER root RUN apk add --no-cache curl USER payara ADD "target/world.war" "/tmp/" ENTRYPOINT ["java", "-jar", "payara-micro.jar","--deploy","/tmp/world.war"]
Dockerリポジトリにプッシュ
mvn clean package docker build -t helloworld-world . docker tag helloworld/world:latest ${WORLD_REPO_URI}:latest docker push ${WORLD_REPO_URI}:latest
タスク定義の設定
タスク定義の登録をCLIで実行する際に、オプション値にjsonで渡すものがあり、シンタックスベースの場合ダブルクォートのエスケープが必要になるので、JSONで設定します。
サービスのコンテナは、内部でヘルスチェックを行うようにします。
Helloサービス
task-hello.jsonの作成
{ "family": "helloworld-hello", "networkMode": "awsvpc", "taskRoleArn": "<ROLE_ARN>", "executionRoleArn": "<ROLE_ARN>", "containerDefinitions": [ { "name": "helloworld-hello", "image": "<リポジトリURI>", "portMappings": [ { "containerPort": 8080, "hostPort": 8080, "protocol": "tcp" } ], "essential": true, "healthCheck": { "retries": 3, "command": [ "CMD-SHELL", "curl http://localhost:8080/hello/index.html || exit 1" ], "timeout": 5, "interval": 30, "startPeriod": 120 }, "environment": [ { "name": "WORLD_HOST", "value": "world.helloworld" } ] } ], "requiresCompatibilities": [ "FARGATE" ], "cpu": "512", "memory": "1024" }
Worldサービス
task-world.jsonの作成
{ "family": "helloworld-world", "networkMode": "awsvpc", "taskRoleArn": "<ROLE_ARN>", "executionRoleArn": "<ROLE_ARN>", "containerDefinitions": [ { "name": "helloworld-world", "image": "<リポジトリURI>", "portMappings": [ { "containerPort": 8080, "hostPort": 8080, "protocol": "tcp" } ], "essential": true, "healthCheck": { "retries": 3, "command": [ "CMD-SHELL", "curl http://localhost:8080/world/index.html || exit 1" ], "timeout": 5, "interval": 30, "startPeriod": 120 } } ], "requiresCompatibilities": [ "FARGATE" ], "cpu": "512", "memory": "1024" }
Nginx
task-nginx.jsonの作成
{ "family": "helloworld-nginx", "networkMode": "awsvpc", "taskRoleArn": "<ROLE_ARN>", "executionRoleArn": "<ROLE_ARN>", "containerDefinitions": [ { "name": "helloworld-nginx", "image": "<リポジトリURI>", "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ], "essential": true, "healthCheck": { "retries": 3, "command": [ "CMD-SHELL", "curl http://localhost/ || exit 1" ], "timeout": 5, "interval": 30, "startPeriod": 0 } } ], "requiresCompatibilities": [ "FARGATE" ], "cpu": "256", "memory": "512" }
環境構築
では、準備が整ったので、CLIベースに順を追って構築していきます。 作業ディレクトリは、上記で作成したtask-xxxx.jsonがある場所に移動してください。
VPCの設定
# VPC作成 VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 | jq -r '.Vpc.VpcId') # DNS hostname resolutionを有効 aws ec2 modify-vpc-attribute --vpc-id ${VPC_ID} --enable-dns-support aws ec2 modify-vpc-attribute --vpc-id ${VPC_ID} --enable-dns-hostnames # インターネットゲートウェイの作成 IGW_ID=$(aws ec2 create-internet-gateway | jq -r '.InternetGateway.InternetGatewayId') # VPCにインターネットゲートウェイを追加 aws ec2 attach-internet-gateway --internet-gateway-id ${IGW_ID} --vpc-id ${VPC_ID} # サブネット(パブリック:2、プライベート:2)を作成 PUBLIC_SUBNET_A_ID=$(aws ec2 create-subnet --vpc-id ${VPC_ID} --availability-zone ap-northeast-1a --cidr-block 10.0.0.0/24 | jq -r '.Subnet.SubnetId') PUBLIC_SUBNET_C_ID=$(aws ec2 create-subnet --vpc-id ${VPC_ID} --availability-zone ap-northeast-1c --cidr-block 10.0.1.0/24 | jq -r '.Subnet.SubnetId') PRIVATE_SUBNET_A_ID=$(aws ec2 create-subnet --vpc-id ${VPC_ID} --availability-zone ap-northeast-1a --cidr-block 10.0.2.0/24 | jq -r '.Subnet.SubnetId') PRIVATE_SUBNET_C_ID=$(aws ec2 create-subnet --vpc-id ${VPC_ID} --availability-zone ap-northeast-1c --cidr-block 10.0.3.0/24 | jq -r '.Subnet.SubnetId') # パブリックのルートテーブル作成 PUBLIC_ROUTE_TABLE_ID=$(aws ec2 create-route-table --vpc-id ${VPC_ID} | jq -r '.RouteTable.RouteTableId') aws ec2 create-route --route-table-id ${PUBLIC_ROUTE_TABLE_ID} --destination-cidr-block 0.0.0.0/0 --gateway-id ${IGW_ID} PUBLIC_A_ASSOCITATE_ID=$(aws ec2 associate-route-table --subnet-id ${PUBLIC_SUBNET_A_ID} --route-table-id ${PUBLIC_ROUTE_TABLE_ID} | jq -r '.AssociationId') PUBLIC_C_ASSOCITATE_ID=$(aws ec2 associate-route-table --subnet-id ${PUBLIC_SUBNET_C_ID} --route-table-id ${PUBLIC_ROUTE_TABLE_ID} | jq -r '.AssociationId') #Elastic IPを発行 ALLOCATION_ID=$(aws ec2 allocate-address --domain vpc | jq -r '.AllocationId') # NATゲートウェイを作成 NAT_GATEWAY_ID=$(aws ec2 create-nat-gateway --subnet-id ${PUBLIC_SUBNET_A_ID} --allocation-id ${ALLOCATION_ID} | jq -r '.NatGateway.NatGatewayId') # プライベートのルートテーブル作成 PRIVATE_ROUTE_TABLE_ID=$(aws ec2 create-route-table --vpc-id ${VPC_ID} | jq -r '.RouteTable.RouteTableId') aws ec2 create-route --route-table-id ${PRIVATE_ROUTE_TABLE_ID} --destination-cidr-block 0.0.0.0/0 --nat-gateway-id ${NAT_GATEWAY_ID} PRIVATE_A_ASSOCITATE_ID=$(aws ec2 associate-route-table --subnet-id ${PRIVATE_SUBNET_A_ID} --route-table-id ${PRIVATE_ROUTE_TABLE_ID} | jq -r '.AssociationId') PRIVATE_C_ASSOCITATE_ID=$(aws ec2 associate-route-table --subnet-id ${PRIVATE_SUBNET_C_ID} --route-table-id ${PRIVATE_ROUTE_TABLE_ID} | jq -r '.AssociationId')
セキュリティグループの設定
# ロードバランサ、Nginx用のセキュリティグループ作成 WEB_SG_ID=$(aws ec2 create-security-group --group-name helloworld-web-sg --description "web security group" --vpc-id ${VPC_ID} | jq -r '.GroupId') # インバウンド設定で80を許可する aws ec2 authorize-security-group-ingress --group-id ${WEB_SG_ID} --protocol tcp --port 80 --cidr 0.0.0.0/0 # サービス用のセキュリティグループ作成 APP_SG_ID=$(aws ec2 create-security-group --group-name helloworld-app-sg --description "application security group" --vpc-id ${VPC_ID} | jq -r '.GroupId') # インバウンド設定で8080を許可する aws ec2 authorize-security-group-ingress --group-id ${APP_SG_ID} --protocol tcp --port 8080 --cidr 0.0.0.0/0
ロードバランサの設定
# ターゲットグループの作成 # ロードバランサがリクエストを送るターゲットを登録 TARGET_GROUP_ARN=$(aws elbv2 create-target-group --name helloworld-nginx --protocol HTTP --port 80 --vpc-id ${VPC_ID} --target-type ip | jq -r '.TargetGroups[0].TargetGroupArn') # ロードバランサ作成 LB_ARN=$(aws elbv2 create-load-balancer --name alb-helloworld --subnets ${PUBLIC_SUBNET_A_ID} ${PUBLIC_SUBNET_C_ID} --security-groups ${WEB_SG_ID} | jq -r '.LoadBalancers[0].LoadBalancerArn') # リスナー登録 aws elbv2 create-listener --load-balancer-arn ${LB_ARN} --protocol HTTP --port 80 --default-actions Type=forward,TargetGroupArn=${TARGET_GROUP_ARN}
ECSの設定
# タスク定義作成 TASK_HELLO_REVISION=$(aws ecs register-task-definition --cli-input-json file://./task-hello.json | jq -r '.taskDefinition.revision') TASK_WORLD_REVISION=$(aws ecs register-task-definition --cli-input-json file://./task-world.json | jq -r '.taskDefinition.revision') TASK_NGINX_REVISION=$(aws ecs register-task-definition --cli-input-json file://./task-nginx.json | jq -r '.taskDefinition.revision') # クラスタ作成 CLUSTER_NAME=fargate-helloworld aws ecs create-cluster --cluster-name ${CLUSTER_NAME} # サービス検出ネームスペース作成 OPERATION_ID=$(aws servicediscovery create-private-dns-namespace --name helloworld --vpc ${VPC_ID} | jq -r '.OperationId') # 作成したネームスペースのIDを確認 NAMESPACE_ID=$(aws servicediscovery get-operation --operation-id ${OPERATION_ID} | jq -r '.Operation.Targets.NAMESPACE') ネームスペース作成に時間がかかるので、1分ほど待つ。 # Helloサービス名登録 HELLO_SERVICE_ID=$(aws servicediscovery create-service --name hello --dns-config "NamespaceId=${NAMESPACE_ID},RoutingPolicy=MULTIVALUE,DnsRecords=[{Type=A,TTL=60}]" | jq -r '.Service.Id') HELLO_REGISTRY_ARN=$(aws servicediscovery get-service --id ${HELLO_SERVICE_ID} | jq -r '.Service.Arn') # Worldサービス名登録 WORLD_SERVICE_ID=$(aws servicediscovery create-service --name world --dns-config "NamespaceId=${NAMESPACE_ID},RoutingPolicy=MULTIVALUE,DnsRecords=[{Type=A,TTL=60}]" | jq -r '.Service.Id') WORLD_REGISTRY_ARN=$(aws servicediscovery get-service --id ${WORLD_SERVICE_ID} | jq -r '.Service.Arn') # Nginxサービス名登録 NGINX_SERVICE_ID=$(aws servicediscovery create-service --name nginx --dns-config "NamespaceId=${NAMESPACE_ID},RoutingPolicy=MULTIVALUE,DnsRecords=[{Type=A,TTL=60}]" | jq -r '.Service.Id') NGINX_REGISTRY_ARN=$(aws servicediscovery get-service --id ${NGINX_SERVICE_ID} | jq -r '.Service.Arn') # Helloサービス作成 aws ecs create-service --cluster ${CLUSTER_NAME} --service-name hello-service --task-definition helloworld-hello:${TASK_HELLO_REVISION} --desired-count 1 --launch-type "FARGATE" --network-configuration "awsvpcConfiguration={subnets=["${PRIVATE_SUBNET_A_ID}","${PRIVATE_SUBNET_C_ID}"],securityGroups=["${APP_SG_ID}"],assignPublicIp=DISABLED}" --service-registries "registryArn=${HELLO_REGISTRY_ARN},containerName=helloworld-hello" # Worldサービス作成 aws ecs create-service --cluster ${CLUSTER_NAME} --service-name world-service --task-definition helloworld-world:${TASK_WORLD_REVISION} --desired-count 1 --launch-type "FARGATE" --network-configuration "awsvpcConfiguration={subnets=["${PRIVATE_SUBNET_A_ID}","${PRIVATE_SUBNET_C_ID}"],securityGroups=["${APP_SG_ID}"],assignPublicIp=DISABLED}" --service-registries "registryArn=${WORLD_REGISTRY_ARN},containerName=helloworld-world" # Nginxサービス作成 aws ecs create-service --cluster ${CLUSTER_NAME} --service-name nginx-service --task-definition helloworld-nginx:${TASK_NGINX_REVISION} --desired-count 1 --launch-type "FARGATE" --network-configuration "awsvpcConfiguration={subnets=["${PRIVATE_SUBNET_A_ID}","${PRIVATE_SUBNET_C_ID}"],securityGroups=["${WEB_SG_ID}"],assignPublicIp=DISABLED}" --service-registries "registryArn=${NGINX_REGISTRY_ARN},containerName=helloworld-nginx" --load-balancers "targetGroupArn=${TARGET_GROUP_ARN},containerName=helloworld-nginx,containerPort=80"
動作確認
以下のコマンドを実行する。
open http://$(aws elbv2 describe-load-balancers --load-balancer-arns ${LB_ARN} | jq -r '.LoadBalancers[0].DNSName')/hello/webresources/hello
表示された画面に「Hello World!」が表示されるか確認してください。 環境構築後、ヘルスチェックが通すまでに時間がかかるので、時間をおいてから確認してください。
環境削除
最後に構築した環境をお片づけしましょう。
# ECSサービスの起動タスク数を0に更新 aws ecs update-service --cluster fargate-helloworld --service nginx-service --desired-count 0 aws ecs update-service --cluster fargate-helloworld --service hello-service --desired-count 0 aws ecs update-service --cluster fargate-helloworld --service world-service --desired-count 0 # ECSサービスを削除 aws ecs delete-service --cluster fargate-helloworld --service nginx-service aws ecs delete-service --cluster fargate-helloworld --service hello-service aws ecs delete-service --cluster fargate-helloworld --service world-service # ECSクラスタを削除 aws ecs delete-cluster --cluster fargate-helloworld # サービスディスカバリのサービス名を削除 aws servicediscovery delete-service --id ${HELLO_SERVICE_ID} aws servicediscovery delete-service --id ${WORLD_SERVICE_ID} aws servicediscovery delete-service --id ${NGINX_SERVICE_ID} # サービスディスカバリのネームスペースを削除 aws servicediscovery delete-namespace --id ${NAMESPACE_ID} # ロードバランサを削除 aws elbv2 delete-load-balancer --load-balancer-arn ${LB_ARN} # ターゲットグループを削除 aws elbv2 delete-target-group --target-group-arn ${TARGET_GROUP_ARN} # NATゲートウェイを削除 aws ec2 delete-nat-gateway --nat-gateway-id ${NAT_GATEWAY_ID} # ルートテーブルを削除 aws ec2 delete-route --route-table-id ${PRIVATE_ROUTE_TABLE_ID} --destination-cidr-block 0.0.0.0/0 aws ec2 disassociate-route-table --association-id ${PRIVATE_A_ASSOCITATE_ID} aws ec2 disassociate-route-table --association-id ${PRIVATE_C_ASSOCITATE_ID} aws ec2 delete-route-table --route-table-id ${PRIVATE_ROUTE_TABLE_ID} aws ec2 delete-route --route-table-id ${PUBLIC_ROUTE_TABLE_ID} --destination-cidr-block 0.0.0.0/0 aws ec2 disassociate-route-table --association-id ${PUBLIC_A_ASSOCITATE_ID} aws ec2 disassociate-route-table --association-id ${PUBLIC_C_ASSOCITATE_ID} aws ec2 delete-route-table --route-table-id ${PUBLIC_ROUTE_TABLE_ID} # インターネットゲートウェイをデタッチ aws ec2 detach-internet-gateway --internet-gateway-id ${IGW_ID} --vpc-id ${VPC_ID} # Elastic IPを解放 aws ec2 release-address --allocation-id ${ALLOCATION_ID} # サブネットを削除 aws ec2 delete-subnet --subnet-id ${PUBLIC_SUBNET_A_ID} aws ec2 delete-subnet --subnet-id ${PUBLIC_SUBNET_C_ID} aws ec2 delete-subnet --subnet-id ${PRIVATE_SUBNET_A_ID} aws ec2 delete-subnet --subnet-id ${PRIVATE_SUBNET_C_ID} # セキュリティグループを削除 aws ec2 delete-security-group --group-id ${WEB_SG_ID} aws ec2 delete-security-group --group-id ${APP_SG_ID} # VPCを削除 aws ec2 delete-vpc --vpc-id ${VPC_ID}
おわりに
AWS CLIのみでFargateを使った環境構築を行いました。 今回はオートスケーリングなどの設定を行いませんでしたが、負荷に応じてタスクごとにスケールが可能なので、 コンピューティングリソースを有効に使いつつ、柔軟に対応ができそうです。 サービスごとにアプリケーションの更新が可能なので、他のサービスに影響を与えずにデプロイが可能になります。
もし、マイクロサービスに手をつけたいけど、インフラ環境をどうすればいいか悩んでいる方は、試してみてください。
参考
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/create-service-discovery.html