サービスディスカバリハンズオン!手を動かしてCloud Mapを理解する!!

2020.03.31

こんにちは(U・ω・U)
AWS事業部の深澤です。

皆さん、サービスディスカバリはご存知でしょうか。こちらはサーバ側で提供しているサービスを利用する時、そのサーバのドメインなりIPアドレスなりを知り参照をする必要があるのですがこの辺りの参照と解決をどう実現するかがサービスディスカバリです。最近ではマイクロサービスを構築するにあたり特にこの辺りが課題になってきています。特に内部の通信ですね。少し前だとインターナルのロードバランサを配置されたりして実現されていたかと思いますが、最近ではAWS Cloud Map(以降、Cloud Map)と呼ばれるよりクラウドネイティブなサービスディスカバリを実現するサービスがあります。

マイクロサービスを構築するにあたり必要なコンテナやlambda、DynamoDB、Kinesisといったサービスをお互いに連携する仕組みを提供してくれるサービスです。本ブログでは手を動かすことに重きをおいておりますのであまりCloud Mapの概要については扱いません。ハンズオンを通してざっくりとした理解をするのが目的です。もっと詳しく概要が知りたいという方は弊社濱田の記事が大変参考になります。是非ご覧ください。

さて今回は分かりそうで分かりにくいこのCloud Mapを実際に手を動かしながら学んでいこうと思います。

本ブログの対象

  • Cloud Mapという名前は知っているけど実際に使ったことがない
  • サービスディスカバリがよく分からない
  • AWS CLIは使える

ECSを今回使いますが、ECSに対する深い知見がなくても全然大丈夫ですよ!ざっくり何をするものかくらいはご存知ですと理解も早いかと。

ハンズオンで何作るの?

最終的には次のような構成図のものを作りたいと思います。最終的にはEC2からCloud Mapを通して冗長化されたコンテナに接続テストしてみます。

どうやって作るの?

AWS CLIを使いましょう。検証したバージョンは以下の通りです。

$ aws --version
aws-cli/1.16.303 Python/3.7.4 Darwin/19.2.0 botocore/1.13.39

今回扱うところと扱わないもの

以下の環境は一般的なVPC環境かと存じますので今回は取り扱いません。こちらを前提にスタートします。

NACL等は一切設定しません。Public SubnetはInternetGateway、Private SubnetはNatGateway経由で外部と通信します。NatGatewayは料金の関係から1つだけの作成としました。Subnetは異なるAZ毎に1つずつ用意しましたが必須ではありません。

一点VPCを作る上でご注意いただきたいのですが、Cloud MapではDNSのホスト名解決を行いますのでVPC内でのDNSホスト名解決のサポートをオンにして設計ください。詳しくは以下の「VPC の DNS サポート」をご参照いただければと思います。

なのでハンズオンのお品書き的には次のようになります。

  • コンテナ用のセキュリティグループを作成
  • Cloud Mapリソースを作成
    • プライベートなDNS名前空間の作成
    • 作成した名前空間内のサービス作成
  • ECS クラスターを作成
  • タスク定義作成
  • サービス定義作成
  • サービスディスカバリでいろいろ遊んでみる

ハンズオンスタート!!

コンテナ用のセキュリティグループを作成

まずは次のようにセキュリティグループを作りましょう。[vpc-id]にはご自身の環境のvpc-idを入れてください。

aws ec2 create-security-group \
    --group-name cloudmap-handson-sg-container \
    --description "The security group for containers used in cloud map hands-on." \
    --vpc-id [vpc-id]

{
    "GroupId": "sg-000000000aaaaaaaa"
}

GroupIdが返ってきましたね。このセキュリティグループにインバウンドのルールを付与します。このコンテナはhttpリクエストを受け付けたいのでhttpのポートをオープンします。範囲はVPCのCIDRからのみ受け付けるようにします。CIDRの値はご自身の環境に応じてご調整ください。次のコマンドを実行します。

aws ec2 authorize-security-group-ingress \
    --group-id sg-000000000aaaaaaaa \
        --protocol tcp \
    --port 80 \
    --cidr 10.0.0.0/16

ここで使ったGroupIdは後ほどECSのServiceを定義する際に使います。どこかにメモしておいて下さい。

Cloud Mapリソースを作成

続いてCloud Mapのリソースを作成していきます。まずはプライベートなDNS名前空間の作成です。以下のコマンドを実行します。[vpc-id]にはご自身の環境のvpc-idを入れてください。

aws servicediscovery create-private-dns-namespace \
    --name httpd-service-discovery.internal \
    --vpc [vpc-id]

{
    "OperationId": "h2qe3s6dxftvvt7riu6lfy2f6c3jlhf4-je6chs2e"
}

この返ってきたOperationIdを使って、名前空間が正しく作成されたことを確認しましょう!次のコマンドを実行します。

aws servicediscovery get-operation --operation-id h2qe3s6dxftvvt7riu6lfy2f6c3jlhf4-je6chs2e
{
    "Operation": {
        "Id": "h2qe3s6dxftvvt7riu6lfy2f6c3jlhf4-je6chs2e",
        "Type": "CREATE_NAMESPACE",
        "Status": "SUCCESS",
        "CreateDate": 1519777852.502,
        "UpdateDate": 1519777856.086,
        "Targets": {
            "NAMESPACE": "ns-uejictsjen2i4eeg"
        }
    }
}

StatusがSUCCESSになっていますね。コンソール画面でもCloud Mapの名前空間が作成できたことを確認できます。
ちなみにこの時、Route 53側でもホストゾーンが作成されています。

では次にget-operationで取得したNAMESPACEの値を使ってCloud Map名前空間内のサービスを作りましょう。以下のコマンドを実行して下さい。

aws servicediscovery create-service \
    --name cloudmap-handson \
    --dns-config 'NamespaceId="ns-uejictsjen2i4eeg",DnsRecords=[{Type="A",TTL="300"}]' \
    --health-check-custom-config FailureThreshold=1

コマンドが成功するとこんな感じでレスポンスが返ってきます。

{
    "Service": {
        "Id": "srv-utcrh6wavdkggqtk",
        "Arn": "arn:aws:servicediscovery:ap-northeast-1:12345678910:service/srv-utcrh6wavdkggqtk",
        "Name": "cloudmap-handson",
        "DnsConfig": {
            "NamespaceId": "ns-uejictsjen2i4eeg",
            "DnsRecords": [
                {
                    "Type": "A",
                    "TTL": 300
                }
            ]
        },
        "HealthCheckCustomConfig": {
            "FailureThreshold": 1
        },
        "CreatorRequestId": "e49a8797-b735-481b-a657-b74d1d6734eb"
    }
}

ここで返ってきたArnは後ほどECSのサービス定義で使用しますので控えておいて下さい。またNamespaceIdも後ほど検証で使いますので控えておいて下さい。こちらが実際にサービスディスカバリの役割を担います。さて続いてECS側をさくっと用意しちゃいましょう!

ECS クラスターを作成

次のコマンドを実行して下さい。

aws ecs create-cluster --cluster-name cloudmap-handson-cluster

次のようなレスポンスがくればOKです。

{
    "cluster": {
        "clusterArn": "arn:aws:ecs:ap-northeast-1:12345678910:cluster/cloudmap-handson-cluster",
        "clusterName": "cloudmap-handson-cluster",
        "status": "ACTIVE",
        "registeredContainerInstancesCount": 0,
        "runningTasksCount": 0,
        "pendingTasksCount": 0,
        "activeServicesCount": 0,
        "statistics": [],
        "tags": [],
        "settings": [
            {
                "name": "containerInsights",
                "value": "enabled"
            }
        ],
        "capacityProviders": [],
        "defaultCapacityProviderStrategy": []
    }
}

タスク定義作成

ECSタスクの定義をjsonで作成します。以下のようなjsonを作成しtask-definition.jsonというファイル名で保存して下さい。なおコンテナですが公式httpdコンテナ(alpine)を採用します。

{
    "family": "httpd-task",
    "networkMode": "awsvpc",
    "containerDefinitions": [
        {
            "name": "httpd-container",
            "image": "httpd:2.4.41-alpine",
        "portMappings":[{
                "hostPort": 80,
                "protocol": "tcp",
                "containerPort": 80
        }]
        }
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512"
}

保存したら次のコマンドを実行しECSタスクを作成しましょう。

aws ecs register-task-definition --cli-input-json file://task-definition.json

上記のjsonはタスク定義で定義可能な部分をかなり省略していて、レスポンスでは実際に作成されたタスクの省略された部分を含むjsonが返ってきます。非常に長いので全てをここに掲載するのは避けますが、familyとrevisionを確認しておいて下さい。次のECSサービス定義作成で使います。

{
    "taskDefinition": {
        〜〜〜
        "family": "httpd-task",
        "revision": 1,
        〜〜〜
    }
}

サービス定義作成

こちらも先にjsonから作成します。サービス定義のjsonを次のようにして下さい。taskDefinitionは今さっき作成したECSタスクのfamilyとrevisionの組み合わせです。serviceRegistriesのregistryArnにはサービスディスカバリ作成時のArnを入れましょう。[subnet-id]はご自身の環境のPrivate SubnetのIDを入れて下さい。securityGroupsにはハンズオンの序盤で作成したセキュリティグループのIDが入ります。ファイルはservice-definition.jsonとしましょう。なお、データプレーンにはFARGATEを使います。

{
    "cluster": "cloudmap-handson-cluster",
    "serviceName": "cloudmap-handson-httpd-service",
    "taskDefinition": "httpd-task:1",
    "serviceRegistries": [
        {
            "registryArn": "arn:aws:servicediscovery:ap-northeast-1:12345678910:service/srv-utcrh6wavdkggqtk"
        }
    ],
    "desiredCount": 1,
    "launchType": "FARGATE",
    "networkConfiguration": {
        "awsvpcConfiguration": {
            "subnets": [
                "[subnet-id]",
                "[subnet-id]"
            ],
            "securityGroups": [
                "sg-000000000aaaaaaaa"
            ],
            "assignPublicIp": "DISABLED"
        }
    }
}

保存したら次のコマンドを実行しECSサービスを作成しましょう。

aws ecs create-service --cli-input-json file://service-definition.json

ここもレスポンスはECSタスク定義時と同様の理由で非常に長いため省略します。

さて長いことお疲れ様でした!これでサービスディスカバリで遊ぶ準備は完了です!

サービスディスカバリでいろいろ遊んでみる

まずは以下のコマンドを実行してサービスディスカバリに登録されているインスタンス情報をみてみましょう。

aws servicediscovery list-instances --service-id srv-utcrh6wavdkggqtk

次のようなレスポンスが返ってきます。

{
    "Instances": [
        {
            "Id": "b0a34b2b-ce8b-4dc8-b2c2-4fd765b56b5b",
            "Attributes": {
                "AVAILABILITY_ZONE": "ap-northeast-1a",
                "AWS_INIT_HEALTH_STATUS": "HEALTHY",
                "AWS_INSTANCE_IPV4": "10.0.8.203",
                "ECS_CLUSTER_NAME": "cloudmap-handson-cluster",
                "ECS_SERVICE_NAME": "cloudmap-handson-httpd-service",
                "ECS_TASK_DEFINITION_FAMILY": "httpd-task",
                "REGION": "ap-northeast-1"
            }
        }
    ]
}

先ほど作成したコンテナの情報が返ってきました!IPアドレスも付いているので、EC2インスタンスにログインしcurlを実行してみましょう。

curl http://10.0.8.203/
<html><body><h1>It works!</h1></body></html>

It works!が返ってくれば成功です。このように接続先のサービス(今回はECSコンテナ)を検出し接続情報をくれるのがサービスディスカバリです。しかし、接続のためには都度対象のIPアドレスを見つけないといけないのでしょうか。実は名前解決も可能です。先ほどCloud Mapの名前空間を作成した時点でRoute 53を確認したのを覚えていますか。その時、Route 53にCloud Mapの名前空間名でホストゾーンが作成されていました。ここにコンテナへのDNSレコードがあります。この名前空間内のサービスを作成した際のNamespaceIdを覚えてますか??こちらを用いて次のようなコマンドを実行してみましょう。

aws servicediscovery get-namespace --id ns-uejictsjen2i4eeg

成功するとCloud Mapの名前空間情報が返ってきます。

{
    "Namespace": {
       〜〜〜
        "Properties": {
            "DnsProperties": {  
                "HostedZoneId": "AAAAAAAAAAAAAAAA11111"
            },
            "HttpProperties": {
                "HttpName": "cloudmap-handson.internal"
            }
        }
        〜〜〜
    }
}

ここにRoute 53側で作成されたHostedZoneIdが含まれています。これを用いて次のようなコマンドを実行し設定してあるレコードをみてみます。

aws route53 list-resource-record-sets --hosted-zone-id AAAAAAAAAAAAAAAA11111
{
    "ResourceRecordSets": [
        〜〜〜
        {
            "Name": "cloudmap-handson.cloudmap-handson.internal.",
            "Type": "A",
            〜〜〜
            "TTL": 300,
            "ResourceRecords": [
                {
                    "Value": "10.0.8.203"
                }
            ],
            〜〜〜
        }
    ]
}

何やらAレコードがありますね!早速EC2上で叩いてみましょう。

curl cloudmap-handson.cloudmap-handson.internal
<html><body><h1>It works!</h1></body></html>

先ほどIPアドレスを叩いた結果と同じ結果が返ってきました!!さてここで疑問が浮かびますね。「Aレコードってことはコンテナの台数増やしたらどうなるの?」ということを考えられた方も多いかと思います。早速やってみましょう。先ほどのECSサービスに設定してあるdesired-countを1から4に増やしてみます。

aws ecs update-service \
        --cluster cloudmap-handson-cluster \
        --service cloudmap-handson-httpd-service \
        --desired-count 4

これでもう一度先ほどのレコードセットをみてみましょう。

aws route53 list-resource-record-sets --hosted-zone-id AAAAAAAAAAAAAAAA11111

{
    "ResourceRecordSets": [
        {
            "Name": "cloudmap-handson.cloudmap-handson.internal.",
            "Type": "A",
            〜〜〜
             "ResourceRecords": [
                {
                    "Value": "10.0.8.88"
                }
            ],
        },
        {
            "Name": "cloudmap-handson.cloudmap-handson.internal.",
            "Type": "A",
            〜〜〜
             "ResourceRecords": [
                {
                    "Value": "10.0.9.53"
                }
            ],
        },
        {
            "Name": "cloudmap-handson.cloudmap-handson.internal.",
            "Type": "A",
            〜〜〜
             "ResourceRecords": [
                {
                    "Value": "10.0.8.203"
                }
            ],
        },
        {
            "Name": "cloudmap-handson.cloudmap-handson.internal.",
            "Type": "A",
            〜〜〜
             "ResourceRecords": [
                {
                    "Value": "10.0.9.82"
                }
            ],
        },
    ]
}

Aレコードが増えている!!ということでコンテナの台数に応じてレコードの数もスケールしていくので良しなに負荷分散が行えます。

リソースの削除

お疲れ様でした!最後に今回作成したリソースを削除して今回のハンズオンは終了です。

ECSサービスのコンテナ数を0にする

aws ecs update-service \
        --cluster cloudmap-handson-cluster \
        --service cloudmap-handson-httpd-service \
        --desired-count 0

ECSサービスを削除する

aws ecs delete-service \
        --cluster cloudmap-handson-cluster \
        --service cloudmap-handson-httpd-service

ECSタスクの登録解除

aws ecs deregister-task-definition --task-definition httpd-task:1

ECSクラスターの削除

aws ecs delete-cluster --cluster cloudmap-handson-cluster

サービス検出のサービスを削除

aws servicediscovery delete-service --id srv-utcrh6wavdkggqtk

名前空間を削除

aws servicediscovery delete-namespace --id ns-uejictsjen2i4eeg

セキュリティグループの削除

aws ec2 delete-security-group --group-id sg-000000000aaaaaaaa

※Route 53はCloudMap側の削除が完了すると一緒になくなりますのでご安心ください。

最後に

本ハンズオン、お楽しみいただけましたでしょうか!これまで内部通信はインターナルロードバランサが常識でしたがこちらのCloud Mapを用いることでサービス間の通信がよりしやすくなるのではと思います。今回のハンズオンをきっかけにサービスディスカバリへの理解を深めていただけたら幸いです。

以上、深澤(@shun_quartet)でした!