[アップデート] AWS App Meshが「仮想ゲートウェイ」を使ったメッシュ外部からのIngressアクセスをサポートしました

「あれ?今まで無かったの?」という気がしなくもないですが、App Meshに待望の(?)アップデートが来ました!
2020.07.15

みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。

AWSが提供するメッシュサービス AWS App Mesh において、新しい機能「仮想ゲートウェイ」(Virtual Gateway) がリリースされました。

AWS App Mesh launches ingress support with virtual gateways

これは「App Meshの使い方が大きく広がるんじゃないか?」と思える、結構ビッグなアップデートではないかと思います。

AWSブログでハンズオンも公開されていますので、こちらに沿って試してみたいと思います。

Introducing Ingress support in AWS App Mesh | Containers

ハンズオンで作成するApp Meshの構成

以下の2通りのハンズオンが用意されていますが、

  • 「App Mesh」と「ECS」の組み合わせ
  • 「App Mesh」と「EKS」の組み合わせ

今回は「ECS」を使ったハンズオンを試してみました。

ハンズオンを最初から最後まで通すと、以下のようなApp Meshの構成が作成されます。
(VPCなどのインフラリソースについては省略しています)

構成要素を右側から順に説明していきます。

ECSサービス (メッシュ化の対象となるコンテナアプリケーション)

「ColorTeller」と名付けられた4種類のシンプルなコンテナアプリケーションです。
(「4種類」と言いつつ図では「5個」ありますが、最後の「ColorGatewayService」については後ほど説明します)

HTTPプロトコル (ポートは「9080」) で待ち受けを行い、リクエストを受けるとサービスの名前に応じた「white」「blue」「black」「red」という文字列をそれぞれ返します。

なお、4種類の振る舞いをする別々のアプリケーション (ECSタスク定義) として定義されていますが、実体はGo言語で書かれた単一のコンテナイメージです。
(環境変数で与えられたパラメーターによって応答する文字列が決定されます)

アプリケーション自体は単一のシンプルなコンテナで動作しますが、App Meshに対応させるために Envoy コンテナを「サイドカー」としてアタッチする必要があります。
(ECSタスク定義に「app」「envoy」という2つのコンテナが含まれています)

Envoyコンテナを定義する際に、環境変数APPMESH_VIRTUAL_NODE_NAMEに仮想ノードを判別する以下の値を設定する必要があります。
mesh/<メッシュ名>/virtualNode/<仮想ノード名>

今回のハンズオンでは、設定する値は以下のようになります。(「ColorTellerWhiteService」の場合の例)
mesh/ColorApp-Ingress/virtualNode/colorteller-white-vn

App Meshのメッシュおよび各リソース

App Meshのメッシュ「ColorApp-Ingress」を定義して、メッシュ上に各リソースを作成します。

「仮想ノード」(Virtual Node)

4つのECSサービスと1対1で紐付けられている、4つの仮想ノードが定義されています。

仮想ノードのリスナーは、プロトコルが「http」、ポートが「9080」に設定されています。
また、「TLS」の設定もありますが、今回はTLSを使用しません。(理由は後述します)

仮想ノードに紐付けるECSサービスの検出は、colorteller-white.default.svc.cluster.localといったDNS名によって行われます。
そのため、ECSサービスの作成時に「サービスの検出」(サービスディスカバリ) を設定する必要があります。

ECSサービスに対するヘルスチェックをパス「/ping」に対して行うよう設定されています。
(アプリケーションコンテナ側では「/ping」への要求に対して「HTTP 200」で応答するように実装されています)

「仮想ルーター」(Virtual Router) および「ルート」(Route)

2つの仮想ルーターが定義されています。
それぞれの仮想ルーターのルートは、以下のように定義されています。

  • colorteller-vr-1
    • 仮想ノード「colorteller-white-vn」および「colorteller-blue-vn」に対して重み「1:1」で振り分ける
  • colorteller-vr-2
    • 仮想ノード「colorteller-black-vn」および「colorteller-red-vn」に対して重み「1:1」で振り分ける

「仮想サービス」(Virtual Service)

2つの仮想サービスが定義されています。
それぞれの仮想サービスは、上で定義した仮想ノードがプロバイダに設定されています。

「仮想ゲートウェイ」(Virtual Gateway) および「ゲートウェイルート」(Gateway Route)

今回のアップデートで新たに追加されたリソースです。
メッシュの外部にあるAWSリソース (例えばELB) がメッシュの内部にあるリソースと通信する際の、文字通り「ゲートウェイ」の役割をします。

仮想ゲートウェイの設定には、以下の要素があります。

リスナー

仮想ゲートウェイに対する通信は「リスナー」として定義されます。
プロトコルは「HTTP」「HTTP/2」「gRPC」から選択可能で、今回は「HTTP」を使用します。
ポートは今回「9080」を使用します。
その他は「ヘルスチェック」「TLS」の設定がありますが、今回はいずれも使用しません。

ゲートウェイルート

仮想ゲートウェイからメッシュ内部のリソースに対する通信は「ゲートウェイルート」として定義します。
ルーティングのルールは、HTTPおよびHTTP/2の場合は「パスベース」となり、gRPCの場合は「サービス名」の指定になります。

今回は「HTTP」プロトコルを使用するため、パスベースのルーティングを行います。

  • パスのプレフィクスが/color1と一致する場合:
    • 仮想サービス「colorteller-1.default.svc.cluster.local」へルーティング
  • パスのプレフィクスが/color2と一致する場合:
    • 仮想サービス「colorteller-2.default.svc.cluster.local」へルーティング

これらの設定に加えて、仮想ゲートウェイが動作するための「実体」を用意する必要があります。

仮想ゲートウェイの「実体」= Envoyコンテナ

仮想ゲートウェイの「実体」は、以下のいずれかによって実行される「Envoy」です。

  • ECSの「サービス」で実行されるEnvoyコンテナ
  • EKS/Kubernetesの「サービス」で実行されるEnvoyコンテナ
  • EC2インスタンスで実行されるEnovy

今回は「ECSサービス」のEnvoyコンテナを使用します。

仮想ノードに紐付く「ECSサービス」ではEnvoyはサイドカーとしてアプリケーションコンテナに付帯させる位置付けでしたが、仮想ゲートウェイではEnvoyコンテナが本体となることに注意してください。

今回のハンズオンでは、Envoyコンテナに加えて、ロギングのための「CloudWatch Agent」コンテナが追加されています。

仮想ノードの時と同様に、Envoyコンテナを定義する際に、環境変数APPMESH_VIRTUAL_NODE_NAMEに仮想ゲートウェイを判別する以下の値を設定する必要があります。
mesh/<メッシュ名>/virtualGateway/<仮想ゲートウェイ名>

今回のハンズオンでは、設定する値は以下のようになります。
mesh/ColorApp-Ingress/virtualGateway/colorgateway-vg

ハンズオンに沿って試してみた

それでは、ここからは実際にハンズオンを試していきます。

準備

AWS CLI

App Meshのアップデートに対応したバージョンへアップデートする必要があります。

  • AWS CLI v2系: 2.0.30 以上
  • AWS CLI v1系: 1.18.97 以上

今回は、以下のバージョンで試しました。

$ aws --version
aws-cli/2.0.30 Python/3.7.3 Linux/4.4.0-18362-Microsoft botocore/2.0.0dev34

EC2キーペア

ハンズオンで環境構築を行うCloudFormationがEC2キーペアを参照しますので、予め用意しておいてください。

なお、今回のハンズオンではECSクラスターをFargateベースで作成しますので、EC2ホストインスタンスはありません。
ただし、ECSとは別に踏み台 (Bastion) としてEC2インスタンスを1台作成しますので、そちらでキーペアが必要になります。

Docker実行環境

ハンズオンの流れでは、使用するアプリケーションコンテナをソースコードとDockerfileからビルドして用意するようになっています。
ローカルPCでDockerが利用できる状態にしておきます。

もしローカルPCでDockerが使用できない場合は「Cloud9を利用する」「EC2インスタンスを起動してDockerをインストールする」などの代替手段でもOKです。

GitHubのサンプルリポジトリをローカルPCへコピー

今回のハンズオンは、GitHubでAWSが公開しているaws/aws-app-mesh-examplesリポジトリに含まれるコンテンツの一部となっています。

以下のコマンドでGitHubリポジトリをローカルPCへコピーします。

$ git clone https://github.com/aws/aws-app-mesh-examples.git

コピーしましたら、今回のハンズオンで使うディレクトリへ遷移します。

$ cd aws-app-mesh-examples/walkthroughs/howto-ingress-gateway

環境変数の設定

以降の作業で参照する環境変数を設定しておきます。
(設定の間違いや漏れがあるとスクリプト実行時にエラーとなりますので、しっかり確認しましょう)

AWSアカウントID、使用するリージョン、および (さきほど用意した) キーペア名:

$ export AWS_ACCOUNT_ID=123456789012
$ export AWS_DEFAULT_REGION=ap-northeast-1
$ export KEY_PAIR_NAME=<キーペア名>

AWSアカウントIDは以下のコマンドラインで取得しても構いません。

$ export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --output text --query 'Account')

今回のハンズオンで使う各種の名前:

$ export ENVIRONMENT_NAME=AppMeshIngressExample
$ export MESH_NAME=ColorApp-Ingress
$ export SERVICES_DOMAIN="default.svc.cluster.local"
$ export COLOR_TELLER_IMAGE_NAME="howto-ingress/colorteller"

「Envoyコンテナ」のイメージパス:

$ export ENVOY_IMAGE=840364872350.dkr.ecr.ap-northeast-1.amazonaws.com/aws-appmesh-envoy:v1.12.4.0-prod

840364872350の部分は、自分のAWSアカウントIDではなく、上記の通りに入力してください。(AWSが用意しているアカウントです)

イメージパスには「リージョン」および「バージョンタグ」が含まれます。
(上記は「東京リージョン」かつ「2020/07/14現在の最新バージョン」の場合のイメージパスとなります)

使用するリージョンに応じたECRリポジトリ、最新のバージョンタグについては、下記のAWSドキュメントを参照してください。
Envoy image - AWS App Mesh

ハンズオンのインフラ環境を作成

VPC環境を作成する

ECSクラスターを実行するVPC環境を作成します。

以下のようにスクリプトを実行します。

$ ./infrastructure/vpc.sh

スクリプトを実行すると、CloudFormationのスタックが作成され、各種リソースが作成されていきます。

スクリプトの最後にSuccessfully created/updated stack - AppMeshIngressExample-vpcと出力されれば、作成は成功です。
これで、以下のリソースが作成されました。

  • VPC
  • インターネットゲートウェイ
  • サブネット: パブリック × 2、プライベート × 2
  • NATゲートウェイ × 2
  • Elastic IP × 2 (NATゲートウェイ用)
  • ルートテーブル

ECSクラスター、ECRリポジトリを作成する

以下のようにスクリプトを順に実行します。

$ ./infrastructure/ecs-cluster.sh
$ ./infrastructure/ecr-repositories.sh

VPC構築と同様に、スクリプトを実行するとCloudFormationのスタックが作成されます。

それぞれ、以下のように出力されれば、作成は成功です。
Successfully created/updated stack - AppMeshIngressExample-ecs-cluster
Successfully created/updated stack - AppMeshIngressExample-ecr-repositories

アプリケーションコンテナのビルドおよびECRリポジトリへのプッシュ

以下のスクリプトを実行すると、コンテナのビルドからECRリポジトリへのプッシュまで、一連の流れが一気に行われます。

$ GO_PROXY=direct ./src/colorteller/deploy.sh

アプリケーションのビルドにはGo言語が使われていますが、Dockerのマルチステージビルドが採用されているため、ローカルPCにGo言語環境は不要です。

Cloud9やEC2インスタンスを利用する場合は、スクリプトを実行する前に、ECRリポジトリへログインするための設定が行われていることを確認してください。

スクリプトの処理が最後まで正常に行われれば、以下のように出力されるはずです。

+ docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/howto-ingress/colorteller:latest
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/howto-ingress/colorteller]
57890d9c5d52: Pushed
latest: digest: sha256:f567124307291ea2e51aac82cc59b0065d4e109cee4217502734097002308cbc size: 528

エラーとなる場合は、「Dockerビルド」「ECRへのログイン」「ECRリポジトリへのプッシュ」のステップのうち、どのステップで失敗しているのかを確認して、トラブルシュートするのがよいかと思います。

App Meshのリソースを作成

いよいよ今回のメインのApp Meshリソースを作成していきます。

ですが、その前に、ハンズオン資料で用意されている手順を一部変更したいと思います。

ハンズオンでは「TLS」を使用するようになっていますが・・・

サンプルリポジトリに用意されているハンズオンでは、App Meshのメッシュ内通信で「TLS」を使用するように構成されています。

TLSを使用する際に「プライベートTLS証明書」が必要となりますが、ハンズオンの手順では「AWS Certificate Manager プライベート認証機関」(ACM Private CA) を使って作成するように案内されています。

ACM Private CAの利用料金体系は「運用するプライベートCA 1つあたり 400 USD/月」+「発行した証明書の数に応じた料金」となっています。
料金 - AWS Certificate Manager | AWS

最低でも400ドルかかってしまうというのは、ハンズオンをちょっと試してみるのに使うのはツライ・・・
ということで、今回はTLSを使わずにハンズオンを進めることにしました。

※ ACM Private CAを使う方法以外にも「自前で認証局を立ててプライベート証明書を発行する」という方法などがあると思います。 機会があれば、そういった方法を使ったApp MeshのTLS構成を試してみたいと思います。

2020/07/15追記
ACM Private CAの料金体系について、以下のようなご指摘を頂きました。(ありがとうございます)
・初回利用時に30日間の無料トライアルあり
・利用期間が1か月未満の月については、日割りで料金が計算される
ということですので、そのうち改めてプライベート証明書を発行してTLSを試してみようと思います。

仮想ゲートウェイの定義ファイルを書き換える

仮想ゲートウェイの定義ファイルにTLSを使用する記述がありますので、これを書き換えます。

$ vi mesh/colorgateway-vg.json

変更前:

colorgateway-vg.json

{
  "spec": {
    "listeners": [
      {
        "portMapping": {
          "port": 9080,
          "protocol": "http"
        },
        "tls": {
          "mode": "STRICT",
          "certificate": {
            "acm": {
              "certificateArn": $CERTIFICATE_ARN
            }
          }
        }
      }
    ]
  }
}

ハイライト部分 (8~15行目) を削除します。

変更後:

colorgateway-vg.json

{
  "spec": {
    "listeners": [
      {
        "portMapping": {
          "port": 9080,
          "protocol": "http"
        }
      }
    ]
  }
}

もう一つ定義ファイルがありますので、こちらも書き換えます。

$ vi mesh/colorgateway-vg-backendDefaults.json

変更前:

colorgateway-vg-backendDefaults.json

{
  "spec": {
    "listeners": [{
      "portMapping": {
        "port": 9080,
        "protocol": "http"
      },
      "tls": {
        "mode": "STRICT",
        "certificate": {
          "acm": {
            "certificateArn": $CERTIFICATE_ARN
          }
        }
      }
    }],
    "backendDefaults": {
      "clientPolicy": {
        "tls": {
          "validation": {
            "trust": {
              "acm": {
                "certificateAuthorityArns": [
                  $ROOT_CA_ARN
                ]
              }
            }
          }
        }
      }
    }
  }
}

ハイライト部分 (7~14行目および19~29行目) を削除します。

変更後:

colorgateway-vg-backendDefaults.json

{
  "spec": {
    "listeners": [{
      "portMapping": {
        "port": 9080,
        "protocol": "http"
      }
    }],
    "backendDefaults": {
      "clientPolicy": {
      }
    }
  }
}

App Meshリソースを作成する

ファイルの修正が終わりましたら、スクリプトを実行します。

$ ./mesh/mesh.sh up

こちらのスクリプトは、CloudFormationではなくAWS CLIコマンドを使ってApp Meshリソースの作成を行います。
(ですので、AWS CLIのバージョンが古いとエラーになる場合があります)

特にエラーが出ることなくスクリプトが最後まで実行されればOKです。

ECSサービスをデプロイ

既に作成済みのECSクラスター上に、ECSの「サービスディスカバリ」「タスク定義」「サービス」を作成していきます。
また、仮想ゲートウェイに対してトラフィックを送る役目の「ELB (NLB)」についても、併せて作成します。

CloudFormationテンプレートを書き換える

NLBターゲットグループの定義の中に「TLS」に関する記述がありますので、こちらを書き換えます。

$ vi infrastructure/ecs-service.yaml

変更前:

ecs-service.yaml (抜粋)

WebTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: 9080
      HealthCheckProtocol: TCP
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 3
      TargetType: ip
      Name: !Sub "${EnvironmentName}-web"
      Port: 443
      Protocol: TLS
      UnhealthyThresholdCount: 3
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 120
      VpcId:
        'Fn::ImportValue': !Sub "${EnvironmentName}:VPC"

ハイライト部分 (649~650行目) を以下のように書き換えます。

変更後:

ecs-service.yaml (抜粋)

WebTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: 9080
      HealthCheckProtocol: TCP
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 3
      TargetType: ip
      Name: !Sub "${EnvironmentName}-web"
      Port: 9080
      Protocol: TCP
      UnhealthyThresholdCount: 3
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 120
      VpcId:
        'Fn::ImportValue': !Sub "${EnvironmentName}:VPC"

同じファイルの中で、もう1点、書き換えを行います。

NLBリスナーの定義の中に「TLS」に関する記述がありますので、こちらについても書き換えます。
外部 (インターネット) 向けロードバランサーですのでACMから無償で発行できるパブリック証明書が利用できますが、手順の簡略化のために今回はTLSを使わないものとしました。

変更前:

ecs-service.yaml (抜粋)

PublicLoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn:
      - PublicLoadBalancer
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref WebTargetGroup
          Type: 'forward'
      LoadBalancerArn: !Ref 'PublicLoadBalancer'
      Port: 443
      Protocol: TLS
      Certificates:
        - CertificateArn: !Sub '${LoadBalancerCertificateArn}'

ハイライト部分のうち、667~668行目の「Port」「Protocol」を書き換えて、669~670行目の「Certificate」セクションは削除します。

変更後:

ecs-service.yaml (抜粋)

PublicLoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn:
      - PublicLoadBalancer
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref WebTargetGroup
          Type: 'forward'
      LoadBalancerArn: !Ref 'PublicLoadBalancer'
      Port: 80
      Protocol: TCP

ECSサービスをデプロイする

以下のようにスクリプトを実行します。

$ ./infrastructure/ecs-service.sh

スクリプトを実行するとCloudFormationのスタックが作成されます。

以下のように出力されれば、作成は成功です。
Successfully created/updated stack - AppMeshIngressExample-ecs-service

最後に、踏み台サーバーのIPアドレスとNLBのエンドポイントFQDNが表示されますので、メモしておきましょう。

Bastion endpoint:
xx.xx.xx.xx
ColorApp Endpoint:
http://AppMe-Publi-XXXXXXXXXXXXX-XXXXXXXXXXXXXXXX.elb.ap-northeast-1.amazonaws.com

アプリケーションに対してアクセスしてみる

これで全ての環境が準備できましたので、NLBのエンドポイントからアクセスしてみましょう。

まず、さきほど表示されたNLBエンドポイントFQDNを環境変数に設定します。

export COLORAPP_ENDPOINT=http://AppMe-Publi-XXXXXXXXXXXXX-XXXXXXXXXXXXXXXX.elb.ap-northeast-1.amazonaws.com

パスに「/color1」を指定してアクセスします。

$ curl "${COLORAPP_ENDPOINT}/color1/tell"
white
$ curl "${COLORAPP_ENDPOINT}/color1/tell"
blue
$ curl "${COLORAPP_ENDPOINT}/color1/tell"
white
$ curl "${COLORAPP_ENDPOINT}/color1/tell"
blue
$ curl "${COLORAPP_ENDPOINT}/color1/tell"
blue
$ curl "${COLORAPP_ENDPOINT}/color1/tell"
blue

6回アクセスしてみたところ、「white」が2回、「blue」が4回という結果でした。
(1:1の加重ルーティングですが、完全に等確率でレスポンスが返る訳ではありません)

今度は、パスに「/color2」を指定してアクセスします。

$ curl "${COLORAPP_ENDPOINT}/color2/tell"
black
$ curl "${COLORAPP_ENDPOINT}/color2/tell"
red
$ curl "${COLORAPP_ENDPOINT}/color2/tell"
black
$ curl "${COLORAPP_ENDPOINT}/color2/tell"
red
$ curl "${COLORAPP_ENDPOINT}/color2/tell"
red
$ curl "${COLORAPP_ENDPOINT}/color2/tell"
red

「black」または「red」のいずれかが返ってきました。

これで、「仮想ゲートウェイ」でパスベースの振り分けが行われること、「仮想ルーター」で重さによる振り分けが行われることが確認できました。

後片付け

ハンズオンが終わりましたら、作成したリソースを削除します。

CloudFormationで作成したリソースを削除します:
(ECRリポジトリを削除する前に、登録したイメージを削除する必要があることに注意してください)

$ aws cloudformation delete-stack --stack-name $ENVIRONMENT_NAME-ecs-service
$ aws ecr delete-repository --force --repository-name $COLOR_TELLER_IMAGE_NAME
$ aws cloudformation delete-stack --stack-name $ENVIRONMENT_NAME-ecr-repositories
$ aws cloudformation delete-stack --stack-name $ENVIRONMENT_NAME-ecs-cluster
$ aws cloudformation delete-stack --stack-name $ENVIRONMENT_NAME-vpc

App Meshの各種リソースを削除します:

$ ./mesh/mesh.sh down

おわりに

TLSに関して修正を行わざるを得なかったのは残念ですが、App Meshの新しい機能「仮想ゲートウェイ」を試すことができました。

※ 「内部通信だからTLSはそれほど重要ではないのでは」という意見があるかもしれませんが、昨今の「ゼロトラストネットワーク」の考え方や、App Mesh (サービスメッシュ) の利用目的の一つに「マルチテナント」等があることを考慮すると、TLSによるデータ転送中の暗号化は重要なファクターであると思います。

ハンズオンはEKSを使ったパターンも用意されていますので、機会があれば、次回はEKSを試してみたいと思います。