[curl/AWS]AmazonのAPIにcurlコマンドでHTTPリクエストを送信しようとして詰んだ話

<h1>はじめに</h1>
はじめまして。
TalendというETLツールの技術者を3年ほどやっています。
今回は、(Talendとは直接関係ない話題ですが) 事前検証で実施したcurlコマンドにて失敗した話と、その解決策について記載します。

<h1>やりたいこと</h1>

本記事のゴールは、下記の最終目標の前段階として
* **AWS CLIを使用せず、curlコマンドを用いて直接AWSのAPIにHTTPリクエストを送信したい** ※この際、Amazon API Gatewayは使用しない
* **送信したリクエストに対するレスポンスが想定通りであることを確認したい**

の2点です。

最終的な目標は
**TalendのCloud Engine (以下「CE」)上から、Amazon Simple Notification Service (Amazon SNS) のトピックにメッセージを発行し、メール通知を行う**
ことです。

:::note info
トピックの発行を行うだけであれば、以下のAWS CLIコマンドを実行することで簡単に実現できます。※ただし、AWS CLIの導入が必要となります。


```
$ aws sns publish --topic-arn (topic ARN) --message (送信するメッセージ)
```
:::

※詳細は下記ドキュメント参照。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-services-sns.html#cli-publish-sns-topic

今回は、**CEにAWS CLIを導入することができない**ため、AWSのAPIを利用します。

<h2>なぜcurl？</h2>

Talend Studioには、"tHTTPClient"というコンポーネントがあります。

https://help.talend.com/ja-JP/components/8.0/http/thttpclient

これを見ると「**コンポーネントがあるなら、curlではなくコンポーネントを使えばいいのでは？**」という至極当然の疑問が浮かぶと思いますが、その回答を行う前に、**AWS Signature Version 4**について簡単にまとめます。

<h3>AWS Signature Version 4</h3>

公式リファレンス

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_aws-signing.html

以下、冒頭を引用。
> リクエストで送信する認証情報には、署名が含まれている必要があります。AWSSignature Version 4 (SigV4) は、AWS API リクエストに認証情報を追加するための AWS 署名プロトコルです。

要するに、「AWSのAPIにリクエストを送信する場合、この形式の署名が必要です」といった内容です。計算方法について、詳細が知りたい方は公式リファレンスをご覧ください。
※AWS CLIの各コマンドにはこの署名計算も含まれており、**今回みたいな特殊な事情やこだわりがなければ、素直にAWS CLIを利用したほうが楽です**。

公式リファレンスの内容を軽く見ていただくとわかるのですが、この署名、計算が非常に煩雑です。
ただ、この計算を簡単に行える方法がありまして、それが**curlコマンドのオプション**です。
このため、「**curlではなくコンポーネントを使えばいいのでは？**」という疑問に対する回答は、
「**curlコマンドのオプションを利用することで変なミスを減らせると思ったから**」
です。
なお、この浅はかな考えで、今回はものすごく苦労しました。

**前置きはここまでとして、本題に入ります。**

<h1>HTTPリクエスト送信</h1>

<h2>コマンド</h2>

```
curl -i -vvv --location --aws-sigv4 aws:amz:(AWSリージョン):sns --user (アクセスキー):(シークレットキー) --request POST https://sns.(AWSリージョン).amazonaws.com/?Action=Publish&TopicArn=(topic ARN)&Message=Hello
```

※実行環境によっては
```
curl: (3) URL using bad/illegal format or missing URL
```
となり、コマンド実行に失敗する場合があります。その場合、環境に応じてコマンドの微修正を行ってください (主にクォートの付与などが必要です)。

**--aws-sigv4**オプションを指定すると、上記の署名の付与等を行ってくれるので、今回はこのオプションを利用して、AWSのAPIにHTTPリクエストを送信します。

<h2>リクエスト送信</h2>

CEのOSは、
> it has a Linux-flavored operating system.

らしいので、ひとまずWSLのUbuntuでコマンドを実行してみました。
その際のレスポンスの内容はこちら。

```
<ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
  <Error>
    <Type>Sender</Type>
    <Code>SignatureDoesNotMatch</Code>
    <Message>The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.</Message>
  </Error>
  <RequestId>xxx</RequestId>
</ErrorResponse>

```

**エラーレスポンスが返され、トピックからのメール送信も行われず。**

<h2>別の環境からリクエスト送信</h2>

試しにWindowsのコマンドプロンプトで、全く同じコマンドを実行しました。
その際のレスポンス。

```
<PublishResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
  <PublishResult>
    <MessageId>xxx</MessageId>
  </PublishResult>
  <ResponseMetadata>
    <RequestId>yyy</RequestId>
  </ResponseMetadata>
</PublishResponse>
```

**先ほどとは異なり、想定される (と思しき) レスポンスが返され、メールも送信されていました。**

<h2>検証</h2>

curlのバージョンを見てみたところ、

**WSL**
```
$ curl --version
curl 7.84.0 (i686-pc-cygwin) libcurl/7.84.0 OpenSSL/1.1.1q zlib/1.2.12 libssh2/1.10.0
Release-Date: 2022-06-27
Protocols: dict file ftp ftps gopher gophers http https imap imaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS HSTS HTTPS-proxy IPv6 Largefile libz NTLM NTLM_WB SSL threadsafe TLS-SRP UnixSockets
```

**コマンドプロンプト**
```
$ curl --version
curl 8.8.0 (Windows) libcurl/8.8.0 Schannel zlib/1.3 WinIDN
Release-Date: 2024-05-22
Protocols: dict file ftp ftps http https imap imaps ipfs ipns mqtt pop3 pop3s smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS HSTS HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM SPNEGO SSL SSPI threadsafe Unicode UnixSockets
```

curlのバージョンによって通ったり通らなかったりするらしいことがわかりました。

<h2>事象の理由</h2>

いろいろ調査を行ったところ、以下の情報にたどり着きました。

https://github.com/curl/curl/issues/9717

どうやら、curlコマンドの"--aws-sigv4"オプションに不具合があり、
本来はクエリパラメータのキー名をソートしたうえで計算を行う必要があるようなのですが、ソートが行われていないとのことでした。
この不具合が解消されたのはcurlのバージョン7.86であるため、

* WSL : **バージョンが7.86未満であるため、この不具合に引っかかり失敗**
* コマンドプロンプト : **バージョンが7.86以降であるため、成功**

という結果になったようです。

<h2>事象の回避</h2>

> 本来はクエリパラメータのキー名をソートしたうえで計算を行う必要があるのですが、ソートが行われていないとのことでした。

なので、リクエストのクエリパラメータをこちらでソートしてやれば、事象は回避できそうです。

**コマンド**
```
curl -i -vvv --location --aws-sigv4 aws:amz:(AWSリージョン):sns --user (アクセスキー):(シークレットキー) --request POST https://sns.(AWSリージョン).amazonaws.com/?Action=Publish&&Message=Hello&TopicArn=(topic ARN)
```

**レスポンス**

```
<PublishResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
  <PublishResult>
    <MessageId>xxx</MessageId>
  </PublishResult>
  <ResponseMetadata>
    <RequestId>yyy</RequestId>
  </ResponseMetadata>
</PublishResponse>
```

リクエスト送信に対して、想定通りの挙動となりました。

<h1>まとめ</h1>

今回はAWS CLIの導入ができず、かつコマンドのバージョンアップもできないという特殊な環境であったため、どうあっても導入されているcurlのバージョンの挙動を調べる必要があり、いろいろと手間取りました。正直、コマンドに不具合が内包されているのは想定外でしたが、「そんなケースもあるのかー」と、いい経験にはなりました。

以上。