AWSのSigV4署名を付与してIAM認証のAPI GatewayにHTTPリクエストを送るRubyとGoのサンプルコード

この記事は、[リンクバルアドベントカレンダー2022](https://qiita.com/advent-calendar/2022/linkbal)の1日目の記事です。

## はじめに
SigV4署名を付与してIAM認証のAPI GatewayにHTTPリクエストをするRubyとGoのサンプルコードを紹介します！

AWSのAPIを使うには、SigV4署名が必要です。AWS CLIやAWS SDKは、SigV4署名をよしなにやってくれるので、APIを使う側がSigV4署名を意識することはありません。

https://aws.amazon.com/jp/builders-flash/202210/way-to-operate-api-2/

しかし、IAM認証をしたAPI GatewayのAPIをリクエストするときなど、単純なHTTPリクエストの際には、自前で署名を付与する必要があります（AWSのAPIには、SigV4署名を付けてAPI Gatewayをリクエストしてくれる便利なものは存在しません）。

署名の付与は自前ですが、署名をするためのライブラリは用意されているので、そちらを使えばすんなり署名ができます。

## Ruby
### ドキュメント
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Sigv4/Signer.html

### サンプルコード
重要な部分はコメントで説明しています。

```rb
require 'aws-sdk-core'
require 'net/http'
require 'uri'
require 'json'
require 'yaml'

# SigV4署名を行うオブジェクトを作成
signer = Aws::Sigv4::Signer.new(
  service: 'execute-api',# API GatewayのAPIを呼ぶためのserviceの指定
  region: 'ap-northeast-1',
  credentials_provider: Aws::Credentials.new('xxx', 'xxxxx'), # 認証情報の取得。詳細は後述。
)

# API Gatewayのエンドポイント
api_url = 'https://xxx.execute-api.ap-northeast-1.amazonaws.com/xxx/xxx'
body_data = {
  hoge: "xxxx",
  foo: "xxx"
}
req_body = body_data.to_json

# SigV4署名を行う。HTTP Method, URL, bodyを引数に渡す
# signature.headers が、SigV4署名されたhttp headerです
signature = signer.sign_request(
  http_method: 'POST',
  url: api_url,
  body: req_body,
)

signature.headers['Content-Type'] = 'application/json'

uri = URI.parse(api_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

# 署名されたheaderを、HTTPリクエストのheaderに追加
response = http.post(uri.path, req_body, signature.headers)
```

### credentials_providerについて
credentials_providerは、以下の5種類があります。
漏洩のリスクやキーの管理など、主にセキュリティ的に `InstanceProfileCredentials` か `ECSCredentials` の利用が望ましいです。極力アクセスキーを利用するのは避けましょう。

- Aws::Credentials
    - ACCESS_KEY_ID, SECRET_ACCESS_KEY, SESSION_TOKEN（任意） を利用
    - コード例: `Aws::Credentials.new('xxxx', 'xxxxxxxxx')`
        - 第三引数がSESSION_TOKENで、指定しなければnilになります
    - ドキュメント https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Credentials.html
- Aws::SharedCredentials
    - SharedCredentials、つまり `~/.aws/credentials` を利用
    - コード例: `Aws::SharedCredentials.new`
    - ドキュメント https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/SharedCredentials.html
- Aws::InstanceProfileCredentials
    - EC2にアタッチされているIAMロールを利用
        - EC2上で動作するプログラムでしか、利用できません
    - コード例: `Aws::InstanceProfileCredentials.new`
    - ドキュメント https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/InstanceProfileCredentials.html
- Aws::AssumeRoleCredentials AssumeRole
    - AssumeRoleを利用
    - コード例: `Aws::AssumeRoleCredentials.new(client: Aws::STS::Client.new(...), role_arn: "linked::account::arn", role_session_name: "session-name"
)`
    - ドキュメント https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/AssumeRoleCredentials.html
- Aws::ECSCredentials
    - ECSのタスク定義のIAMロールを利用
    - コード例: `Aws::ECSCredentials.new(retries: 3)`
   - ドキュメント https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ECSCredentials.html



## Go
### ドキュメント
https://docs.aws.amazon.com/sdk-for-go/api/aws/signer/v4/

### サンプルコード
**注意：SDKがv1のコードです**

```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/aws/aws-sdk-go/aws/session"
	v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
)

type RequestStruct struct {
	Hoge string `json:"hoge"`
	Foo  string `json:"foo"`
}

func main() {
    // API Gatewayのエンドポイントを指定
	apiURL := "https://xxx.execute-api.ap-northeast-1.amazonaws.com/xxx"
	data := RequestStruct {
		Hoge: "xxx",
		Foo:  "yyy",
	}

	body, err := json.Marshal(data)
	if err != nil {
		fmt.Printf("error: %+v\n", err)
	}

	// *Reader型のreqBodyを作成
	reqBody := strings.NewReader(string(body))

	req, err := http.NewRequest(http.MethodPost, apiURL, reqBody)
	if err != nil {
		fmt.Printf("error: %+v\n\n", err)
	}
	req.Header.Set("Content-Type", "application/json")

	// 認証情報を取得
	sess := session.Must(session.NewSession())
	creds := stscreds.NewCredentials(sess, "arn:aws:iam::xxxxxxxx:role/xxxxxxxxxxxx")

	// SigV4署名を行うsignerを作成
	signer := v4.NewSigner(creds)

    // SigV4署名
	signer.Sign(
		req, // *http.Request
		reqBody, // io.ReadSeeker
		"execute-api", // AWSのService
		"ap-northeast-1", // region
		time.Now(), // 署名時刻
	)

	client := new(http.Client)
	res, err := client.Do(req)
	if err != nil {
		fmt.Printf("error: %+v\n\n", err)
	}

	defer res.Body.Close()
}
```

SDKがv2だと書き方が変わるため、ドキュメントや以下のような記事をご覧ください。

https://documentroot.org/articles/send-http-request-with-sigv4-in-go.html


### Credentialsについて
Rubyと同じく方法がいくつかあるので、ドキュメントをご覧ください！（ぶん投げ）

https://docs.aws.amazon.com/sdk-for-go/api/aws/credentials/#Credentials

クラスメソッドさんの記事がわかりやすいです。

https://dev.classmethod.jp/articles/using-credential-at-aws-sdk-for-go/


## とりあえずリクエストだけしたいとき
そんなときは `curl` の `"--aws-sigv4"` オプションを付ければ、可能らしいです。

https://chariosan.com/2021/09/26/curl_aws-sigv4_iamrole_access_opensearch/

また、`awscurl` というSigV4署名を付けてcurlしてくれるツールもあります。profileなどの指定が、aws cliと同じ用にできるのでらくちんです。

https://github.com/okigan/awscurl


## おわりに
IAMの理解が浅いと、Credentialの取得やAPI Gatewayのリソースポリシーの書き方などでつまずきます。僕はつまずきました。よくわかっていない方は、まずはIAMを理解するのがおすすめです！


## 参考
API Gatewayのリクエストではありませんが、いろいろな言語の参考になるサンプルコードです。

https://docs.amazonaws.cn/en_us/opensearch-service/latest/developerguide/request-signing.html#request-signing-ruby

Pythonのサンプルコード

https://cloud5.jp/iam_authorization/

Node.jsのサンプルコード

https://dev.classmethod.jp/articles/api-gateway-iam-authentication-sigv4-aws-sdk-v3/

Java

https://dev.classmethod.jp/articles/create-sigv4-with-aws-sdk-for-java-v1/


もしSigV4署名を自前で実装したい場合のサンプルコード

https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-v4-examples.html