チュートリアル - Amazon ECS with Fargate 上の Go アプリケーションのトレースを有効にする

概要

このチュートリアルでは、AWS Elastic Container Service (ECS) with Fargate 上のクラスターにインストールされたサンプル Go アプリケーションでトレースを有効にするための手順を説明します。このシナリオでは、Datadog Agent もクラスターにインストールされています。

ホスト上のアプリケーションと Agent、コンテナ内のアプリケーションとホスト上の Agent、クラウドインフラストラクチャー上のアプリケーションと Agent、他の言語で書かれたアプリケーションなど、他のシナリオについては、他のトレース有効化のチュートリアルを参照してください。例えば、コンテナや EKS を使用したチュートリアルの中には、Datadog で見られる自動インスツルメンテーションとカスタムインスツルメンテーションの違いを説明するものがあります。このチュートリアルでは、完全にカスタムインスツルメンテーションされた例までスキップします。

このチュートリアルでは、中級レベルの AWS トピックも使用しているので、AWS のネットワークとアプリケーションにある程度慣れていることが必要です。もしあなたが AWS にそれほど精通しておらず、Datadog APM のセットアップの基本を学ぼうとしているならば、代わりにホストまたはコンテナのチュートリアルのいずれかを使用してください。

Go 向けのトレース設定を幅広くまとめたドキュメントについては、Go アプリケーションのトレース設定 を参照してください。Note: This documentation uses v2 of the Go tracer, which Datadog recommends for all users. If you are using v1, see the migration guide to upgrade to v2.

前提条件

  • Datadog のアカウントと組織の API キー
  • Git
  • Docker
  • Terraform
  • Amazon ECS
  • イメージをホスティングするための Amazon ECR リポジトリ
  • AdministratorAccess 権限を持つ AWS IAM ユーザー。アクセスキーとシークレットアクセスキーを使用して、ローカルの資格情報ファイルにプロファイルを追加する必要があります。詳しくは、AWS SDK for Go V2 の構成をご覧ください。

サンプルの Go アプリケーションをインストールする

次に、トレースするためのサンプルアプリケーションをインストールします。このチュートリアルのコードサンプルは github.com/DataDog/apm-tutorial-golang.git で見ることができます。以下を実行することで git リポジトリの複製を行います。

git clone https://github.com/DataDog/apm-tutorial-golang.git

リポジトリには、Docker コンテナ内で動作するようにあらかじめ構成されたマルチサービスの Go アプリが含まれています。コンテナを作成するための docker-compose YAML ファイルは docker ディレクトリに配置されています。このチュートリアルでは、サンプルアプリケーションを構成する notes および calendar サービス用のコンテナをビルドする service-docker-compose-ECS.yaml ファイルを使用します。

また、このチュートリアルでは、terraform/Fargate ディレクトリにあるいくつかのコンフィギュレーションファイルを使用して、Fargate でサンプルアプリケーションを ECS にデプロイするための環境を構築しています。

ECS の初期設定

このアプリケーションでは、AWS プロファイル (ECS クラスターを作成し、ECR から読み取るための正しい権限で構成済み)、AWS リージョン、Amazon ECR リポジトリの追加など、いくつかの初期構成が必要です。

terraform/Fargate/global_constants/variables.tf を開きます。以下の変数の値を、正しい AWS アカウント情報に置き換えます。

output "aws_profile" {
    value = "<AWS_PROFILE>"
    sensitive = true
}

output "aws_region" {
    value = "<AWS_REGION>"
    sensitive = true
}

output "aws_ecr_repository" {
    value = "<AWS_ECR_REPOSITORY_URL>"
    sensitive = true
}

datadog_api_key セクションは、とりあえずコメントにしておきます。Datadog の設定はチュートリアルの後半で行うことになります。

アプリケーションイメージの構築とアップロード

コンテナイメージのレジストリである Amazon ECR に馴染みがない方は、Amazon ECR を AWS CLI で使うを読むとよいかもしれません。

サンプルプロジェクトの /docker ディレクトリで、以下のコマンドを実行します。

  1. このコマンドでユーザー名とパスワードを入力し、ECR で認証します。

    aws ecr get-login-password --region us-east-1 | docker login --username <YOUR_AWS_USER> --password-stdin <USER_CREDENTIALS>

  2. サンプルアプリの Docker イメージを構築し、プラットフォーム設定を合わせます。

    DOCKER_DEFAULT_PLATFORM=linux/amd64 docker-compose -f service-docker-compose-ECS.yaml build

  3. コンテナに ECR 宛先のタグを付けます。

    docker tag docker_notes:latest <ECR_REGISTRY_URL>:notes
    docker tag docker_calendar:latest <ECR_REGISTRY_URL>:calendar

  4. コンテナを ECR レジストリにアップロードします。

    docker push <ECR_REGISTRY_URL>:notes
    docker push <ECR_REGISTRY_URL>:calendar

(トレースを有効にしていない) アプリケーションはコンテナ化され、ECS がプルできるようになります。

アプリケーションをデプロイする

アプリケーションを起動し、トレースせずにいくつかのリクエストを送信します。アプリケーションがどのように動作するかを確認した後、トレーシングライブラリと Datadog Agent を使用してインスツルメントを行います。

まずは、Terraform スクリプトを使って Amazon ECS にデプロイします:

  1. terraform/Fargate/deployment ディレクトリで、以下のコマンドを実行します。

    terraform init
    terraform apply
    terraform state show 'aws_alb.application_load_balancer'
    

    : terraform apply コマンドが CIDR ブロックメッセージを返す場合、IP アドレスを取得するスクリプトはローカルマシンでは動作しませんでした。これを解決するには、terraform/Fargate/deployment/security.tf ファイルで値を手動で設定します。load_balancer_security_groupingress ブロック内で、どの cidr_blocks 行がコメントアウトされているかを切り替え、コメントアウトされていない例の行をマシンの IP4 アドレスで更新してください。

  2. ロードバランサーの DNS 名をメモしておきます。サンプルアプリの API コールでは、そのベースドメインを使用します。インスタンスが起動するまで数分待ちます。

  3. 別のターミナルを開いて、アプリを行使するために API リクエストを送信します。ノートアプリケーションは、同じコンテナで実行されているメモリ内 H2 データベースにデータを保存する REST API です。これにいくつかのコマンドを送信します。

    curl -X GET 'BASE_DOMAIN:8080/notes'
    []
    curl -X POST 'BASE_DOMAIN:8080/notes?desc=hello'
    {"id":1,"description":"hello"}
    curl -X GET 'BASE_DOMAIN:8080/notes?id=1'
    {"id":1,"description":"hello"}
    curl -X GET 'BASE_DOMAIN:8080/notes'
    [{"id":1,"description":"hello"}]
    curl -X PUT 'BASE_DOMAIN:8080/notes/1?desc=UpdatedNote'
    {"id":1,"description":"UpdatedNote"}
    curl -X GET 'BASE_DOMAIN:8080/notes'
    [{"id":1,"description":"UpdatedNote"}]
    curl -X POST 'BASE_DOMAIN:8080/notes?desc=NewestNote&add_date=y'
    {"id":2,"description":"NewestNote with date 12/02/2022."}

    このコマンドは notescalendar の両方のサービスを呼び出します。

  4. アプリケーションの実行を確認したら、以下のコマンドを実行してアプリケーションを停止し、AWS リソースをクリーンアップして、トレースを有効にできるようにします。

    terraform destroy

トレースを有効にする

次に、トレースを有効にするために Go アプリケーションを構成します。

トレースサポートを有効にするには

  1. apm-tutorial-golang/cmd/notes/main.go で以下のインポートのコメントを解除してください。

    cmd/notes/main.go

         sqltrace "github.com/DataDog/dd-trace-go/contrib/database/sql/v2"
         chitrace "github.com/DataDog/dd-trace-go/contrib/go-chi/chi/v2"
         httptrace "github.com/DataDog/dd-trace-go/contrib/net/http/v2"
         "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
       
  2. main() 関数で、以下の行のコメントを解除します。

    cmd/notes/main.go

       tracer.Start()
       defer tracer.Stop()

    cmd/notes/main.go

       client = httptrace.WrapClient(client, httptrace.RTWithResourceNamer(func(req *http.Request) string {
          return fmt.Sprintf("%s %s", req.Method, req.URL.Path)
       }))

    cmd/notes/main.go

       r.Use(chitrace.Middleware(chitrace.WithService("notes")))
  3. setupDB() で、以下の行のコメントを解除します。

    cmd/notes/main.go

       sqltrace.Register("sqlite3", &sqlite3.SQLiteDriver{}, sqltrace.WithService("db"))
       db, err := sqltrace.Open("sqlite3", "file::memory:?cache=shared")

    cmd/notes/main.go

       db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
  4. 上記の手順で、完全にサポートされているライブラリでの自動トレーシングが可能になりました。コードがサポートされているライブラリに該当しない場合、スパンを手動で作成することができます。

    notes/notesController.go を開きます。このサンプルには、コードにカスタムトレースを設定するさまざまな方法を示す、コメントアウトされたコードがすでに含まれています。

  5. notes/notesController.gomakeSpanMiddleware 関数は、リクエストを指定された名前のスパンでラップするミドルウェアを生成します。以下の行のコメントを解除します。

    notes/notesController.go

       r.Get("/notes", nr.GetAllNotes)                // GET /notes
       r.Post("/notes", nr.CreateNote)                // POST /notes
       r.Get("/notes/{noteID}", nr.GetNoteByID)       // GET /notes/123
       r.Put("/notes/{noteID}", nr.UpdateNoteByID)    // PUT /notes/123
       r.Delete("/notes/{noteID}", nr.DeleteNoteByID) // DELETE /notes/123

    notes/notesController.go

       r.Get("/notes", makeSpanMiddleware("GetAllNotes", nr.GetAllNotes))               // GET /notes
       r.Post("/notes", makeSpanMiddleware("CreateNote", nr.CreateNote))                // POST /notes
       r.Get("/notes/{noteID}", makeSpanMiddleware("GetNote", nr.GetNoteByID))          // GET /notes/123
       r.Put("/notes/{noteID}", makeSpanMiddleware("UpdateNote", nr.UpdateNoteByID))    // PUT /notes/123
       r.Delete("/notes/{noteID}", makeSpanMiddleware("DeleteNote", nr.DeleteNoteByID)) // DELETE /notes/123

    また、以下のインポート周りのコメントも削除してください。

    notes/notesController.go

        "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
       
  6. doLongRunningProcess 関数は、親コンテキストから子スパンを作成します。コメントを削除して有効にします。

    notes/notesHelper.go

       func doLongRunningProcess(ctx context.Context) {
        childSpan, ctx := tracer.StartSpanFromContext(ctx, "traceMethod1")
        childSpan.SetTag(ext.ResourceName, "NotesHelper.doLongRunningProcess")
        defer childSpan.Finish()
    
        time.Sleep(300 * time.Millisecond)
        log.Println("Hello from the long running process in Notes")
        privateMethod1(ctx)
      }

  7. privateMethod1 関数は、コンテキストから完全に独立したサービスを作成することを示します。コメントを削除して有効にします。

    notes/notesHelper.go

    func privateMethod1(ctx context.Context) {
       childSpan, _ := tracer.StartSpanFromContext(ctx, "manualSpan1",
          tracer.SpanType("web"),
          tracer.ServiceName("noteshelper"),
       )
       childSpan.SetTag(ext.ResourceName, "privateMethod1")
       defer childSpan.Finish()
    
       time.Sleep(30 * time.Millisecond)
       log.Println("Hello from the custom privateMethod1 in Notes")
    }

    カスタムトレースの詳細については、Go カスタムインスツルメンテーションを参照してください。

  8. terraform/Fargate/deployment/main.tf を開きます。サンプルアプリには ECS Fargate 上で Datadog Agent を実行しトレースを収集するために必要な基本構成が既にあります。つまり、ECS Fargate を有効にし APM を有効にする API キー (次のステップで構成する) です。この定義は、notes タスクと calendar タスクの両方で提供されています。

  9. API キー変数に値を指定します。terraform/Fargate/global_constants/variables.tf を開き、output "datadog_api_key" セクションのコメントを解除し、組織の Datadog API キーを提供します。

  10. 異なるバージョンやデプロイ環境間でトレースされたサービスを識別する統合サービスタグにより、Datadog 内で相関が取れるようになり、検索やフィルターに利用できるようになります。統合サービスタグ付けに使用する環境変数は、DD_SERVICEDD_ENVDD_VERSION の 3 つです。ECS 上にデプロイされたアプリケーションの場合、これらの環境変数はコンテナのタスク定義内で設定されます。

    このチュートリアルでは、/terraform/Fargate/deployment/main.tf ファイルに、ノートとカレンダーアプリケーションのためのこれらの環境変数がすでに定義されています。例えば notes の場合:

    {
     ...
    
       name : "notes-task",
       image : "${module.settings.aws_ecr_repository}:notes",
       essential : true,
       portMappings : [
         {
           containerPort : 8080,
           hostPort : 8080
         }
       ],
       memory : 512,
       cpu : 256,
       environment : [
         {
           name : "CALENDAR_HOST",
           value : "calendar.apmlocalgo"
         },
         {
           name : "DD_SERVICE",
           value : "notes"
         },
         {
           name : "DD_ENV",
           value : "dev"
         },
         {
           name : "DD_VERSION",
           value : "0.0.1"
         }
       ],
       dockerLabels : {
         "com.datadoghq.tags.service" : "notes",
         "com.datadoghq.tags.env" : "dev",
         "com.datadoghq.tags.version" : "0.0.1"
       },
     },
    
     ...
    

    そして calendar の場合:

    ...
    
       name : "calendar-task",
       image : "${module.settings.aws_ecr_repository}:calendar",
       essential : true,
       environment : [
         {
           name : "DD_SERVICE",
           value : "calendar"
         },
         {
           name : "DD_ENV",
           value : "dev"
         },
         {
           name : "DD_VERSION",
           value : "0.0.1"
         }
       ],
       dockerLabels : {
         "com.datadoghq.tags.service" : "calendar",
         "com.datadoghq.tags.env" : "dev",
         "com.datadoghq.tags.version" : "0.0.1"
       },
     ...
    

    また、同じユニバーサルサービスタグの serviceenvversion の値に対する Docker ラベルが設定されていることがわかります。これにより、アプリケーションを起動したら Docker メトリクスを取得することもできます。

アプリケーションイメージの再構築とアップロード

前回と同じ手順でトレースを有効にしてイメージを再構築します。

aws ecr get-login-password --region us-east-1 | docker login --username <YOUR_AWS_USER> --password-stdin <USER_CREDENTIALS>
DOCKER_DEFAULT_PLATFORM=linux/amd64 docker-compose -f service-docker-compose-ECS.yaml build
docker tag docker_notes:latest <ECR_REGISTRY_URL>:notes
docker tag docker_calendar:latest <ECR_REGISTRY_URL>:calendar
docker push <ECR_REGISTRY_URL>:notes
docker push <ECR_REGISTRY_URL>:calendar

トレースを有効にしたマルチサービスアプリケーションはコンテナ化され、ECS がプルできるようになります。

トレースを見るためにアプリを起動する

アプリケーションを再デプロイし、API を実行します。

  1. インスツルメンテーション用の設定を組み込んだ構成ファイルを使い、先ほどと同じ Terraform コマンド でアプリケーションを Amazon ECS に再デプロイします。terraform/Fargate/deployment ディレクトリで、次のコマンドを実行します:

    terraform init
    terraform apply
    terraform state show 'aws_alb.application_load_balancer'
    
  2. ロードバランサーの DNS 名をメモしておきます。サンプルアプリの API コールでは、そのベースドメインを使用します。

  3. インスタンスが起動するまで数分待ちます。アプリケーション用のコンテナが準備できたことを確認するため、数分間待ちます。いくつかの curl コマンドを実行して、インスツルメンテーションされたアプリを実行します。

    curl -X GET 'BASE_DOMAIN:8080/notes'
    []
    curl -X POST 'BASE_DOMAIN:8080/notes?desc=hello'
    {"id":1,"description":"hello"}
    curl -X GET 'BASE_DOMAIN:8080/notes?id=1'
    {"id":1,"description":"hello"}
    curl -X GET 'BASE_DOMAIN:8080/notes'
    [{"id":1,"description":"hello"}]
    curl -X PUT 'BASE_DOMAIN:8080/notes/1?desc=UpdatedNote'
    {"id":1,"description":"UpdatedNote"}
    curl -X GET 'BASE_DOMAIN:8080/notes'
    [{"id":1,"description":"hello"}]
    curl -X POST 'BASE_DOMAIN:8080/notes?desc=NewestNote&add_date=y'
    {"id":2,"description":"NewestNote with date 12/02/2022."}
    このコマンドは notescalendar の両方のサービスを呼び出します。
  4. しばらく待って、Datadog の UI を見てみてください。APM > Traces に移動します。Traces リストには、次のように表示されます。

    Traces ビューに、ホストから届いた trace データが表示されている。

    データベース (db) と notes アプリのエントリがあります。トレースリストには、すべてのスパン、いつ開始したか、どのリソースがスパンで追跡されたか、どれくらいの時間がかかったか、が表示されます。

もし、トレースが表示されない場合は、Traces Search フィールドのフィルターをクリアしてください (使用していない ENV などの環境変数にフィルターをかけている場合があります)。

トレースの検証

Traces ページで、POST /notes トレースをクリックすると、各スパンにかかった時間や、あるスパンが完了する前に他のスパンが発生したことを示すフレームグラフが表示されます。グラフの上部にあるバーは、前の画面で選択したスパンです (この場合、ノートアプリケーションへの最初のエントリポイントです)。

バーの幅は、それが完了するまでにかかった時間を示します。低い深さのバーは、高い深さのバーの寿命の間に完了するスパンを表します。

POST トレースのフレームグラフは次のようになります。

POST トレースのフレームグラフ。

GET /notes トレースは次のようになります。

GET トレースのフレームグラフ。

詳しくは、カスタムインストルメンテーションをご覧ください。

単一のアプリケーションをトレースすることは素晴らしいスタートです。しかし、トレースの本当の価値は、リクエストがサービスを通じてどのように流れているかを見ることです。これを_分散型トレーシング_と呼びます。最後の API コール (ノートに日付を追加したコール) のトレースをクリックすると、2 つのサービス間の分散型トレースを見ることができます。

分散型トレーシングのフレームグラフ。

複数のアプリケーションからのインタラクションを組み合わせたフレームグラフです。

  • 最初のスパンは、ユーザーが送信した POST リクエストで、サポートされている go-chi ライブラリを通じて chi ルーターが処理します。
  • 2 つ目のスパンは、makeSpanMiddleware 関数によって手動でトレースされた createNote 関数です。この関数は、HTTP リクエストのコンテキストからスパンを作成します。
  • 次のスパンは、サポートされている http ライブラリと main.go ファイルで初期化されたクライアントを使用して、ノートアプリケーションから送信されたリクエストです。この GET リクエストは、カレンダーアプリケーションに送信されます。カレンダーアプリケーションのスパンは、別のサービスであるため、青色で表示されます。
  • カレンダーアプリケーションの内部では、go-chi ルーターが GET リクエストを処理し、GetDate 関数は GET リクエストの下にある独自のスパンで手動でトレースされます。
  • 最後に、紫の db 呼び出しは、サポートされている sql ライブラリからの独自のサービスです。GET /Calendar リクエストと同じレベルに表示されますが、これはどちらも親スパンの CreateNote から呼び出されるからです。

確認が終わったら、すべてのリソースをクリーンアップし、デプロイを削除してください。

terraform destroy

トラブルシューティング

もし、期待通りのトレースが受信できない場合は、Go トレーサーのでデバッグモードを設定してください。詳しくはデバッグモードの有効化を読んでください。

参考資料