👣

AWS Distro for OpenTelemetry を使ってトレースを X-Ray でサクッと可視化する on EKS

2022/10/06に公開

この文書は何か

AWS Distro for OpenTelemetry(ADOT) を使ったトレース情報の収集と X-Ray での可視化を、EKS 上にアプリケーションをデプロイし最小構成でハンズオンします。

OpenTelemetry は、テレメトリを収集するための計装ライブラリやエクスポーター、送信プロトコルの標準仕様を定めているオープンソースプロジェクトです。アプリケーションにベンダ依存の API・SDK を入れることなく、テレメトリ収集の計装をすることができるため、プロプライエタリからの解放を期待できます。
※ O11yCon2022 の OpenTelemetryのこれまでとこれから のセッションが大変勉強になります。

ADOT は OpenTelemetry の AWS サポートディストリビューションです。EKS を含む AWS サービス上で OpenTelemetry を使うために便利なコンポーネントがプリインストールされています。
アプリケーションはオープン化(OpenTelemetry 化)しつつ、AWS のモニタリングソリューションは継続的に使いたいというケースは多くあると思うので、ADOT は個人的興味のあるサービスです。

今回やること

EKS の構築から、ADOT 環境の構築、openTelemetry-go を用いたサンプルアプリの実装、X-Ray でのトレース情報の取得までを最小構成で行います。

  • EKS クラスタの構築
  • ADOT のインストール
  • opentelemetry-go を用いたアプリ実装と EKS へのデプロイ
  • X-Ray の確認

EKS クラスタの構築

EKS クラスタの構築は eksctl コマンドを用いることで簡易的に実施できます。

  • IAM の準備
    • eksctl でクラスタ作成では VPC や NAT IP など含んだ様々コンポーネントを作成するため、適切なポリシーを付与したユーザーを使った操作が必要です
    • 今回は構築目的なので、AdministratorAccess ポリシーをアタッチしたユーザーを作成し、その認証情報を使い eksctl を実行をします
    • 以下のように config ファイルに認証情報をセットしていることを確認してください
      ❯ aws configure list
            Name                    Value             Type    Location
            ----                    -----             ----    --------
         profile                <not set>             None    None
      access_key     ******************** shared-credentials-file
      secret_key     ******************** shared-credentials-file
          region           ap-northeast-1      config-file    ~/.aws/config
      
  • EKS クラスタの構築
    以下のコマンドを実行し Kubernets クラスタを作成します。実行には 15 分ほどかかりました。
    ※ CloudFormation で環境が作られるため、CloudFormation の管理画面で進捗を確認できます
    ❯ eksctl create cluster \
    	--name <クラスタ名は適宜変更> \
    	--region <リージョンは適宜変更 例: ap-northeast-1> \
    	--version 1.22 \ # ※ 1.19 以降要
    	--nodegroup-name <ノードグループは適宜変更> \
    	--node-type t3.small \
    	--nodes 2 \
    	--nodes-min 2 \
    	--nodes-max 3
    
    構築が終わったら EKS クラスタ への接続(とクラスタ名の変更)を行います。
    • kubeconfig の更新
      ❯ aws eks --region <リージョン> update-kubeconfig --name <クラスタ名>
      ❯ kubectl config rename-context <元の名前> <新しい名前>
      
      ~
      (|otel-work-eks-zenn:default) # kube-ps1 設定で接続クラスタ表示。上で作ったクラスタに接続
    • Pod リソースの確認
      ❯ kubectl get pod -A
      NAMESPACE     NAME                       READY   STATUS    RESTARTS   AGE
      kube-system   aws-node-9rmjn             1/1     Running   0          2m46s
      kube-system   aws-node-qw5jb             1/1     Running   0          2m41s
      kube-system   coredns-5b6d4bd6f7-d8h68   1/1     Running   0          12m
      kube-system   coredns-5b6d4bd6f7-gt6qm   1/1     Running   0          12m
      kube-system   kube-proxy-9fhwp           1/1     Running   0          2m41s
      kube-system   kube-proxy-n9v7q           1/1     Running   0          2m46s
      

ADOT のインストール

ADOT 環境を整備するために、ADOT アドオンのインストール、ADOT Collector のデプロイなどが必要となります。基本的には CLI 操作で完結します。

  • ADOT アドオンのインストール
    ADOT アドオンは k8s に ADOT 操作機能を提供します。アドオンについてはこちらを参照。
    • ADOT アドオンをインストールするための RBAC 設定を適用
      ❯ kubectl apply -f https://amazon-eks.s3.amazonaws.com/docs/addons-otel-permissions.yaml
      
    • cert-manager のインストール。公式 はこちらを参照。
      # helm chart への repository 追加
      ❯ helm repo add jetstack https://charts.jetstack.io
      ❯ helm repo update
      
      # sample namespace への cert-manager のデプロイ
      ❯ helm install \
        cert-manager jetstack/cert-manager \
        --namespace sample \
        --create-namespace \
        --version v1.5.5. \ # ADOT は 1.6.0 未満をサポート
        --set installCRDs=true	
      
    • ADOT アドオンのインストール
      ❯ aws eks create-addon --addon-name adot --addon-version v0.45.0-eksbuild.1 --cluster-name <クラスタ名>
      ❯ aws eks describe-addon --addon-name adot --cluster-name <クラスタ名> | jq .addon.status
      "ACTIVE" # ADOT アドオンが有効化
      
  • ADOT Collector のデプロイ設定
    ADOT Collector はアプリケーションからエクスポートされたテレメトリ情報を集約し、X-Ray などのバックエンドに送信するプロキシです。
    ※ Collector に関しては O11yCon2022 の 入門 OpenTelemetry Collector が参考になります
    ADOT アドオンは Kubernetes Operator の実装であり、OpenTelemetryCollector のカスタムリソースをデプロイすることができます。マニフェストは以下。
    opentelemetry-collector-sample.yaml
    apiVersion: opentelemetry.io/v1alpha1
    kind: OpenTelemetryCollector
    metadata:
      name: sample
      namespace: sample
    spec:
      image: public.ecr.aws/aws-observability/aws-otel-collector:v0.17.0
      mode: deployment
      serviceAccount: sample
      config: |
        receivers:
          otlp:
    	protocols:
    	  grpc:
    	    endpoint: "0.0.0.0:4318"
        processors:
        exporters:
          awsxray:
    	region: ap-northeast-1
        service:
          pipelines:
    	traces:
    	  receivers: [otlp]
    	  processors: []
    	  exporters: [awsxray]
    
    いくつか留意点があるので説明します。
    • ServiceAccouunt を作成し、AWSXRayDaemonWriteAccess ポリシーを持つロールを紐付ける必要があります
      ❯ kubectl create sa <サービスアカウント名> -n sample # ここの名前を↑のマニフェストに記述します
      ❯ kubectl annotate sa sample -n sample eks.amazonaws.com/role-arn=arn:aws:iam::<自身のARN>:role/<自身のロール名>
      
    • トレース情報を otlp 形式で受信し、awsxray 形式で送信する設定をしています
  • ADOT Collector のデプロイ
    上記のマニフェストファイルをアプライします
    ❯ kubectl apply -f opentelemetry-collector-sample.yaml -n sample
    ❯ kubectl get po -n sample | grep collector
    sample-collector-7cbd4db667-rznzm          1/1     Running   0          29s
    
  • 最後、EKS クラスタに IAM OIDC Provider を設定します。
    ❯ eksctl utils associate-iam-oidc-provider --cluster <自身のクラスタ名> --approve
    
    OIDC プロバイダ ID は AWS コンソールの EKS クラスタ概要から確認できるOpenID Connect プロバイダー URL の下 32 文字のランダム値を参照。以下のように上記で作ったロールの信頼関係を編集します。
    ロールの信頼関係
    {
        "Version": "2012-10-17",
        "Statement": [
    	{
    	    "Effect": "Allow",
    	    "Principal": {
    		"Service": "ec2.amazonaws.com"
    	    },
    	    "Action": "sts:AssumeRole"
    	},
    	### --- 今回追加する部分 --- ###
    	{
    	    "Sid": "Statement2",
    	    "Effect": "Allow",
    	    "Principal": {
    		"Federated": "arn:aws:iam::<自身のARN>:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/<自身のOIDCプロバイダID>"
    	    },
    	    "Action": "sts:AssumeRoleWithWebIdentity",
    	    "Condition": {
    		"StringEquals": {
    		    "oidc.eks.ap-northeast-1.amazonaws.com/id/<自身のOIDCプロバイダID>:sub": "system:serviceaccount:<namespace 名 例: sample>:<サービスアカウント名 例: sample"
    		}
    	    }
    	}
    	### --- 今回追加する部分 --- ###
        ]
    }	
    

opentelemetry-go を用いたアプリ実装と EKS へのデプロイ

EKS クラスタ構築と ADOT 環境準備はこれで終わりです。あとはアプリケーションに OpenTelemetry で計装をしていくだけです。今回は opentelemetry-go の SDK を使ってトレース・スパンを取得・エクスポートする計装をしていきます。
サンプルアプリは go の Web Framework である gin を用いて作りました。トレースやスパン情報は Context を介して伝播するため Context Propergation を扱いやすくするためです。

  • アプリの実装
    ソースコード全量はこちらです。
    main.go
    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"os"
    	"os/signal"
    	"time"
    
    	"github.com/gin-gonic/gin"
    	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
    	"go.opentelemetry.io/contrib/propagators/aws/xray"
    	"go.opentelemetry.io/otel"
    	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    	"go.opentelemetry.io/otel/propagation"
    
    	"go.opentelemetry.io/otel/sdk/resource"
    	sdktrace "go.opentelemetry.io/otel/sdk/trace"
    	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/credentials/insecure"
    )
    
    func initProvider() (func(context.Context) error, error) {
    	ctx := context.Background()
    
    	res, err := resource.New(ctx,
    		resource.WithAttributes(
    			semconv.ServiceNameKey.String("sample"),
    		),
    	)
    	if err != nil {
    		return nil, fmt.Errorf("failed to create resource: %w", err)
    	}
    
    	conn, err := grpc.DialContext(ctx, "sample-collector.sample.svc.cluster.local:4318", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
    	if err != nil {
    		return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err)
    	}
    	traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
    	if err != nil {
    		return nil, fmt.Errorf("failed to create trace exporter: %w", err)
    	}
    
    	bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
    	var tracerProvider *sdktrace.TracerProvider
    	tracerProvider = sdktrace.NewTracerProvider(
    		sdktrace.WithSampler(sdktrace.AlwaysSample()),
    		sdktrace.WithResource(res),
    		sdktrace.WithSpanProcessor(bsp),
    		sdktrace.WithIDGenerator(xray.NewIDGenerator()),
    	)
    	otel.SetTracerProvider(tracerProvider)
    	otel.SetTextMapPropagator(propagation.TraceContext{})
    
    	return tracerProvider.Shutdown, nil
    }
    
    var tracer = otel.Tracer("sample")
    
    func main() {
    	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    	defer stop()
    
    	shutdown, err := initProvider()
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer func() {
    		if err := shutdown(ctx); err != nil {
    			log.Fatal("failed to shutdown TracerProvider: %w", err)
    		}
    	}()
    
    	r := gin.New()
    	r.Use(otelgin.Middleware("sample"))
    	r.GET("/sample", sample1)
    	r.Run(":8080")
    
    }
    
    func sample1(c *gin.Context) {
    	_, span := tracer.Start(c.Request.Context(), "sample1")
    	time.Sleep(time.Second * 1)
    	log.Println("sample1 done.")
    	sample2(c)
    	span.End()
    }
    
    func sample2(c *gin.Context) {
    	_, span := tracer.Start(c.Request.Context(), "sample2")
    	time.Sleep(time.Second * 2)
    	log.Println("sample2 done.")
    	sample3(c)
    	span.End()
    }
    
    func sample3(c *gin.Context) {
    	_, span := tracer.Start(c.Request.Context(), "sample3")
    	time.Sleep(time.Second * 3)
    	log.Println("sample3 done.")
    	span.End()
    }
    
    いくつか解説をしていきます。細かい内容は OpenTelemetry in Go が参考になります
    • initProvider() ではトレースプロバイダーを生成しています
      上記でデプロイした ADOT Collector に Kubernetes クラスタ内通信で接続しています
      conn, err := grpc.DialContext(ctx, "sample-collector.sample.svc.cluster.local:4318", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
      
    • tracer.Start で任意の処理でスパンを生成します。
      ここでは各 sample メソッド内で処理を実行し、後続のメソッドを呼び出す形に実装しています。処理が終わったらスパンを閉じるため、span.End() を実行します。
      _, span := tracer.Start(c.Request.Context(), "sample1")
      time.Sleep(time.Second * 1)
      log.Println("sample1 done.")
      sample2(c)
      span.End()
      
  • アプリのデプロイ
    今回は gitlab のレジストリにコンテナイメージを置き、EKS 上にデプロイしました。
    サンプルアプリの yaml ファイル
    sample-app.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: sample
    spec:
      containers:
      - name: sample
        image: registry.gitlab.com/keisuke.sakasai/otel-sample-app-zenn:latest
    
    ❯ kubectl apply -f sample-app.yaml
    ❯ kubectl get po | grep sample
    sample                                     1/1     Running   0          22m
    

X-Ray の確認

ここまで正常に設定がされていれば、サンプルアプリから X-Ray にトレース・スパン情報が送信されコンソール画面から確認することができます。
Pod に portforward して、localhost に対してリクエストを送信してみます。

❯ kubectl port-forward sample 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080curl localhost:8080/sample

X-Ray のコンソール画面にいくと、トレースリストに該当のトレース情報が確認できます。以下のように Span も正常に取得できていることがわかります。

最後に

今回は AWS の ADOT を用いてトレース情報を X-Ray に送信する方法について紹介しました。
アプリケーションに一度計装を施してしまえば、あとは AWS の提供するモニタリングソリューションをすぐに使うことができるので、マネージドサービスの強力さを改めて感じました。
アプリケーション側は OpenTelemetry を使って標準化を行い、バックエンドソリューションは用途に応じて簡単に切り替えることができるようにしておくことが良いのかなと思います。
OpenTelemetry を使って標準化の波に乗りましょう。

Discussion