Azure Container Appsを試す前に知っておきたいこと - Ingress編

Posted on Nov 8, 2021

何の話か

Ignite 2021秋でAzure Container Appsが発表されました。Kubernetesを基盤に、Envoy、KEDA、DaprといったCNCFプロジェクトの成果物をマネージドサービスとして提供するサービスです。ここ数年、マイクロソフトがCNCFで取り組んできたことをまとめてドン、という感じで、なかなか熱いものがあります。

ですが、プレビュー初期ということもあり、公式ドキュメントが充実しているとは言いにくい状況です。そこで、ドキュメントが整備されるまで、試す前に知っておくといいかな、ということをいくつか紹介します。

なお、プレビュー中ということもあり、これから説明する内容には変更される可能性があります。公式ドキュメントを正としてください。また、選定や設計するにあたって足りない情報があれば、どしどし要求しましょう。公式ドキュメント(英語)の各ページ下部にGitHub Issuesへのリンクがあります。

今回はIngress編です。

Azure Container Apps Ingressとは

EnvoyをベースとしたProxyです。

Azure Container Apps プレビューでの HTTPS イングレスの設定

Ingressはユーザの作成したContainer Appsアプリケーションに対し、Container Apps環境内外からアクセスできるよう、エンドポイントを提供します。TLS終端はもちろん、複数リビジョンへのトラフィックスプリットもできます。Container Appsの使いこなしのカギのひとつです。

以下の3つは、わたしが初見の際に持った疑問です。みなさんも、知っておく価値はあると思います。

  • gRPC使える?
  • 名前解決やサービスディスカバリってどうなってる?
  • 同じ環境内のIngress経由トラフィックをプライベートネットワークに閉じ込められる?

gRPC使える?

使えます。

では試してみましょう。grpc/grpc-go のhelloworldサンプルをベースに、負荷分散が効くか確認するため、応答メッセージにホスト名を加えたサーバアプリを作ります。また、gRPCリフレクションも設定します。

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net"
	"os"

	"google.golang.org/grpc"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
	"google.golang.org/grpc/reflection"
)

var (
	port = flag.Int("port", 50051, "The server port")
)

type server struct {
	pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	hostname, _ := os.Hostname()
	return &pb.HelloReply{Message: "Hello " + in.GetName() + ", this is " + hostname}, nil
}

func main() {
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	reflection.Register(s)
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

このgRPCサーバアプリケーション(grpc-greeter)をContainer Appsにデプロイします。負荷分散を確認したいので、レプリカ数は2です。また、Ingressは"external"で公開します。transportは"http2"1、targetPortは"50051"です。

では、gRPCurlでサービスをリストしてみましょう。接続先はIngressエンドポイントです。

$ grpcurl grpc-greeter.makabecchi-123456.canadacentral.azurecontainerapps.io:443 list
grpc.reflection.v1alpha.ServerReflection
helloworld.Greeter

リストできていますね。では、helloworld.Greeter/SayHelloを呼び出してみましょう。

$ grpcurl -d '{"name": "Toru"}' grpc-greeter.makabecchi-123456.canadacentral.azurecontainerapps.io:443 helloworld.Greeter/SayHello
{
  "message": "Hello Toru, this is grpc-greeter--bd346ys-679c6454bc-kctf9"
}
$ grpcurl -d '{"name": "Toru"}' grpc-greeter.makabecchi-123456.canadacentral.azurecontainerapps.io:443 helloworld.Greeter/SayHello
{
  "message": "Hello Toru, this is grpc-greeter--bd346ys-679c6454bc-6452x"
}

呼び出せました。また、複数回呼び出すと、応答のホスト名が変わり、負荷分散できていることがわかります。

名前解決やサービスディスカバリってどうなってる?

Ingressには、FQDNが付与されます。

Azure Container Apps プレビューでアプリケーションを接続する

[Container app name].[Environment unique identifier].[Region name].azurecontainerapps.io

がルールです。たとえば、以下のように。

myapp.happyhill-70162bb9.canadacentral.azurecontainerapps.io

このFQDNを引くと、IngressのIPアドレスが得られます。

ポイントは、externalとinternalで扱いが変わることです。

試しに3つ、Ingress付きアプリケーションを作ってみましょう。すべてNGINXです。external向けを1つ、internalを2つ。

  • nginx-external.makabecchi-123456.canadacentral.azurecontainerapps.io
  • nginx-internal.makabecchi-123456.canadacentral.azurecontainerapps.io
  • nginx-internal-2.makabecchi-123456.canadacentral.azurecontainerapps.io

では、Container Apps環境外部、インターネットから名前解決してみます。

$ dig +noall +answer nginx-external.makabecchi-123456.canadacentral.azurecontainerapps.io
nginx-external.makabecchi-123456.canadacentral.azurecontainerapps.io. 3556 IN A 20.151.223.25
$ dig +noall +answer nginx-internal.makabecchi-123456.canadacentral.azurecontainerapps.io
nginx-internal.makabecchi-123456.canadacentral.azurecontainerapps.io. 3600 IN A 20.151.223.25
$ dig +noall +answer nginx-internal-2.makabecchi-123456.canadacentral.azurecontainerapps.io
nginx-internal-2.makabecchi-123456.canadacentral.azurecontainerapps.io. 3600 IN A 20.151.223.25

すべて同じパブリックIPアドレス(20.151.223.25)が返ってきました。

external向けには、意図通りインターネット経由でアクセスできます。

$ curl -sv https://nginx-external.makabecchi-123456.canadacentral.azurecontainerapps.io
*   Trying 20.151.223.25:443...
* TCP_NODELAY set
* Connected to nginx-external.makabecchi-123456.canadacentral.azurecontainerapps.io (20.151.223.25) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
...
< HTTP/2 200
< server: nginx/1.21.3
...

internal向けには、できません。internal向けに2つ作りましたが、結果は同様です。

$ curl -sv https://nginx-internal.internal.makabecchi-123456.canadacentral.azurecontainerapps.io
*   Trying 20.151.223.25:443...
* TCP_NODELAY set
* Connected to nginx-internal.internal.makabecchi-123456.canadacentral.azurecontainerapps.io (20.151.223.25) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
...
< HTTP/2 404
...

以上から、同じ環境にあるIngressのエンドポイントは、アプリケーション間で共有されることがわかります。そして、internal Ingressは、環境外部からのトラフィックを転送しません。

では、Container Apps環境内部からの名前解決は、どうでしょうか。確認用のコンテナを作って確かめましょう。digやcurlが含まれるコンテナイメージ giantswarm/tiny-tools を使います。ところでデバッグコンテナ的な何かがほしいですね。のびしろです。

digで名前解決を確認します。

$ az containerapp create -n tiny-tools -g rg-containerapp-poc --environment containerapps-env --image giantswarm/tiny-tools --command 'dig, +noall, +answer, nginx-internal.internal.makabecchi-123456.canadacentral.azurecontainerapps.io' --revisions-mode single --cpu 0.25 --memory 0.5Gi

標準出力は、Container Appsに接続したLog Analyticsで確認できます。

k8se-envoy-internal.k8se-system.svc.cluster.local. 5 IN	A 10.0.183.11

環境内部からの名前解決要求に対しては、プライベートアドレスが返ってくることがわかります。もうひとつのIngress internalエンドポイントについても、確認しましょう。

$ az containerapp update -n tiny-tools -g rg-containerapp-poc --image giantswarm/tiny-tools --command 'dig, +noall, +answer, nginx-internal-2.internal.makabecchi-123456.canadacentral.azurecontainerapps.io'
k8se-envoy-internal.k8se-system.svc.cluster.local. 5 IN	A 10.0.183.11

同じIPアドレスが返ってきました。internal Ingressではプライベートな共有エンドポイントが作られ、環境内部からはそのIPで解決されることがわかります。ホスト名からも、想像ができますね。

ここでKubernetes経験者は、同じNamespaceのServiceであればサービス名的な何かだけで解決できるのでは、と思うでしょう。わたしもそうでした。ただし、現在のContainer AppsはNamespaceとServiceが非可視、またIngressがFQDNでフィルタ処理をしているため、現状ではできません。

なお、Ingressを使わず、Daprを使ってもアプリケーション、サービス呼び出しができます。こちらの指定はシンプルです。環境識別子やリージョン名を指定せずにサービスディスカバリできます。例えば以下のように。

http://localhost:3500/v1.0/invoke/myapp

同じ環境内のIngress経由トラフィックをプライベートネットワークに閉じ込められる?

これまでの流れでわかりますね。できます。

では、internal Ingress経由でcurlしてみましょう。

$ az containerapp update -n tiny-tools -g rg-containerapp-poc --image giantswarm/tiny-tools --command 'curl, -sv, https://nginx-internal.internal.makabecchi-123456.canadacentral.azurecontainerapps.io'

Log Analyticsで確認します。

*   Trying 10.0.183.11:443...
* Connected to nginx-internal.internal.makabecchi-123456.canadacentral.azurecontainerapps.io (10.0.183.11) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
...
< HTTP/2 200 
< server: nginx/1.21.3
...

  1. はじめに"http2"にしてから"auto"に変えたリビジョンを作ってもOKだったので"auto"でよし、と思っていたのですが、変更値を反映しないバグがあって、結果オーライだったらしい。 ↩︎