どうも、小野です。
今回はAWS Fargateを使って、下図のような構成で作っていきます。
AWS Fargate
AWS Fargateは元々あるECSからEC2の管理を除外したサービスになります。
つまり、ECSをよりマネージドにしたものです。
Fargateについて知りたい方は公式サイトを確認してください。
aws.amazon.com
説明するにあたりAWSコンソールでもよいのですが、UIが頻繁に変わったりするので、今回はCLIベースで説明していきます。
※動作確認を行うターミナルは、コマンド実行時に変数を利用している関係上、最後まで閉じないようにしてください。
動作環境
前提
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/ {
proxy_pass http://hello.helloworld:8080/hello/;
}
location ^~ /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() {
String host = System.getenv("WORLD_HOST" );
String url = "http://" + host + ":8080/world/webresources" ;
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_ID =$( aws ec2 create-vpc --cidr-block 10.0.0.0/ 16 | jq -r ' .Vpc.VpcId ' )
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 ' )
aws ec2 attach-internet-gateway --internet-gateway-id ${IGW_ID} --vpc-id ${VPC_ID}
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_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 ' )
セキュリティグループの設定
WEB_SG_ID =$( aws ec2 create-security-group --group-name helloworld-web-sg --description " web security group " --vpc-id ${VPC_ID} | jq -r ' .GroupId ' )
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 ' )
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 ' )
NAMESPACE_ID =$( aws servicediscovery get-operation --operation-id ${OPERATION_ID} | jq -r ' .Operation.Targets.NAMESPACE ' )
ネームスペース作成に時間がかかるので、1 分ほど待つ。
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_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_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 ' )
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 "
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 "
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!」が表示されるか確認してください。
環境構築後、ヘルスチェックが通すまでに時間がかかるので、時間をおいてから確認してください。
環境削除
最後に構築した環境をお片づけしましょう。
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
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
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}
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}
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}
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