AWS SDK の裏側を見てみよう !

~AWS SDK for Go (v1) のコードとともに (前編)

2022-03-02
デベロッパーのためのクラウド活用方法

王 力捷

皆さん、こんにちは !
ソリューションアーキテクト (SA) の王 (@elecho1_t) です。

普段みなさんどのような方法で AWS サービスを利用していますでしょうか ?
ブラウザ上でマネジメントコンソール経由で操作したり、コマンドラインから AWS CLI 経由で使ったり、開発しているアプリケーションでライブラリ経由で使ったりと、様々な使い方をされていると思います。

このうち、3 つ目の、様々な開発言語やプラットフォームのアプリケーションから AWS サービスを利用するためのツール群は AWS SDK と呼ばれています。

現在 (2022 年 1 月 15 日時点) では、右図のように Python, Java, C++, Go などの主要な開発言語向けの AWS SDK が提供されています。

またこれらの言語に加えて、昨年末の AWS re:Invent 2021 では Swift / Kotlin / Rust 向けの SDK がデベロッパープレビューとして発表されました。

クリックすると拡大します

それでは、AWS SDK が裏側でどのような処理をしているのか意識されたことはありますでしょうか ?

実はこの AWS SDK のソースコードは GitHub 上で公開されています。(こちらのページ で、GitHub 上で公開されている AWS SDK の一覧を確認することができます。) そのため、こちらのソースコードを追っていけば裏側の処理を具体的に知ることができます。

この記事では、Go 言語向けの AWS SDK for Go を例にしながら、実際のソースコードとともに AWS SDK の裏側の仕組みについてご紹介していきたいと思います。

その中でも前編ということで、本記事では AWS SDK の基本的な使い方から、Session や Service Client を用いて AWS サービス利用の設定を行う際の AWS SDK for Go の裏側の動きをご紹介していきます。

このクラウドレシピ (ハンズオン記事) を無料でお試しいただけます »

毎月提供されるクラウドレシピのアップデート情報とともに、クレジットコードを受け取ることができます。 


1. 本記事のゴール

本記事のゴールは、

  • AWSサービスを利用するための API リクエスト送信の仕組みを知っていただく
  • ソースコードを読むメリット・楽しさを少しでも知っていただく
  • AWS SDK for Go (Version1) において Session, Service Client の設定を行う際の、裏側の動作の雰囲気を知っていただく

こととなります。

一方、本記事では以下については扱いませんのでご了承ください。

  • Go 言語の文法
  • AWS SDK の具体的な使い方
  • AWS SDK for Go (Version1) 以外の AWS SDK の実装 (対象言語・対象プラットフォーム・バージョンによって AWS SDK 内の処理構造が異なる可能性があります。)

全て読むのは難しいと思われた場合でも、気負わずに一部でもかいつまんで読んでいただき、少しでもご参考になれば幸いです。実際に AWS SDK を利用して開発を行う際には、ぜひまず デベロッパーガイドAPI リファレンスガイド をご参照ください。


2. AWS SDKとは

ソースコードを読んで具体的な実装を見ていく前に、AWS SDK について簡単にご紹介したいと思います。

AWS SDK を使うと、お使いの言語で AWS リソースの操作を行うことができます。例えば、Amazon DynamoDB のテーブルから item を取得するためのサンプルコードは以下になります。(こちらのソースコード および こちらのドキュメント からの抜粋となります。)

このサンプルコードでは、Amazon DynamoDB にある Movies テーブルから、Year が 2015 かつ Title が "The Big New Movie" である item の情報を取得して表示するということを行っています。

package main

import ( 
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
    
    "fmt"
    "log" 
)

// Create struct to hold info about new item
type Item struct {
    Year   int
    Title  string
    Plot   string
    Rating float64
}

func main() {
    // Initialize a session that the SDK will use to load
    // credentials from the shared credentials file ~/.aws/credentials
    // and region from the shared configuration file ~/.aws/config.
    sess := session.Must(session.NewSessionWithOptions(session.Options{    // ...(A)
        SharedConfigState: session.SharedConfigEnable,
    }))

    // Create DynamoDB client
    svc := dynamodb.New(sess)    // ...(B)
    
    
    tableName := "Movies"
    movieName := "The Big New Movie"
    movieYear := "2015"

    result, err := svc.GetItem(&dynamodb.GetItemInput{    // ...(C)
        TableName: aws.String(tableName),
        Key: map[string]*dynamodb.AttributeValue{
            "Year": {
                N: aws.String(movieYear),
            },
            "Title": {
                S: aws.String(movieName),
            },
        },
    })
    if err != nil {
        log.Fatalf("Got error calling GetItem: %s", err)
    }
    
    if result.Item == nil {
        msg := "Could not find '" + *title + "'"
        return nil, errors.New(msg)
    }
        
    item := Item{}

    err = dynamodbattribute.UnmarshalMap(result.Item, &item)
    if err != nil {
        panic(fmt.Sprintf("Failed to unmarshal Record, %v", err))
    }

    fmt.Println("Found item:")
    fmt.Println("Year:  ", item.Year)
    fmt.Println("Title: ", item.Title)
    fmt.Println("Plot:  ", item.Plot)
    fmt.Println("Rating:", item.Rating)
}

データベース情報の記述や取得後のデータ整形のためのコードが大部分を占めていますが、このサンプルコードで AWS サービスとやり取りする上で AWS SDK が重要な役割を果たしているのは以下の 3 文です。

// (A) Session: 基本的な共通設定(25行目)
    sess := session.Must(session.NewSessionWithOptions(session.Options{
        SharedConfigState: session.SharedConfigEnable,
    }))
 // (B) Service client: 単独のAWSサービスを利用するための設定(今回はAmazon DynamoDB)(30行目)
    svc := dynamodb.New(sess)
    // (C) Request: 具体的なAWSサービス利用(今回はAmazon DynamoDBのitem取得)(37行目)
    result, err := svc.GetItem(&dynamodb.GetItemInput{
        TableName: aws.String(tableName),
        Key: map[string]*dynamodb.AttributeValue{
            "Year": {
                N: aws.String(movieYear),
            },
            "Title": {
                S: aws.String(movieName),
            },
        },
    })

AWS SDK for Go には Session, Service client, Request という 3 つの概念があります。

  • Session : AWS サービスと接続するための基本的な共通設定です。例えば、利用するリージョンや AWS での認証方法を設定できます。
  • Service client : 単独の AWS サービスを利用するための設定です。作成時にリクエストの送信先エンドポイント (詳細は後述) の設定なども内部で行います。なお、必要に応じて Session で設定した内容を上書きすることもできます。
  • Request : AWS サービスに対して具体的な操作を指示する API リクエストの生成・送信などを管理するものです。Service client に紐付いたメソッドを実行することで (上の例では GetItem())、内部で API リクエスト周りを管理するデータが作成され、実際に API リクエスト送信・レスポンス受信などが行われます。

各概念の説明は、こちらのドキュメント にも記載されています。

なお、3 文目では GetItem() という操作をしたいだけのはずなのに、いきなり Request という概念が出てきました。なぜかというと、AWS SDK で指示した具体的な操作はすべて API リクエストの形で AWS サービスに送られるからです。

ここで一旦 API リクエストを使って AWS サービスを操作する仕組みを見てみましょう。


3. API リクエストによる AWS サービスの操作

AWS SDK を使用すると、基本的に AWS サービスへの API リクエストの詳細を意識することはありません。AWS SDK が AWS サービスとの接続部分を隠蔽してくれます。そのため、ユーザーの使用感はこちらの図のようになっているはずです。

しかし、その裏側では、右図のように AWS のサービスエンドポイントに対し API リクエストを送ることで AWS サービスに指示を伝えています。
AWS SDK では、裏側でこのAPIリクエスト周りの処理を行っています。

例えば、

  • API リクエストを送るサービスエンドポイント特定
    (例:dynamodb.ap-northeast-1.amazonaws.com
  • API リクエストボディの作成
  • AWS 認証情報による署名付与

などの処理が行われます。

ちなみに、API リクエスト・レスポンスの詳細は AWS ドキュメントの API Reference で確認できます。(例えば、Amazon DynamoDB の GetItem 操作のリクエスト・レスポンスの形式や例は こちら から確認できます。)

なお、前の章で AWS SDK の利用例をお見せした際、Session と Service client はどちらも設定を行うのになぜ分かれているのか疑問に思ったかもしれません。その理由の 1 つは、各リージョンの AWS サービスごとにリクエストを送るエンドポイントが異なるためです。

例えば、dynamodb.ap-northeast-1.amazonaws.com というエンドポイントは、東京リージョン (ap-northeast-1) にある Amazon DynamoDB へ API リクエストを送るためのエンドポイントです。

そのため、Session では基本的な共通設定だけを持っておき、API リクエストを送るサービスエンドポイントの情報は特定の AWS サービスの操作に特化した Service client が持つという仕組みになっています。(他にも、AWS サービスごとに Service client を分けないとメソッドが増えすぎてしまうという理由もあるかと思います。)

ちなみに、AWS サービスごとのエンドポイント一覧は こちらのページ から確認できます。例えば、こちらのドキュメント によると東京リージョン (ap-northeast-1) の Amazon DynamoDB の item に直接アクセスしたい場合、dynamodb.ap-northeast-1.amazonaws.com に HTTP もしくは HTTPS を用いて API リクエストを送ることになります。


4. ソースコードリーディング : 事前説明

前章までで、AWS SDK の基本的な使用方法や、API リクエストによる AWS サービス操作の仕組みについて説明しました。

ここからは、AWS SDK が裏側でどのような処理をしているかソースコードとともに見ていきましょう。この章では、実際にソースコードリーディングを始める前に前提事項や Tips をご紹介したいと思います。

4-1. ソースコードリーディングをするメリット

外部 OSS ライブラリのソースコードリーディングをするメリットを下記の通り簡単にまとめてみました。

  • ドキュメントに記載されていないもしくは記載に気付かなかった細かな仕様を知ることができる。
  • デフォルト設定をたどることができる。
  • 予想外の動作をした際に原因を探ることができる。
  • 利用方法を最適化できる。(例 : HTTPClient は Session で作成されるので、Session を作りすぎるのは良くない。)
  • 動作を理解することで、OSS ライブラリを改良し、プルリクエストなどを通して OSS ライブラリへ貢献できるようになる。

また、利用に限らずエンジニアのスキルアップとしての観点では、下記のメリットが考えられます。

  • コード構造を参考にすることができる。
  • 自プロジェクトで他の人のソースコードを読む際の練習になる。
  • プログラミング言語の今まで知らなかった文法・機能を知ることができる。

初めはかなり時間がかかってしまうのが難点ですが、気になるメソッドに絞ったりインターネット上の公開コンテンツを参考にしながらソースコードを読んでみると、意外と楽しいかも知れないですよ !

4-2. ソースコードリーディングのコツ

読み方のコツ

まず、詳しく知りたい部分に焦点を絞って読んでみましょう。起点を 1 つのメソッドに絞ってみると、目的がはっきりして迷子になりにくくなります。それでも、AWS SDK のような比較的規模の大きいアプリケーションになると、コードも複雑になっている部分も出てきます。

例えば、関数やメソッドの中身がそもそも長かったり、1 つの機能を実現するために他の関数やメソッドを何重にも呼び出していたりして、コードをたどっていくうちにいま何のためにどこを見ているのか混乱しがちです。その際は、関数やメソッドの入出力、すなわち引数と返り値に注目すると見通しが良くなります

入出力に注目しながら関数やメソッドの呼び出し過程をメモしていくと、コードを追いやすくなります。

デバッグツールの利用

デバッグツールを利用してブレークポイントやステップ実行を組み合わせると、関数・メソッドの実行順をより簡単にかつ正確に追いやすくなります。Go 言語の場合、delvegdb を利用したデバッグが可能です。また、IDE と統合すると便利に使うことができます。

ただし、デバッグツールを利用するには、ソースコードをデバッグ用のオプションを付けてビルドし直したりとひと手間かかることもあります。

GitHub の利用

わざわざソースコードをダウンロードするのがめんどくさい、デバッグツールや IDE の使い方がよく分からない場合は、ブラウザで開いた GitHub 上でソースコードをたどることもおすすめです。

実際に GitHub 上で型の定義や呼び出し先の関数やメソッドの実装をたどる手順は下記の通りとなります。

まず、型名や関数名、メソッド名にマウスカーソルをのせるとハイライトされ、カーソルの形がクリックできそうな形に変わります。

クリックすると拡大します

クリックしてみると、定義がサジェストされます。

クリックすると拡大します

こちらの図の Config() メソッドのように複数候補がある場合もあります。その場合は、あてはまる定義を選びましょう。(こちらの図の例では defaults.Config() を見たいので、3 つ目の aws/defaults/defaults.go を選びます。)

クリックすると拡大します

そして、選んだ詳しく見たい定義をクリックすると、定義箇所に移動することができます。(右クリック→「新しいタブで開く」などによって別タブで開くことも可能です。)

クリックすると拡大します

変数名などの場合は、サジェストが出ない場合もあります。その場合は、ページ最上部の検索ボックスでレポジトリ内検索などを行い、定義箇所や使用箇所を探しましょう。

なお、他にもソースコードリーディングのコツは各所で論じられています。気になる方はぜひ調べてみてください。

4-3. なぜ AWS SDK for GoのVersion 2 ではなく Version 1 を扱うの ?

2021/1/19 に、AWS SDK for Go の Version 2 が一般公開 (GA) となりました。Version 1 と比べて、ユーザーによる設定の簡素化や、CPU 時間・メモリ利用量の削減による性能向上が行われています。(リリースブログはこちら)

一方、本記事ではVersion 2ではなく AWS SDK for Go Version 1 (v1.42.35) を扱います。その理由として、Version 2 では設計が洗練された分内部実装の抽象化が進んでいるため、具体的な動作イメージをつかんでいただきにくくなっているためです。Version 2 の実装が Version 1 と比べてどのように変わったかはまた別の機会にご紹介できればと考えております。

なお、現在 (2022 年 1 月 15 日時点) のところ、Version 1 と Version 2 はともにフルサポートされており、サポート終了はアナウンスされていません。(サポート状況は こちらのページ で確認できます。) ただ、AWS SDK のメンテナンスポリシー に従うと一般的には新しいバージョンの方が今後のサポート対象期間が長いため、新規に利用を開始する際は Version 2 を利用されることが推奨されます

4-4. ソースコードリーディングをする部分

    // (A) Session(基本的な共通設定)の作成
    sess := session.Must(session.NewSessionWithOptions(...))
    // (B) Service client(単独のAWSサービスを利用するための設定)の作成
    svc := dynamodb.New(sess)
    // (C) Request送信(具体的なAWSサービス利用操作)の実行(次回へ続く)
    result, err := svc.GetItem(...)

AWS SDK を利用する際、基本的に (A) Session 作成、(B) Service client 作成、(C) Request 送信の 3 パートの記述を行います。そのうち、本記事では (A) Session 作成、(B) Service client 作成の裏側の実装を見ていきます。

どのように設定の読み込み・保持を行っているのかについて焦点にあてて見ていこうと思います。(C) Request 送信の裏側は次回以降へご期待ください。

ここからは、実際のコードを載せながら、挙動をご紹介します。


5. ソースコードリーディング:(A) Session の作成

Session 作成の操作の中身を見て行きましょう。

    // (A) Session(基本的な共通設定)の作成
    sess := session.Must(session.NewSessionWithOptions(...))

5-1. 本章で見ていく関数・メソッドの実行順序

├── (1) session.Must()    // Session作成時のエラーハンドリングを行う関数
└── (2) session.NewSessionWithOptions()    // Session作成を呼び出す関数
    ├── (3) session.loadSharedEnvConfig()     // 環境変数の読み取りを呼び出す関数
    │   └── session.envConfigLoad()    // 環境変数の読み取りを行う関数
    └── (4) session.newSession()     // 設定統合・Session作成を行う関数
        ├── (5) defaults.Config()     // デフォルト設定を生成する変数
        └── (6) defaults.Handlers()    // デフォルトハンドラーを生成する変数

5-2. (1) session.Must() 関数

session.Must() 関数 は以下のように定義されています。

func Must(sess *Session, err error) *Session {
    if err != nil {
        panic(err)
    }

    return sess
}

この関数は、Session がエラーなく作成されたことを保証するための wrapper 関数です。もし Session 作成中にエラーが発生した場合、組み込み関数 panic() によりプログラムを強制終了させます。セッション本体は引数で受け取った sess であり、本ケースでは session.NewSessionWithOptions() 関数によって作成されます。

気づき 1

session.Must() 関数を使うと、Session 作成時にエラーが発生した場合プログラムを終了させます。すなわち、session.Must() 関数を利用して Session を作成する前にたくさん他の処理を行っても、Session 作成時にエラーが出てしまうと無駄になってしまいます。そして、例えば環境が変わって AWS 認証情報の保管方法が変わったなど、意外と Session 作成時にエラーが発生することはあります。

そのため、AWS サービスの操作を行う直前ではなく、アプリケーション実行のなるべく早い段階で Session の作成を行うことがおすすめです。具体的には、main() 関数の初めの方で Session を作成する、もしくは Session を main() 関数から出してグローバル変数として初期化する、などのアプローチが考えられます。上記の特性を理解した上で、開発中のアプリケーションに適したアプローチをご採用ください。

5-3. (2) session.NewSessionWithOptions() 関数

session.NewSessionWithOptions() 関数 は以下のように定義されています。

func NewSessionWithOptions(opts Options) (*Session, error) {
    var envCfg envConfig
    var err error
    if opts.SharedConfigState == SharedConfigEnable {
        envCfg, err = loadSharedEnvConfig()
        if err != nil {
            return nil, fmt.Errorf("failed to load shared config, %v", err)
        }
    } else {
        envCfg, err = loadEnvConfig()
        if err != nil {
            return nil, fmt.Errorf("failed to load environment config, %v", err)
        }
    }

    if len(opts.Profile) != 0 {
        envCfg.Profile = opts.Profile
    }

    switch opts.SharedConfigState {
    case SharedConfigDisable:
        envCfg.EnableSharedConfig = false
    case SharedConfigEnable:
        envCfg.EnableSharedConfig = true
    }

    return newSession(opts, envCfg, &opts.Config)
}

まず、最後の return で session.newSession() の結果を返しています。すなわち、Session 本体の作成は session.newSession() で行われると推察できます。そして、session.newSession() を呼び出す際の引数には、もともとの NewSessionWithOptions() の引数である opts に加えて envCfg が指定されています。この envCfg は 2 行目で宣言されており、5 行目の session.loadSharedEnvConfig() 関数もしくは 10 行目の session.loadEnvConfig() 関数によって値が代入されています。

この envCfg は 2 行目で宣言されており、5 行目の session.loadSharedEnvConfig() 関数もしくは 10 行目の session.loadEnvConfig() 関数によって値が代入されています。

それでは、envCfg にはどのような値が入るのか、例として session.loadSharedEnvConfig() 関数を見てみましょう。

5-4. (3) session.loadSharedEnvConfig() 関数

session.loadSharedEnvConfig() 関数 は、以下のように定義されています。

func loadSharedEnvConfig() (envConfig, error) {
    return envConfigLoad(true)
}

func envConfigLoad(enableSharedConfig bool) (envConfig, error) {
    cfg := envConfig{}

    cfg.EnableSharedConfig = enableSharedConfig

    // Static environment credentials
    var creds credentials.Value
    setFromEnvVal(&creds.AccessKeyID, credAccessEnvKey)
    setFromEnvVal(&creds.SecretAccessKey, credSecretEnvKey)
    setFromEnvVal(&creds.SessionToken, credSessionEnvKey)
    if creds.HasKeys() {
        // Require logical grouping of credentials
        creds.ProviderName = EnvProviderName
        cfg.Creds = creds
    }

    // Role Metadata
    setFromEnvVal(&cfg.RoleARN, roleARNEnvKey)
    setFromEnvVal(&cfg.RoleSessionName, roleSessionNameEnvKey)

    // (〜中略〜)

    setFromEnvVal(&cfg.SharedCredentialsFile, sharedCredsFileEnvKey)
    setFromEnvVal(&cfg.SharedConfigFile, sharedConfigFileEnvKey)

    // (〜中略〜)

    if err := setUseDualStackEndpointFromEnvVal(&cfg.UseDualStackEndpoint, awsUseDualStackEndpoint); err != nil {
        return cfg, err
    }

    if err := setUseFIPSEndpointFromEnvVal(&cfg.UseFIPSEndpoint, awsUseFIPSEndpoint); err != nil {
        return cfg, err
    }

    return cfg, nil
}

まず、session.loadSharedEnvConfig() 関数はそのまま session.envConfigLoad() 関数の返り値を返しています。

そして、session.envConfigLoad() 関数は最後の return で cfg 変数を返していますが、途中 session.setFromEnv() 関数がたくさん呼ばれ、cfg 変数の様々なフィールドに値が設定されていることがわかります。この session.setFromEnv() 関数では、環境変数から設定値を読み込んでいます。

例えば、12-14 行目では、AccessKeyIDSecretAccessKeySessionToken というフィールドに、~~Key で定義されている環境変数名から読み込まれた値が AWS 認証情報として代入されています。(なお、もう少し深追いすれば、setFromEnvVal() 関数 の実装や、~~Key などにどのような環境変数名が入っているか確認できます。ちなみに例えば、credAccessEnvKey に入っている環境変数名は ["AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY"] です) そして、18 行目で環境変数から読み込んだ認証情報を cfg.Creds フィールドに代入しています。

気づき 2

このコードを読んでいると、例えば 32 行目の session.setUseDualStackEndpointFromEnvVal() というように、普段気にしないような設定があることに気づきます。

DualStack (デュアルスタック) というのは IPv4 と IPv6 両方で通信を行う形式なのですが、AWS SDK ではこのデュアルスタックを有効化するかどうか設定することができます。(デフォルトでは IPv4 のみです。)

気づき 3

アプリケーション側で AWS SDK を用いて Session を作成する際、以下のように書くことで、~/.aws/config や ~/.aws/credentials などの設定ファイルから設定を読み込むよう設定できます。

    sess := session.Must(session.NewSessionWithOptions(session.Options{
        SharedConfigState: session.SharedConfigEnable,
    }))

しかし、session.envConfigLoad() という関数内では環境変数からのみ設定を読み込み、~/.aws/config や ~/.aws/credentials などの設定ファイルからの設定の読み込みは行っていません。

設定ファイルからの設定読み込みはどこで行っているのかは、続きを見てみましょう。

5-5. (4) session.newSession() 関数

上記 session.loadSharedEnvConfig() 関数から返ってきた envCfg を利用して、session.newSession() 関数 で Session を作成します。

func newSession(opts Options, envCfg envConfig, cfgs ...*aws.Config) (*Session, error) {
    cfg := defaults.Config()

    handlers := opts.Handlers
    if handlers.IsEmpty() {
        handlers = defaults.Handlers()
    }

    // Get a merged version of the user provided config to determine if
    // credentials were.
    userCfg := &aws.Config{}
    userCfg.MergeIn(cfgs...)
    cfg.MergeIn(userCfg)

    // Ordered config files will be loaded in with later files overwriting
    // previous config file values.
    var cfgFiles []string
    if opts.SharedConfigFiles != nil {
        cfgFiles = opts.SharedConfigFiles
    } else {
        cfgFiles = []string{envCfg.SharedConfigFile, envCfg.SharedCredentialsFile}
        if !envCfg.EnableSharedConfig {
            // The shared config file (~/.aws/config) is only loaded if instructed
            // to load via the envConfig.EnableSharedConfig (AWS_SDK_LOAD_CONFIG).
            cfgFiles = cfgFiles[1:]
        }
    }

    // Load additional config from file(s)
    sharedCfg, err := loadSharedConfig(envCfg.Profile, cfgFiles, envCfg.EnableSharedConfig)
    if err != nil {
        if len(envCfg.Profile) == 0 && !envCfg.EnableSharedConfig && (envCfg.Creds.HasKeys() || userCfg.Credentials != nil) {
            // Special case where the user has not explicitly specified an AWS_PROFILE,
            // or session.Options.profile, shared config is not enabled, and the
            // environment has credentials, allow the shared config file to fail to
            // load since the user has already provided credentials, and nothing else
            // is required to be read file. Github(aws/aws-sdk-go#2455)
        } else if _, ok := err.(SharedConfigProfileNotExistsError); !ok {
            return nil, err
        }
    }

    if err := mergeConfigSrcs(cfg, userCfg, envCfg, sharedCfg, handlers, opts); err != nil {
        return nil, err
    }

    if err := setTLSOptions(&opts, cfg, envCfg, sharedCfg); err != nil {
        return nil, err
    }

    s := &Session{
        Config:   cfg,
        Handlers: handlers,
        options:  opts,
    }

    initHandlers(s)

    // (〜中略〜)

    return s, nil
}

まず、最後の返り値で s という変数を返していますが、s は以下のように宣言・代入されています。

    s := &Session{
        Config:   cfg,
        Handlers: handlers,
        options:  opts,
    }

ちなみに、Session は構造体名です。すなわち、cfg, handlers, opts という 3 つの変数が、Session にとって重要な情報であるとわかります。

まず簡単なものから見ていくと、3 つ目の opts は、ユーザーが session.NewSessionWithOptions() 関数を呼び出した際に渡した引数をベースに、いくつかの追加設定を加えたものになります。

次に、1 つ目の cfg は、先ほどでも session.envConfigLoad() 関数で見た通り、AWS 認証情報やその他リージョンなどの設定を保持しています。そして、43 行目の session.mergeConfigSrc() 関数で、cfg, userCfg, envCfg, sharedCfg の 4 種類の設定変数を統合しています。

これらの設定変数がそれぞれどこからやってきて何を保持しているのか読んでみると、

  • cfg : 2 行目の dafaults.Config() より作成。デフォルト設定を保持
  • userCfg : 11-13 行目の userCfg.MergeIn(cfgs...) より作成。引数で与えられた、session.NewSessionWithOptions() 関数を呼んだ際に指定した設定を保持
  • envCfg : この関数を呼び出した際の引数。先程の (3)envConfigLoad() 関数で環境変数から読み込んだ設定を保持
  • sharedCfg : 30 行目の loadSharedConfig() より作成。~/.aws/config や ~/.aws/credentials などの設定ファイルから読み込んだ設定を保持

となっていることが分かります。

5-6. (5) dafaults.Config() 関数

前項の 4 つの設定のうち、特に 2 行目の cfg := dafaults.Config() で作成するデフォルト設定を見てみましょう。以下のように定義されています。

func Config() *aws.Config {
    return aws.NewConfig().
        WithCredentials(credentials.AnonymousCredentials).
        WithRegion(os.Getenv("AWS_REGION")).
        WithHTTPClient(http.DefaultClient).
        WithMaxRetries(aws.UseServiceDefaultRetries).
        WithLogger(aws.NewDefaultLogger()).
        WithLogLevel(aws.LogOff).
        WithEndpointResolver(endpoints.DefaultResolver())
}

この Config() で、デフォルトのリクエストを送るための HTTPClient や、Log Level、リクエスト先のエンドポイント URL を決定するための Endpoint Resolver が設定されます。

気づき 4

上記から分かる通り、session の持つ Config に、リクエストの送信に使う HTTP Client を保存しています。そのため、Session は使い回せる場合は使い回すことが推奨されます。(異なるリージョンを使う場合や異なる IAM ロールを使う場合を除く。) それは、リクエストを送る際に同一の HTTP Client を利用することで、TCP コネクションを使い回すことができる場合があるからです。

TCP コネクションを確立するためには、通常クライアントとサーバー間で数回やり取りする必要がありますが、もし TCP コネクションが有効な間に次のリクエストを送れば、TCP コネクションを確立するための処理を減らすことができます。

また、Session を作る度に毎回環境変数や設定ファイルから認証情報や設定を取得する必要があり、この余計な処理を減らすこともできます。

Tips 1

Session は、こちらのドキュメント で “safe to use concurrently as long as the Session is not being modified.” と書かれている通り、(おそらくSession 自体を変更することはほとんどないと思いますが) 変更中でない限り goroutine などで同時利用可能です。

5-7. (6) dafaults.Handlers() 関数

(4) session.newSession() 関数で作成する Session の持つ 3 要素 cfg, handlers, opts のうち、2 つ目の handlers ではリクエスト送信時の各段階で行う様々な処理を指定しています。

この handlers の型request.Handler 型の構造体で、以下のように定義されています。

type Handlers struct {
    Validate         HandlerList
    Build            HandlerList
    BuildStream      HandlerList
    Sign             HandlerList
    Send             HandlerList
    ValidateResponse HandlerList
    Unmarshal        HandlerList
    UnmarshalStream  HandlerList
    UnmarshalMeta    HandlerList
    UnmarshalError   HandlerList
    Retry            HandlerList
    AfterRetry       HandlerList
    CompleteAttempt  HandlerList
    Complete         HandlerList
}

すなわち、リクエストの “Build” や “Send” などの各段階で行うべき処理が、HandlerList として保持されています。
HandlerList というのは、名前の通り各処理を行う handler のリスト (配列) を含んでいます。

この handlers は、(4) session.newSession() 関数の 6 行目にある以下の通り、通常はデフォルトの handlers が代入されています。

        handlers = defaults.Handlers()

デフォルトの handlers に何が含まれているのか見てみましょう。

func Handlers() request.Handlers {
    var handlers request.Handlers

    handlers.Validate.PushBackNamed(corehandlers.ValidateEndpointHandler)
    handlers.Validate.AfterEachFn = request.HandlerListStopOnError
    handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler)
    handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHander)
    handlers.Build.AfterEachFn = request.HandlerListStopOnError
    handlers.Sign.PushBackNamed(corehandlers.BuildContentLengthHandler)
    handlers.Send.PushBackNamed(corehandlers.ValidateReqSigHandler)
    handlers.Send.PushBackNamed(corehandlers.SendHandler)
    handlers.AfterRetry.PushBackNamed(corehandlers.AfterRetryHandler)
    handlers.ValidateResponse.PushBackNamed(corehandlers.ValidateResponseHandler)

    return handlers
}

ここで、PushBackNamed() メソッドは、各段階の HandlersList に handler を追加します。(PushBackNamed() メソッド本体)

例えば、4 行目で追加する最初の handler では、Validate 時の処理としてリクエストを送るサービスエンドポイントが登録されているか確認するという処理を行います。

このように、Session は、外部から読み込んだ設定を格納する cfg、各ステップでの処理を格納した handlers、ユーザーによってコード中で追加した設定を格納した opts という 3 種類のデータからなります。

なお、Session は、ClientConfig() メソッドや resolveEndpoint() メソッドなどの、基本的な設定情報を取得・生成するためのメソッドも持ち、後ほどの Client 作成時に使われます。


6. ソースコードリーディング : (B) クライアントの作成 svc := dynamodb.New(sess)

この章では、前章で作成した Session を利用して Amazon DynamoDB を操作するための Service client を作成します。ちなみに、svcSerVice Client の略です。

    // (B) Service client(単独のAWSサービスを利用するための設定)の作成
    svc := dynamodb.New(sess)

6-1. 本章で見ていく関数・メソッドの実行順序

└── (1) dynamodb.New()    // Amazon DynamoDB用のService Client作成を呼び出す関数
    ├── (2) Session.ClientConfig()     // Sessionの設定を取得する関数
    └── (3) dynamodb.newClient()     // Client作成を行う関数

6-2. (1) dynamodb.New() 関数

dynamodb.New() は以下のように定義されています。

func New(p client.ConfigProvider, cfgs ...*aws.Config) *DynamoDB {
    c := p.ClientConfig(EndpointsID, cfgs...)
    if c.SigningNameDerived || len(c.SigningName) == 0 {
        c.SigningName = EndpointsID
        // No Fallback
    }
    return newClient(*c.Config, c.Handlers, c.PartitionID, c.Endpoint, c.SigningRegion, c.SigningName, c.ResolvedRegion)
}

まず、引数 p で、前のステップで作成したSessionを受け取ります。

なお、引数 p の型が Session から client.ConfigProvider に変わっていることが気になるかも知れません。実は client.ConfigProvider は Go 言語で言うインターフェイスであり、Session 型を受け取ることができます。

そして、2 行目で ClientConfig() メソッドを呼ぶことで、Session から設定情報を取り出します。その設定情報を基に dynamodb.newClient() 関数を呼んで、Amazon DynamoDB 用の Service client を返します。

では、まずは ClientConfig() メソッドを深堀りして、どのような設定情報を取得しているのか見てみましょう。

6-3. (2) Session.ClientConfig() メソッド

Session 型の ClientConfig() メソッド は、以下のように定義されています。

func (s *Session) ClientConfig(service string, cfgs ...*aws.Config) client.Config {
    s = s.Copy(cfgs...)

    resolvedRegion := normalizeRegion(s.Config)

    region := aws.StringValue(s.Config.Region)
    resolved, err := s.resolveEndpoint(service, region, resolvedRegion, s.Config)
    if err != nil {
        s.Handlers.Validate.PushBack(func(r *request.Request) {
            if len(r.ClientInfo.Endpoint) != 0 {
                // Error occurred while resolving endpoint, but the request
                // being invoked has had an endpoint specified after the client
                // was created.
                return
            }
            r.Error = err
        })
    }

    return client.Config{
        Config:             s.Config,
        Handlers:           s.Handlers,
        PartitionID:        resolved.PartitionID,
        Endpoint:           resolved.URL,
        SigningRegion:      resolved.SigningRegion,
        SigningNameDerived: resolved.SigningNameDerived,
        SigningName:        resolved.SigningName,
        ResolvedRegion:     resolvedRegion,
    }
}

この関数の返り値に注目すると、Config や Handlers に加えて、API リクエストを送る Endpoint の URL やリージョン情報が返されていることがわかります。

なお、PartitionID というフィールドは、Amazon Athena や Amazon DynamoDB で出てくるパーティションとは完全に別物で、通常の AWS リージョンなのか、それとも中国リージョンや GovCloud リージョンなのか区別するために使われるフィールドです。
また、Signing- という名前がつくフィールドは、リクエストを送る際の署名に使うフィールドです。

ちなみに、エンドポイント URL などを保持している resolved 変数は、7 行目にある通り Session の resolveEndpoint() メソッドから取得しています。

詳しい取得方法は resolveEndpoint() メソッド を深堀りしていくと知ることができますが、大分複雑なのでここでは割愛させていただきます。

6-4. (3) dynamodb.newClient() 関数

それでは、いよいよ Service client を作成する dynamodb.newClient() 関数 を見ていきましょう。

// newClient creates, initializes and returns a new service client instance.
func newClient(cfg aws.Config, handlers request.Handlers, partitionID, endpoint, signingRegion, signingName, resolvedRegion string) *DynamoDB {
    svc := &DynamoDB{
        Client: client.New(
            cfg,
            metadata.ClientInfo{
                ServiceName:    ServiceName,
                ServiceID:      ServiceID,
                SigningName:    signingName,
                SigningRegion:  signingRegion,
                PartitionID:    partitionID,
                Endpoint:       endpoint,
                APIVersion:     "2012-08-10",
                ResolvedRegion: resolvedRegion,
                JSONVersion:    "1.0",
                TargetPrefix:   "DynamoDB_20120810",
            },
            handlers,
        ),
    }
    svc.endpointCache = crr.NewEndpointCache(10)

    // Handlers
    svc.Handlers.Sign.PushBackNamed(v4.SignRequestHandler)
    svc.Handlers.Build.PushBackNamed(jsonrpc.BuildHandler)
    svc.Handlers.Unmarshal.PushBackNamed(jsonrpc.UnmarshalHandler)
    svc.Handlers.UnmarshalMeta.PushBackNamed(jsonrpc.UnmarshalMetaHandler)
    svc.Handlers.UnmarshalError.PushBackNamed(
        protocol.NewUnmarshalErrorHandler(jsonrpc.NewUnmarshalTypedError(exceptionFromCode)).NamedHandler(),
    )

    // Run custom client initialization if present
    if initClient != nil {
        initClient(svc.Client)
    }

    return svc
}

ここでは、あまり複雑な処理はしていません。

先程の ClientConfig() メソッドで取得した設定値を基に、Amazon DynamoDB 用の Service client (svc) を作成しています。

そして、リクエスト署名用の v4.SignRequestHandler や、JSON-RPC 形式のリクエストボディを生成するための jsonrpc.UnmarshalHandler など、いくつかの handler を追加します。

handlers 追加後の svc を最後の return で返します。

Tips 2

Service client も、使い回せる場合は使い回すことが推奨されます。
その理由として、本記事では詳述していませんが、もし IAM ロールや ID フェデレーションを利用して AWS にアクセスする場合に、AWS の一時認証情報が Service client にて取得・保持されるからです。

この AWS の一時認証情報は、AWS Security Token Service (STS) や AWS EC2 Instance Metadata Service (IMDS) などの、 他の AWS サービスから取得されます
(詳細は こちらのドキュメント をご参照ください)。

そのため、毎度 Service client を作成すると、初めてリクエストを送る度に外のサービスから一時的な認証情報を取得することになり比較的長いラウンドトリップ時間がかかる上に、AWS STS や AWS EC2 IMDS のアクセス回数などのサービスクォータに抵触し、エラーにつながる可能性があります。

goroutine の内側で client を作成することなど、うっかりやりがちですが、気をつけましょう (AWS SDK for Go における認証情報の扱い方については次回以降の記事で紹介予定なので、よろしければご覧ください)。

Tips 3

Session 同様、Service client は こちらのドキュメント に記載がある通り “safe to use concurrently” なので、goroutine などで同時利用可能です。


7. まとめ

本記事では、まず AWS SDK for Go の基本的な使い方や、AWS SDK の裏側の AWS サービスへの API リクエスト送信の仕組みをご紹介しました。その後、ソースコードとともに、AWS SDK for Go (Version1) において Session, Service Client の設定を行う際の裏側の動作をご紹介しました。

本記事を通して、少しでも AWS SDK に興味を持っていただいたり、ソースコードリーディングに親しみを持っていただけると幸いです。

次回は、dynamodb.GetItem() の実装を参考にしながら、実際に AWS サービスを操作するための API リクエストを送るまでの AWS SDK の挙動についてご紹介してみたいと思います。


8. 参考情報


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

王 力捷 (Lijie Wang) @elecho1_t
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト

学生時代に機械学習の研究をしていたこともあり、MLOps に関心を持つ。好きな AWS サービスは AWS App Runner
休日の楽しみは、鉄道動画やバラエティ番組をのんびりと見ること。

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する