Goアプリケーションに対する非侵襲的なコードインジェクション

> 本記事は[こちらのブログ](https://www.alibabacloud.com/blog/non-intrusive-code-injection-for-go-applications_601890?spm=a2c65.11461433.0.0.4ee35355MlSWeA)を参考にしています。
> 翻訳にはアリババクラウドの[ModelStudio](https://www.alibabacloud.com/en/product/modelstudio)([Qwen](https://www.alibabacloud.com/en/solutions/generative-ai/qwen))を使用しております。

<div class="wrap-main-left-article markdown-body">
![1](https://yqintl.alicdn.com/ef755bf1e0410f45841377ce42e1dd063c2e59c4.png)

著者: Qingfeng, Guqi, Musi, Ruman

この記事では、開発者がこの革新的な技術を理解し、適用することで、Golangアプリケーションの監視とサービスガバナンス機能を改善するための深堀り的な技術分析と実践的な指導を提供します。以下では、いくつかの実践的なケースを通じて、この技術を異なるシナリオでどのように適用するかをさらに示し、より実践的な洞察を提供します。

## 1. 背景
Go言語の開発プロセスにおいて、優れたパフォーマンスと効率的なコーディング能力で知られていますが、アプリケーション監視とサービスガバナンスにおけるコストと技術的な課題は依然として大きいです。従来のソリューションでは、開発者が手動でソースコードを調整する必要があり、これにより作業量が増え、既存のアーキテクチャにも影響を与え、シームレスな統合が非常に難しくなります。特に複雑な異種システムでは、包括的で詳細な監視とサービス最適化を達成することは、専門家の経験を必要とする時間のかかるタスクです。このような状況下で、侵入性を最小限に抑えつつ運用効率を向上させる方法論を求めることは、業界全体の共通の目標となっています。

この問題を解決するために、アリババクラウドのCompiler Team、ARMS Team、およびMSE Teamが協力して、Go向けのコンパイル時自動インストルメンテーション技術を開発し、オープンソース化しました[1]。この技術は非侵入的な特徴を持ち、Golangアプリケーションに対してJavaと同等の監視機能を提供します。開発者は既存のコードを一切修正する必要がなく、新しいコンパイルコマンドでgo buildを置き換えるだけで、Goアプリケーションの包括的な監視とガバナンスを実現できます。オープンソース版では16の主流のオープンソースフレームワーク（商用版では38）をサポートしています。また、ユーザーの多様なニーズに対応するため、特にサポートされていないフレームワークを使用しているユーザーや高度なカスタマイズが必要なユーザーのために、モジュラーなインストルメンテーション拡張機能を導入しました。ユーザーは簡単なJSON設定を通じて、任意の対象関数にカスタムコードをゼロ侵入で注入できます。元のコードリポジトリのコードを変更する必要はありません。モジュラーなインストルメンテーション拡張を通じてコード注入が行われ、より細粒度な制御、監視、ガバナンス、およびセキュリティを可能にします。

## 2. モジュラー拡張の原理
通常、go buildコマンドは、ソースコード解析、型チェック、意味解析、コンパイル最適化、コード生成、リンキングという6つの主要なステップを経てGoアプリケーションをコンパイルします。しかし、自動インストルメンテーションを使用すると、これらのステップの前に前処理とインストルメンテーションという2つの追加ステップが挿入されます。

![2](https://yqintl.alicdn.com/c0dd3427d2c1ee8dc9d098a537d2a10aa19af0c1.png)

### 2.1 前処理
この段階では、ツールは最初にユーザー定義のrule.json設定ファイルを読み込みます。このファイルには、カスタムフックコードのインストルメンテーションが必要なフレームワークまたは標準ライブラリのバージョンが詳細に記載されています。rule.json設定ファイルの内容は完全にユーザーによって制御されます。典型的な例は以下の通りです：json
[
  {
    "ImportPath": "google.golang.org/grpc",
    "Function": "NewClient",
    "OnEnter": "grpcNewClientOnEnter",
    "OnExit": "grpcNewClientOnExit",
    "Path": "/path/to/my/code"
  }
]

この設定は、ユーザーがgoogle.golang.org/grpcライブラリのNewClient関数のエントリポイントとエグジットポイントにそれぞれgrpcNewClientOnEnterとgrpcNewClientOnExitというコードセグメントを挿入したいことを示しています。挿入するこれらの2つの関数のコードはローカルパス/path/to/my/codeにあります。次に、ツールはプロジェクトのサードパーティライブラリ依存関係を分析し、rule.jsonで定義されたカスタムインストルメンテーションルールと照合し、これらのルールに必要な追加の依存関係も事前に設定します。すべての前処理が完了したら、ツールは通常のコンパイルプロセスをインターセプトし、各パッケージのコンパイル前にインストルメンテーションフェーズを追加します。

### 2.2 インストルメンテーション
この段階では、ツールはrule.jsonの設定に基づいて、NewClientなどの対象関数にトランポリンコードを挿入します。トランポリンコードの主な機能は、例外処理とコンテキストの埋め込みを行う論理的な跳躍台であり、最終的にはユーザー定義のgrpcNewClientOnEnterとgrpcNewClientOnExit関数にジャンプして監視データの収集やサービストラフィックのガバナンスを完了します。トランポリンコードはパフォーマンスに重要な役割を果たすため、AST（抽象構文木）レベルで一連の最適化を行い、そのオーバーヘッドを最小限に抑えるようにしています。最適化部分に興味のある読者は、プロジェクトのソースコードを参照できますが、ここでは詳しく説明しません。

![3](https://yqintl.alicdn.com/20425c3abe7409695d5a1af187ff19c9649ff494.png)

上記の手順を通じて、ツールはユーザーが指定したコードロジックを効果的に挿入しながら、コードの機能の完全性を確保します。その後、ツールは必要なコンパイルパラメータを修正し、通常のコンパイルプロセスを実行して最終的なアプリケーションを生成します。

## 3. 例
上記の原理を説明した後、いくつかの例を通じてGoの自動インストルメンテーションのモジュラー拡張の使用方法を示します。

### 3.1 HTTPリクエストヘッダーの記録
net/httpを例に取り、多くのユーザーが問題の特定のためにリクエストパラメータとボディに興味を持っています。ここでは、カスタムインストルメンテーションを使用して、リクエストヘッダーと返信ヘッダーを取得する方法を説明します。

#### ステップ1: フックフォルダを作成し、`go mod init hook`で初期化し、次に以下のようなhook.goコードを挿入する：go
package hook

import (
  "encoding/json"
  "fmt"
  "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
  "net/http"
)

// 注: 挿入されるコードの最初のパラメータはapi.CallContextでなければなりません。以降のパラメータは対象関数のパラメータと一致しなければなりません。
func httpClientEnterHook(call api.CallContext, t *http.Transport, req *http.Request) {
  header, _ := json.Marshal(req.Header)
  fmt.Println("request header is ", string(header))
}

// 注: 挿入されるコードの最初のパラメータはapi.CallContextでなければなりません。以降のパラメータは対象関数の戻り値と一致しなければなりません。
func httpClientExitHook(call api.CallContext, res *http.Response, err error) {
  header, _ := json.Marshal(res.Header)
  fmt.Println("response header is ", string(header))
}

#### ステップ2: 以下のconf.json設定を書き、ツールにフックコードをどこに挿入するか指示する：json
[
  {
    "ImportPath": "net/http",
   
# ステップ1: hookフォルダを作成し、go mod init hookで初期化し、次に注入するhook.goコードを追加しますgo
package hook

import (
  "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
)

func partition(arr []int, low, high int) (int, int) {
  if arr[low] > arr[high] {
    arr[low], arr[high] = arr[high], arr[low]
  }
  lp := low + 1
  g := high - 1
  k := low + 1
  p := arr[low]
  q := arr[high]
  for k <= g {
    if arr[k] < p {
      arr[k], arr[lp] = arr[lp], arr[k]
      lp++
    } else if arr[k] >= q {
      for arr[g] > q && k < g {
        g--
      }
      arr[k], arr[g] = arr[g], arr[k]
      g--
      if arr[k] < p {
        arr[k], arr[lp] = arr[lp], arr[k]
        lp++
      }
    }
    k++
  }
  lp--
  g++
  arr[low], arr[lp] = arr[lp], arr[low]
  arr[high], arr[g] = arr[g], arr[high]
  return lp, g
}

func dualPivotQuickSort(arr []int, low, high int) {
  if low < high {
    lp, rp := partition(arr, low, high)
    dualPivotQuickSort(arr, low, lp-1)
    dualPivotQuickSort(arr, lp+1, rp-1)
    dualPivotQuickSort(arr, rp+1, high)
  }
}

func sortOnEnter(call api.CallContext, arr []int) {
  // デュアルピボットクイックソートを使用
  dualPivotQuickSort(arr, 0, len(arr)-1)
  // 元のソートアルゴリズムをスキップ
  call.SetSkipCall(true)
}

# ステップ2: 次のconf.json設定を書くことで、ツールにsort.Intsにhookコードを注入するように指示しますjson
[
  {
    "ImportPath": "sort",
    "Function": "Ints",
    "OnEnter": "sortOnEnter",
    "Path": "/path/to/hook"  // ローカルのhookコードのパスに変更
  }
]

# ステップ3: テストデモを書く。フォルダを作成し、go mod init demoで初期化し、main.goを追加しますgo
package main

import (
  "fmt"
  "sort"
)

func main() {
  arr := []int{6, 3, 7, 9, 4, 4}
  sort.Ints(arr)
  fmt.Printf("== %v\n", arr)
}

# ステップ4: デモディレクトリに切り替えて、otelbuildツールを使用してプログラムをコンパイルおよび実行し、デュアルピボットクイックソートの効果を確認しますsh
$ ./otelbuild -rule=conf.json -- main.go
$ ./main
== [3 4 4 6 7 9]

## 3.3 SQLコードインジェクションの防止
SQLコードインジェクションを防ぐために、`database/sql::(*DB).Query()`に追加のコードを注入し、SQLステートメント内の潜在的なインジェクションリスクをチェックし、すぐにインターセプトすることができます。

### ステップ1: hookフォルダを作成し、go mod init hookで初期化し、次に注入するhook.goコードを追加しますgo
package hook

import (
  "database/sql"
  "errors"
  "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
  "log"
  "strings"
)

func checkSqlInjection(query string) error {
  patterns := []string{"--", ";", "/*", " or ", " and ", ""}
  for _, pattern := range patterns {
    if strings.Contains(strings.ToLower(query), pattern) {
      return errors.New("potential SQL injection detected")
    }
  }
  return nil
}

func sqlQueryOnEnter(call api.CallContext, db *sql.DB, query string, args ...interface{}) {
  if err := checkSqlInjection(query); err != nil {
    log.Fatalf("sqlQueryOnEnter %v", err)
  }
}

### ステップ2: 次のconf.json設定を書くことで、ツールに`database/sql::(*DB).Query()`にhookコードを注入するように指示しますjson
[
  {
    "ImportPath": "database/sql",
    "Function": "Query",
    "ReceiverType": "*DB",
    "OnEnter": "sqlQueryOnEnter",
    "Path": "/path/to/hook"  // ローカルのhookコードのパスに変更
  }
]

### ステップ3: テストデモを書く。フォルダを作成し、go mod init demoで初期化し、main.goを追加しますgo
package main

import (
  "context"
  "database/sql"
  "fmt"
  _ "github.com/go-sql-driver/mysql"
  "os"
  "time"
)

func main() {
  mysqlDSN := "test:test@tcp(127.0.0.1:3306)/test"
  db, _ := sql.Open("mysql", mysqlDSN)

  db.ExecContext(context.Background(), `CREATE TABLE IF NOT EXISTS usersx (id char(255), name VARCHAR(255), age INTEGER)`)
  db.ExecContext(context.Background(), `INSERT INTO usersx (id, name, age) VALUE (?, ?, ?)`, 0, "foo", 10)

  // SQLステートメントに悪意のあるコードを注入して、データベーステーブル内のすべての情報を取得
  maliciousAnd := "foo AND 1 = 1"
  injectedSql := fmt.Sprintf("SELECT * FROM usersx WHERE id = 0 AND name = %s", maliciousAnd)
  db.Query(injectedSql)
}

### ステップ4: デモディレクトリに切り替えて、otelbuildツールを使用してプログラムをコンパイルおよび実行し、SQLインジェクション防止の効果を確認しますsh
$ ./otelbuild -rule=conf.json -- main.go
$ docker run -d -p 3306:3306 -p 33060:33060 -e MYSQL_USER=test -e MYSQL_PASSWORD=test -e MYSQL_DATABASE=test -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:8.0.36
$ ./main

otelbuildツールでコンパイルされたバイナリファイルが、潜在的なSQLインジェクション攻撃を検出し、対応するログを出力することが観察されます。sh
2024/11/04 21:12:47 sqlQueryOnEnter potential SQL injection detected

この例は以下のリンクで見つけることができます:
<a href="https://github.com/alibaba/opentelemetry-go-auto-instrumentation/tree/main/example/extension/sqlinject" target="_blank">https://github.com/alibaba/opentelemetry-go-auto-instrumentation/tree/main/example/extension/sqlinject</a>

## 3.4 リクエストのトラフィック保護を有効にする
sentinel-golangに基づいてgrpc-goのunaryリクエストにトラフィック保護を追加する計画がある場合、自動インスツルメンテーションを通じてgrpcクライアントにミドルウェアを注入することもできます。

### ステップ1: hookフォルダを作成し、go mod init hookで初期化し、次に注入するhook.goコードを追加しますgo
package hook

import (
  "context"
  "google.golang.org/grpc"
  "github.com/sentinel-golang/api"
  "github.com/sentinel-golang/core/base"
  "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
)

// gRPCクライアントのエントリポイントでトラフィック保護ミドルウェアを追加
func newClientOnEnter(call pkgapi.CallContext, target string, opts ...grpc.DialOption) {
  opts = append(opts, grpc.WithChainUnaryInterceptor(unaryClientInterceptor))
}

// sentinel-golangに基づくトラフィック保護ミドルウェア
func unaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
  entry, blockErr := sentinel.Entry(
    method,
    sentinel.WithResourceType(base.ResTypeRPC),
    sentinel.WithTrafficType(base.Outbound),
  )
  defer func() {
    if entry !=
# Golangのコンパイル時自動インスツルメンテーションgo
package main

import (
    "context"
    "fmt"
    "google.golang.org/grpc"
    pb "path/to/your/protobuf" // あなたのprotobufファイルのパスに置き換えてください
)

func main() {
    // gRPCサーバーに接続
    conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
    client := pb.NewYourServiceClient(conn)

    // gRPCリクエストを送信
    response, _ := client.YourMethod(context.Background(), &pb.YourRequest{})
    fmt.Println("Response: ", response)
}

## ステップ4: デモディレクトリに切り替えて、otelbuildツールを使用してプログラムをコンパイルおよび実行し、効果を確認します。sh
$ ./otelbuild -rule=conf.json -- main.go
$ ./main

gRPC-goストリームリクエストに保護ルールを追加したい場合も、アプローチは同様です。また、カナリーリリースのためにリクエストにカナリールーティング、タグベースのルーティング、パーセントベースのルーティングなどの機能を有効にしたい場合は、フレームワークのロードバランサーを必要に応じて強化することもできます。これにより、高度な自律性と拡張性が提供されます。

## 4. まとめと展望
Golangのコンパイル時自動インスツルメンテーションは、マイクロサービス監視における煩雑な手動インスツルメンテーションの問題を解決し、アリババクラウドのパブリッククラウド上で商用展開され、顧客に強力な監視機能を提供しています。この技術は当初、既存のコードを変更せずにユーザーが簡単に監視コードを挿入できるように設計され、アプリケーションのパフォーマンスをリアルタイムで監視および分析できるようにすることを目的としていました。しかし、この技術の応用範囲は予想以上に広がり、サービスガバナンス、コードレビュー、アプリケーションセキュリティ、コードデバッグなどにも活用されています。また、多くの未開拓の領域でも可能性を示しています。

私たちはこの革新的なソリューションをオープンソース化し、OpenTelemetryコミュニティに寄与することを決定しました[4]。現在、寄与に関する合意に達しており、将来的には私たちのコードがOpenTelemetryコミュニティのリポジトリに移行される予定です。ソリューションのオープンソース化は、技術的な共有と改善を促進するとともに、コミュニティの助けを借りてより多くの分野での潜在能力を継続的に探求するのに役立ちます。最後に、私たちの商用製品[5][6]を試していただき、DingTalkグループ（オープンソースグループ: 102565007776, 商用グループ: 35568145）に参加して、共にGolangアプリケーションの監視とサービスガバナンス能力を向上させることをお願いいたします。皆様のご協力により、Golang開発者コミュニティにより大きなクラウドネイティブ体験を提供できると信じています。

## 参考文献
[0] OpenAnolisのプログラミング言語とコンパイラSIG: <a href="https://openanolis.cn/sig/java" target="_blank">https://openanolis.cn/sig/java</a><br>
[1] Go自動インスツルメンテーションオープンソースプロジェクト: <a href="https://github.com/alibaba/opentelemetry-go-auto-instrumentation" target="_blank">https://github.com/alibaba/opentelemetry-go-auto-instrumentation</a><br>
[2] OpenTelemetryにおけるGolangアプリケーションの非侵襲型インスツルメンテーション技術: <a href="https://www.alibabacloud.com/blog/non-intrusive-instrumentation-technology-for-golang-applications-in-opentelemetry_601665" target="_blank">https://www.alibabacloud.com/blog/non-intrusive-instrumentation-technology-for-golang-applications-in-opentelemetry_601665</a><br>
[3] パターン破壊クイックソートアルゴリズム論文: <a href="https://arxiv.org/pdf/2106.05123" target="_blank">https://arxiv.org/pdf/2106.05123</a><br>
[4] OpenTelemetryコミュニティでのプロジェクト寄与に関する議論: <a href="https://github.com/open-telemetry/community/issues/1961" target="_blank">https://github.com/open-telemetry/community/issues/1961</a><br>
[5] アリババクラウド ARMS Go Agent コマーシャル版: <a href="https://www.alibabacloud.com/help/en/arms/tracing-analysis/monitor-go-applications/" target="_blank">https://www.alibabacloud.com/help/en/arms/tracing-analysis/monitor-go-applications/</a><br>
[6] アリババクラウド MSE Go Agent コマーシャル版: <a href="https://www.alibabacloud.com/help/en/mse/getting-started/ack-microservice-application-access-mse-governance-center-golang-version" target="_blank">https://www.alibabacloud.com/help/en/mse/getting-started/ack-microservice-application-access-mse-governance-center-golang-version</a><br>