🔎

AWS でオブサーバビリティを完結させる Application Signals をデモしながら理解する

2024/08/22に公開

こんにちは。
ご機嫌いかがでしょうか。
"No human labor is no human error" が大好きな吉井 亮です。

オブサーバビリティの基盤を用意することは簡単ではありません。(難しくもありません)
そして、それを運用していくコストも考えなければなりません。顧客向けのサービスではないので、導入するにあたりプロジェクトオーナーを説得できない場合もあるかもしれません。

それが CloudWatch の画面だけで完結するとしたら素敵なことですね。
Grafana も Prometheus も Jaeger も要りません。CloudWatch だけです。

デモ

Docker compose で Application Signals を体験できるデモを作りました。このデモを触りながら Application Signals の機能を理解していきましょう。

構成は以下になります。
EC2 上で3つのコンテナを立ち上げます。

app は OpenTelemetry の Java サンプルアプリケーションです。
ソースコードは Docs/Language APIs & SDKs/Java/Instrumentation にあります。
app には ADOT Java Agent を含めています。

cw−agent は CloudWatch Agent です。
app から送信したトレースを受け取り、X-Ray 形式に変換して X-Ray へ送信します。
また、トレースからメトリクスを生成し、EMF に変換して CloudWatch Logs へ送信します。CloudWatch 側で自動的に CloudWatch Metrics に表示してくれます。

adot は、AWS Distro for OpenTelemetry Collector です。
今回の構成では、app から送信されたログを受け取り、CloudWatch Logs へ送信する役割で使用しています。

img

準備

以下のリソースを作成してください。
CloudFormation テンプレート も用意しています。

  • VPC
    • 既存のものでも新規作成でも構いません
  • パブリックサブネット
    • 1つ
    • 既存のものでも新規作成でも構いません
  • EC2 インスタンスプロファイル
    • 1つ
    • 付与するポリシーは2つ
      • AmazonSSMManagedInstanceCore
      • CloudWatchAgentServerPolicy
  • セキュリティグループ
    • 1つ
    • アウトバウンドは全て許可
  • EC2 インスタンス
    • 1台
    • AMI は以下のいずれか
      • Amazon ECS-Optimized Amazon Linux 2023 (AL2023) x86_64 AMI
    • 上のインスタンスプロファイルとセキュリティグループをアタッチ
    • パブリックサブネットに配置
    • スペック -> 2vCPU, 8GB メモリー以上
    • EBS ボリューム -> 30GB 以上
  • CloudWatch Logs ロググループ
    • 1つ
    • グループ名 -> dice-server

EC2 インスタンスが立ち上がったら、以下のコマンドを実行してください。

$ cd ~
$ bash
$ sudo dnf install git zip unzip -y

$ git clone https://github.com/YoshiiRyo1/opsjaws-application-signals-handson.git --depth 1
$ cd opsjaws-application-signals-handson
$ ls

$ chmod +x ./chap1/setup.sh
$ ./chap1/setup.sh

上記コマンドを実行したら、一度 Session Manager からログアウトして再度ログインしてください。

Application Signals 有効

CloudWatch 画面から、左ペインの Application Signalsサービス を選択します。
初回アクセス時には以下のボタンが表示されます。サービスの検出を開始 をクリックしてください。

img

dice のビルド

クローンしたリポジトリの chap2 ディレクトリに移動します。

$ cd ~/opsjaws-application-signals-handson/chap2

build コマンドでアプリケーションをビルドします。

$ docker compose build

出力からエラーが無いことを確認してください。

コンテナ起動

ビルドが正常終了したならば、コンテナを起動します。

$ docker compose up -d
$ docker compose ps -a
## STATUS が全て Up になっていることを確認

リクエスト生成

load_generate.sh を実行して、app にリクエストを送信します。1回実行で1000リクエストを生成します。
リクエストが Application Signals に表示されるまで2~3分かかります。その間、何度が実行してみてください。

$ chmod +x load_generate.sh 
$ ./load_generate.sh

シグナルの確認

マネジメントコンソールの CloudWatch 画面から、左ペインの X-Ray トレーストレース を選択します。
複数個のトレースが確認できるはずです。

xray

トレースの1つをクリックします。詳細画面に遷移します。
ここでは関連するトレースマップ、トレースとスパン、トレースに関連したログが表示されています。
該当する API のレスポンスタイムやどの処理が遅いのか、どの処理が呼び出されているのかなどが確認できます。

trace

子スパンの RollController.index をクリックしてみてください。
概要、リソース、注釈、メタデータ、例外、SQL が表示されます。
OpenTelemetry SDK、ADOT Java Agent、CloudWatch Agent によって付与された様々なメタデータが表示されています。
ここを正確に、かつ、詳細に設定することで、トレースの分析が容易になります。
このデモでは自動計装により様々なメタデータが付与されています。

resources

マネジメントコンソールの CloudWatch 画面から、左ペインの Application Signalsサービス を選択します。
サービス欄に dice-server が表示されています。 app コンテナから送信したシグナルが Application Signals まで届いています。

applicationsignals

dice-server をクリックすると、サービスの健全性を測るために役立つメトリクスが表示されます。
特に サービスオペレーター タブに表示される情報は SLI として利用可能です。

applicationsignals

CloudWatch Logs を見てみます。
/aws/application-signalgs/data というロググループが作成されています。
ここに CloudWatch Agent から EMF 形式でメトリクスが送信されています。

cwlogs

そのメトリクスは CloudWatch Metrics に表示されています。
カスタム名前空間 ApplicationSignals が作られます。Error、Latency、Fault などのメトリクスが表示されています。

cwmetrics

metrics

コンテナ停止

コンテナを停止します。

$ docker compose down

解説

Application Signals を利用するためには、以下の2つのコンポーネントが必要です。

  • CloudWatch Agent
  • AWS Distro for OpenTelemetry

Chapter 2 では、Dockerfile のなかで AWS Distro for OpenTelemetry を組み込んでいます。
Dockerfile に書かず Docker ボリュームを使う方法もあります。
これは、いわゆる自動計装エージェントです。アプリケーションコードに手を入れることなく、各シグナルを収集します。

dice/Dockerfile
ADD https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/v1.32.3/aws-opentelemetry-agent.jar ./aws-opentelemetry-agent.jar

この jar ファイルをコンテナ環境変数 JAVA_TOOL_OPTIONS に設定することで、アプリケーションに対してエージェントを使用可能な状態にできます。

ompose.yaml
JAVA_TOOL_OPTIONS="-javaagent:/app/aws-opentelemetry-agent.jar"

環境変数

他にも必要な環境変数があります。これらを正しく構成することで Application Signals にシグナルを送信できます。

環境変数 説明
OTEL_TRACES_EXPORTER=otlp 必ず otlp
OTEL_LOGS_EXPORTER=otlp 本ハンズオンで使用。ログを ADOT へ送信する。Application Signals では使わない。
OTEL_METRICS_EXPORTER=none 本ハンズオンで使用。Otel のメトリクスは送信しない。
OTEL_PROPAGATORS=xray,tracecontext,baggage,b3 分散トレースにおいて後続へ情報を伝達するために使用。Application Signals では xray を含める。
OTEL_RESOURCE_ATTRIBUTES=service.name=dice-server,aws.log.group.names=dice-server service.name はリソース属性を付与。トレース送信元を識別するのに必須。aws.log.group.namesはトレースとログの紐づけに使用。
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf 必ず http/protobuf
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://cw-agent:4316/v1/traces CloudWatch Agent をトレース宛先として指定。
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://adot:4318/v1/logs 本ハンズオンで使用。ログ宛先。Application Signals では使わない。
OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT=http://cw-agent:4316/v1/metrics CloudWatch Agent をメトリクス宛先として指定。
OTEL_AWS_APPLICATION_SIGNALS_ENABLED=true Application Signals を有効。
OTEL_TRACES_SAMPLER=always_on トレースのサンプリングをしない。
JAVA_TOOL_OPTIONS="-javaagent:/app/aws-opentelemetry-agent.jar" Java 自動計装エージェントのパスを指定。

OpenTelemetry compatibility considerations

ログ

本ハンズオンではアプリケーションのログを CloudWatch Logs のロググループ dice-server に送信しています。
ADOT がログを受け取り、CloudWatch Logs へ送信します。

otel-config.yaml
exporters:
  awscloudwatchlogs:
    log_group_name: "dice-server"
    log_stream_name: "dice"
    region: "ap-northeast-1"
    log_retention: 7

ログにはトレース ID を含めるようにあらかじめ設定しています。

dice/logback.xml
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} trace_id=%X{AWS-XRAY-TRACE-ID} span_id=%X{span_id} trace_flags=%X{trace_flags} %msg%n</pattern>
    </encoder>

コンテナの環境変数 OTEL_RESOURCE_ATTRIBUTESaws.log.group.names=dice-server を設定することで、トレースとログを紐づけることができます。

compose.yaml
OTEL_RESOURCE_ATTRIBUTES=service.name=dice-server,aws.log.group.names=dice-server

X Ray 画面でトレースを見た際に、ログも表示されていたことを思い出してください。
これらの設定により実現しています。

  • ログにトレース ID を含める
  • ログを CloudWatch Logs に送信
  • aws.log.group.names でロググループを指定

宣伝

2024/09/04(水) に OpsJAWS Meetup30 GA記念 Application Signalsを体験しよう を開催します。
本エントリで紹介した内容に加えて、より深堀りしたデモも用意しています。オンラインでも会場参加でもお気軽にご参加ください。

https://opsjaws.connpass.com/event/324019/

おまけ

CloudWach Agent の起動ログを見てみると、OpenTelemetry Collector Config をうかがい知ることができます。

クリックして展開
exporters:
    awsemf/application_signals:
        certificate_file_path: ""
        detailed_metrics: false
        dimension_rollup_option: NoDimensionRollup
        disable_metric_extraction: false
        eks_fargate_container_insights_enabled: false
        endpoint: ""
        enhanced_container_insights: false
        imds_retries: 1
        local_mode: false
        log_group_name: /aws/application-signals/data
        log_retention: 0
        log_stream_name: ""
        max_retries: 2
        metric_declarations:
            - dimensions:
                - - Environment
                  - Operation
                  - Service
                - - Environment
                  - Service
              label_matchers:
                - label_names:
                    - Telemetry.Source
                  regex: ^(ServerSpan|LocalRootSpan)$
                  separator: ;
              metric_name_selectors:
                - Latency
                - Fault
                - Error
            - dimensions:
                - - Environment
                  - Operation
                  - RemoteOperation
                  - RemoteResourceIdentifier
                  - RemoteResourceType
                  - RemoteService
                  - Service
                - - Environment
                  - Operation
                  - RemoteOperation
                  - RemoteService
                  - Service
                - - Environment
                  - RemoteService
                  - Service
                - - Environment
                  - RemoteOperation
                  - RemoteResourceIdentifier
                  - RemoteResourceType
                  - RemoteService
                  - Service
                - - Environment
                  - RemoteOperation
                  - RemoteService
                  - Service
                - - Environment
                  - RemoteResourceIdentifier
                  - RemoteResourceType
                  - RemoteService
                  - Service
                - - RemoteResourceIdentifier
                  - RemoteResourceType
                  - RemoteService
                - - RemoteService
              label_matchers:
                - label_names:
                    - Telemetry.Source
                  regex: ^(ClientSpan|ProducerSpan|ConsumerSpan)$
                  separator: ;
              metric_name_selectors:
                - Latency
                - Fault
                - Error
        middleware: agenthealth/logs
        namespace: ApplicationSignals
        no_verify_ssl: false
        num_workers: 8
        output_destination: cloudwatch
        profile: ""
        proxy_address: ""
        region: ap-northeast-1
        request_timeout_seconds: 30
        resource_arn: ""
        resource_to_telemetry_conversion:
            enabled: false
        retain_initial_value_of_delta_metric: false
        role_arn: ""
        version: "1"
    awsxray/application_signals:
        certificate_file_path: ""
        endpoint: ""
        imds_retries: 1
        index_all_attributes: false
        indexed_attributes:
            - aws.local.service
            - aws.local.operation
            - aws.local.environment
            - aws.remote.service
            - aws.remote.operation
            - aws.remote.environment
            - aws.remote.resource.identifier
            - aws.remote.resource.type
        local_mode: false
        max_retries: 2
        middleware: agenthealth/traces
        no_verify_ssl: false
        num_workers: 8
        profile: ""
        proxy_address: ""
        region: ap-northeast-1
        request_timeout_seconds: 30
        resource_arn: ""
        role_arn: ""
        telemetry:
            enabled: true
            include_metadata: true
extensions:
    agenthealth/logs:
        is_usage_data_enabled: true
        stats:
            operations:
                - PutLogEvents
            usage_flags:
                mode: EC2
                region_type: EC2M
    agenthealth/traces:
        is_usage_data_enabled: true
        stats:
            operations:
                - PutTraceSegments
            usage_flags:
                mode: EC2
                region_type: EC2M
    awsproxy/application_signals:
        aws_endpoint: ""
        certificate_file_path: ""
        dialer:
            timeout: 0s
        endpoint: 0.0.0.0:2000
        imds_retries: 1
        local_mode: false
        profile: ""
        proxy_address: ""
        region: ap-northeast-1
        role_arn: ""
        service_name: ""
processors:
    awsapplicationsignals:
        resolvers:
            - name: ""
              platform: ec2
    resourcedetection:
        aks:
            resource_attributes:
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                k8s.cluster.name:
                    enabled: false
        azure:
            resource_attributes:
                azure.resourcegroup.name:
                    enabled: true
                azure.vm.name:
                    enabled: true
                azure.vm.scaleset.name:
                    enabled: true
                azure.vm.size:
                    enabled: true
                cloud.account.id:
                    enabled: true
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                cloud.region:
                    enabled: true
                host.id:
                    enabled: true
                host.name:
                    enabled: true
        compression: ""
        consul:
            address: ""
            datacenter: ""
            namespace: ""
            resource_attributes:
                cloud.region:
                    enabled: true
                host.id:
                    enabled: true
                host.name:
                    enabled: true
            token_file: ""
        detectors:
            - eks
            - env
            - ec2
        disable_keep_alives: false
        docker:
            resource_attributes:
                host.name:
                    enabled: true
                os.type:
                    enabled: true
        ec2:
            resource_attributes:
                cloud.account.id:
                    enabled: true
                cloud.availability_zone:
                    enabled: true
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                cloud.region:
                    enabled: true
                host.id:
                    enabled: true
                host.image.id:
                    enabled: true
                host.name:
                    enabled: true
                host.type:
                    enabled: true
            tags:
                - ^kubernetes.io/cluster/.*$
                - ^aws:autoscaling:groupName
        ecs:
            resource_attributes:
                aws.ecs.cluster.arn:
                    enabled: true
                aws.ecs.launchtype:
                    enabled: true
                aws.ecs.task.arn:
                    enabled: true
                aws.ecs.task.family:
                    enabled: true
                aws.ecs.task.id:
                    enabled: true
                aws.ecs.task.revision:
                    enabled: true
                aws.log.group.arns:
                    enabled: true
                aws.log.group.names:
                    enabled: true
                aws.log.stream.arns:
                    enabled: true
                aws.log.stream.names:
                    enabled: true
                cloud.account.id:
                    enabled: true
                cloud.availability_zone:
                    enabled: true
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                cloud.region:
                    enabled: true
        eks:
            resource_attributes:
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                k8s.cluster.name:
                    enabled: false
        elasticbeanstalk:
            resource_attributes:
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                deployment.environment:
                    enabled: true
                service.instance.id:
                    enabled: true
                service.version:
                    enabled: true
        endpoint: ""
        gcp:
            resource_attributes:
                cloud.account.id:
                    enabled: true
                cloud.availability_zone:
                    enabled: true
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                cloud.region:
                    enabled: true
                faas.id:
                    enabled: true
                faas.instance:
                    enabled: true
                faas.name:
                    enabled: true
                faas.version:
                    enabled: true
                gcp.cloud_run.job.execution:
                    enabled: true
                gcp.cloud_run.job.task_index:
                    enabled: true
                gcp.gce.instance.hostname:
                    enabled: false
                gcp.gce.instance.name:
                    enabled: false
                host.id:
                    enabled: true
                host.name:
                    enabled: true
                host.type:
                    enabled: true
                k8s.cluster.name:
                    enabled: true
        heroku:
            resource_attributes:
                cloud.provider:
                    enabled: true
                heroku.app.id:
                    enabled: true
                heroku.dyno.id:
                    enabled: true
                heroku.release.commit:
                    enabled: true
                heroku.release.creation_timestamp:
                    enabled: true
                service.instance.id:
                    enabled: true
                service.name:
                    enabled: true
                service.version:
                    enabled: true
        http2_ping_timeout: 0s
        http2_read_idle_timeout: 0s
        idle_conn_timeout: 1m30s
        k8snode:
            auth_type: serviceAccount
            context: ""
            kube_config_path: ""
            node_from_env_var: ""
            resource_attributes:
                k8s.node.name:
                    enabled: true
                k8s.node.uid:
                    enabled: true
        lambda:
            resource_attributes:
                aws.log.group.names:
                    enabled: true
                aws.log.stream.names:
                    enabled: true
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                cloud.region:
                    enabled: true
                faas.instance:
                    enabled: true
                faas.max_memory:
                    enabled: true
                faas.name:
                    enabled: true
                faas.version:
                    enabled: true
        max_idle_conns: 100
        openshift:
            address: ""
            resource_attributes:
                cloud.platform:
                    enabled: true
                cloud.provider:
                    enabled: true
                cloud.region:
                    enabled: true
                k8s.cluster.name:
                    enabled: true
            tls:
                ca_file: ""
                cert_file: ""
                include_system_ca_certs_pool: false
                insecure: false
                insecure_skip_verify: false
                key_file: ""
                max_version: ""
                min_version: ""
                reload_interval: 0s
                server_name_override: ""
            token: ""
        override: true
        proxy_url: ""
        read_buffer_size: 0
        system:
            resource_attributes:
                host.arch:
                    enabled: false
                host.cpu.cache.l2.size:
                    enabled: false
                host.cpu.family:
                    enabled: false
                host.cpu.model.id:
                    enabled: false
                host.cpu.model.name:
                    enabled: false
                host.cpu.stepping:
                    enabled: false
                host.cpu.vendor.id:
                    enabled: false
                host.id:
                    enabled: false
                host.ip:
                    enabled: false
                host.mac:
                    enabled: false
                host.name:
                    enabled: true
                os.description:
                    enabled: false
                os.type:
                    enabled: true
        timeout: 2s
        tls:
            ca_file: ""
            cert_file: ""
            include_system_ca_certs_pool: false
            insecure: false
            insecure_skip_verify: false
            key_file: ""
            max_version: ""
            min_version: ""
            reload_interval: 0s
            server_name_override: ""
        write_buffer_size: 0
receivers:
    otlp/application_signals:
        protocols:
            grpc:
                dialer:
                    timeout: 0s
                endpoint: 0.0.0.0:4315
                include_metadata: false
                max_concurrent_streams: 0
                max_recv_msg_size_mib: 0
                read_buffer_size: 524288
                transport: tcp
                write_buffer_size: 0
            http:
                endpoint: 0.0.0.0:4316
                include_metadata: false
                logs_url_path: /v1/logs
                max_request_body_size: 0
                metrics_url_path: /v1/metrics
                traces_url_path: /v1/traces
service:
    extensions:
        - awsproxy/application_signals
        - agenthealth/traces
        - agenthealth/logs
    pipelines:
        metrics/application_signals:
            exporters:
                - awsemf/application_signals
            processors:
                - resourcedetection
                - awsapplicationsignals
            receivers:
                - otlp/application_signals
        traces/application_signals:
            exporters:
                - awsxray/application_signals
            processors:
                - resourcedetection
                - awsapplicationsignals
            receivers:
                - otlp/application_signals
    telemetry:
        logs:
            development: false
            disable_caller: false
            disable_stacktrace: false
            encoding: console
            level: info
            sampling:
                enabled: true
                initial: 2
                thereafter: 500
                tick: 10s
        metrics:
            address: ""
            level: None
        traces: {}

Discussion