カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

Amazon S3 へのファイルアップロードで POST Policy を使うと、かゆいところに手が届くかもしれない

はじめに

こんにちは。カミナシでソフトウェアエンジニアをしている佐藤です。

みなさんは、アプリケーションのフロントエンドから、Amazon S3 にファイルをアップロードするときに、どのような方法を用いているでしょうか? 「バックエンドのサーバーにファイルを送信し、バックエンドのサーバー経由で S3 にアップロードしている」「Presigned URL を払い出して、フロントエンドから直接 PUT している」など、いくつかの方法があると思います。 弊社で提供しているサービス「カミナシレポート」でも、用途に応じて上記の方法を使い分けて S3 へのファイルのアップロードを行っています。 特に、Presigned URL は、手軽に利用できる上に、バックエンドのサーバーの負荷やレイテンシーの削減といったメリットも大きく、重宝しています。

一方で、その手軽さの反面、アップロードに際して様々な制約をかけたい時に「かゆいところに手が届かない」と感じることもあります。

本記事は、このような時に Presigned URL の代替案となりうる「POST Policy」について入門を試みたものです。POST Policy の概要や実際の実装例・使用例をまとめていきます。

対象読者

  • Amazon S3 へのファイルアップロードの選択肢を増やしたい方(本記事では、POST Policy メインで解説しますが、冒頭で Presigned URL にも軽く触れます)
  • Presigned URL を利用した PUT によるファイルアップロードを使っている中で、より柔軟な制御の必要性を感じたことがある方
  • 「POST Policy」によるファイルアップロードの使用例・実装例が知りたい方

Presigned URL とは

POST Policy を紹介する前に、Presigned URL のおさらいをしておきましょう。

Presigned URL とは端的にいうと、「対象の S3 のオブジェクトにアクセスするための AWS の IAM Credential を持たない主体(ユーザー、フロントエンドアプリケーション etc)が、同オブジェクトを参照または更新できるようにするための仕組み」と言えます。

(*本記事は、ファイルの「アップロード」をメインテーマとしていますが、Presigned URL は、「ダウンロード」「アップロード」双方に対応しています)

以下が Presigned URL の例です。

https://your-bucket-name.s3.ap-northeast-1.amazonaws.com/2024022614/my-sample-file.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=HOGEHOGE%2F20240226%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20240226T142939Z&X-Amz-Expires=60&X-Amz-Security-Token=IQo...3D&X-Amz-SignedHeaders=host&X-Amz-Signature=032a...17

Amazon S3 のエンドポイントに向けた、署名が付与された URL であることがわかります。

このような URL を、「対象の S3 オブジェクトにアクセスするための AWS の IAM Credential を有する主体(バックエンドのアプリケーション etc)」が署名・作成し、先述の「ユーザー」や「フロントエンドアプリケーション」などに受け渡します。 ユーザーやフロントエンドアプリケーションは、この URL に対して、GET や PUT の HTTP リクエストを送信することで、対象の S3 オブジェクトの参照や更新が行えます。

Presigned URL でファイルをアップロードする例

ファイルをダウンロード・アップロードする際に、バックエンドのアプリケーションを経由しなくて良いため、バックエンドのサーバーの負荷やレイテンシーの削減につながるなどのメリットがあります。

Presigned URL についてより詳細な情報は、Amazon S3 - User Guide の Working with presigned URLs をご確認ください。

Presigned URL は手軽に無駄の少ない処理が実現できる非常に便利なものですが、しばしばファイルアップロードに際して、様々な制約をかけたくなることがあります。

  • アップロードできるオブジェクトのサイズを、自サービスのユースケースで必要十分な範囲に制限したい。
  • アップロード時の Content-Type にある程度の柔軟性を持たせた上で、特定の値を強制したい。

上記のような制約は(少なくとも私の知る限りでは)、 Presigned URL を使用したアップロードではかけることができません。

このように、Presigned URL だと「かゆいところに手が届かない」と思った時が、まさに本記事のメインテーマである「POST Policy」の出番です。次項以降で、詳しくみていきましょう。

POST Policy とは

Amazon S3 の API Reference で紹介されている方法で、様々なポリシーが記載された JSON 形式の「policy document」と「policy document」への署名とともに、ファイルをアップロードすることができます。この「policy document」が、「POST Policy」と呼ばれています。 アップロードしたいファイルを署名とともに送信する点は、Presigned URL を利用した PUT によるファイルアップロードとよく似ていますが、以下のような点が異なります。

  • HTTP メソッドは、POST
  • リクエストボディは、multipart/form-data 形式であり、policy document や、署名、アップロード対象のファイルなどが、個々のフィールドにセットされる。
  • policy document に様々な制約を記載できる。例えば、オブジェクトのキーやアップロードするデータのサイズへの制約を記述できる。

以下は、policy document の例です。

{ "expiration": "2024-02-20T00:00:00.000Z",
  "conditions": [
    {"bucket": "myExampleBucket"},
    ["eq", "$key", "20240219/abc"],
    ["starts-with", "$Content-Type", "image/"],
    ["content-length-range", 0, 10240],

    {"x-amz-credential": "ABCDEFG1234567/20240219/ap-northeast-1/s3/aws4_request"},
    {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
    {"x-amz-date": "20240219T000000Z" }
  ]
}

この例では、アップロード対象のオブジェクトのキーは「20240219/abc」に制限され、「Content-Type」は、「image/」で始まるものに制限されます。また、アップロードされるデータのサイズは、0〜10KiB に制限されます。

処理のフローは、Presigned URL の場合と非常によく似ています。バックエンドアプリケーションなどが、 policy document に、AWS Signature Version 4 で署名をし、policy document などとともに、ユーザーやフロントエンドアプリケーションに受け渡します。 ユーザーやフロントエンドアプリケーションは、これらをファイルデータとともに POST します。

POST Policy を用いてファイルをアップロードする例

以降で、実際の例をみながら、理解を深めていきます。

実装例

Go による実装例・使用例を通して、理解を深めていきます。本項ではコードの一部のみを示し、記事末の Appendix.1 にコード全体を記載します。また、Appendix.2 に具体的な環境構築方法や実行方法を記載します。

大まかに以下の Step で実装します。

  • Step1. policy document の作成
  • Step2. signing Key の作成
  • Step3. policy document に対して署名

詳しくそれぞれの Step をみていきましょう。

📝 Step1. policy document の作成

    /* Step1. Policy document の作成 */
    policy := Policy{
        Expiration: time.Now().Add(1 * time.Hour).Format("2006-01-02T15:04:05Z"),
        Conditions: []any{
            Cond{"bucket": bucket},
            Cond{"acl": "private"},
            []string{"eq", "$key", objectKey},
            []string{"starts-with", "$Content-Type", "image/"},
            []any{"content-length-range", 0, 10 * 1024},
            Cond{"x-amz-algorithm": algorithm},
            Cond{"x-amz-credential": xAmzCredential},
            Cond{"x-amz-date": xAmzDate},
            Cond{"x-amz-security-token": sessionToken},
        },
    }

まずは policy document を作成します。expiration、conditions は必須であり、conditions の中でも以下のフィールドは必須とされています。

  • x-amz-algorithm
  • x-amz-credential
  • x-amz-date
  • x-amz-security-token (「temporary security credentials」を利用する場合)

AWS Signature Version 4 では、AssumeRole などによりクレデンシャルを取得した場合など、「temporary security credentials」を利用する場合は x-amz-security-token も必要となるようです。実際、今回の実装例では、このフィールドを含めなければ、ファイルアップロード時にエラーとなりました( 参考 )。

また、conditions 内の各種条件は「完全一致」(Exact Matches)、「前方一致」(Starts With)などのいくつかの方法で指定することが可能です。

conditions の書き方などの詳細は、API Reference の Condition Matching の部分をご確認ください。

今回は以下のような、policy document を生成しています。

{
  "expiration": "2024-02-21T00:47:39Z",
  "conditions": [
    { "bucket": "your-bucket-name" },
    { "acl": "private" },
    ["eq", "$key", "20240220/9c83744b-d463-4b6a-9d9f-dd634999fee3"],
    ["starts-with", "$Content-Type", "image/"],
    ["content-length-range", 0, 10240],
    { "x-amz-algorithm": "AWS4-HMAC-SHA256" },
    {
      "x-amz-credential": "YOURACCESSKEYIDHOGE/20240220/ap-northeast-1/s3/aws4_request"
    },
    { "x-amz-date": "20240220T000000Z" },
    { "x-amz-security-token": "IQoJ...1m" }
  ]
}

*オブジェクトのキーをクライアントサイドで自由に指定できるようにすることは、大きなセキュリティリスクにつながる可能性があるため、よほどの理由がない限り、通常は「eq」で完全一致で指定する=サーバーサイドでキーを決定するべきでしょう( 参考 )。

conditions に指定できる要素についての詳細は、API Reference の Conditions の部分をご確認ください。

📝 Step2. signing Key の作成

func createSigningKey(secretAccessKey string, date time.Time, awsRegion string, awsService string) []byte {
    // DateKey = HMAC-SHA256("AWS4" + "<SecretAccessKey>", "<yyyymmdd>")
    dateKey := hash([]byte("AWS4"+secretAccessKey), date.Format("20060102"))
    // DateRegionKey = HMAC-SHA256(DateKey, "<aws-region>")
    dateRegionKey := hash(dateKey, awsRegion)
    // DateRegionServiceKey = HMAC-SHA256(DateRegionKey, "<aws-service>")
    dateRegionServiceKey := hash(dateRegionKey, awsService)
    // SigningKey = HMAC-SHA256(DateRegionServiceKey, "aws4_request")
    signingKey := hash(dateRegionServiceKey, "aws4_request")
    return signingKey
}

アクセスキーを元に、署名用のキーを作成します。 API Reference の Calculating a Signature を参考に実装しています。

📝 Step3. policy document に対して署名

    /* Step3. policy document に対して署名 */
    base64EncodedSecurityPolicy := toBase64EncodedSecurityPolicy(policy)
    signature := createSignature(signingKey, base64EncodedSecurityPolicy)

Step2. で作成した signing Key を利用して、policy document を Base64 エンコードしたものに署名します。

以上が、大まかな実装の Step です。

なお、このアプリケーションは検証のために作成したスタンドアローンなアプリケーションですが、実際の Web アプリケーションなどでは、上記のような処理を実装したエンドポイントが作成され、PostFormComponentsに相当する内容が、クライアントアプリケーションに渡されるイメージです。

実行してみる

前項目のサンプルコードを実行すると、以下のように出力されます。

==== PostFormComponents ====
export AWS_BUCKET="your-bucket-name"
export OBJECT_KEY="20240218/6107cc12-d3f3-4544-8026-b94998ef999a"
export X_AMZ_CREDENTIAL="YOURACCESSKEYIDHOGE/20240218/ap-northeast-1/s3/aws4_request"
export X_AMZ_ALGORITHM="AWS4-HMAC-SHA256"
export X_AMZ_DATE="20240218T000000Z"
export POLICY="........"
export X_AMZ_SIGNATURE="........"
export X_AMZ_SECURITY_TOKEN="........"
==== PostFormComponents ====

この出力値を元に環境変数を設定し、以下のようなスクリプト( post.sh )を用いて、動作を確認してみましょう。

ACL="${ACL:-private}" 
FILE="${FILE:-data.png}"
CONTENT_TYPE="${CONTENT_TYPE:-image/png}"

curl -X POST \
  -H "Host: ${AWS_BUCKET}.s3.amazonaws.com" \
  -H 'Content-Type: multipart/form-data; boundary=1213456789abcdedg' \
  -F "key=${OBJECT_KEY}" \
  -F "Content-Type=${CONTENT_TYPE}" \
  -F "acl=${ACL}" \
  -F "X-Amz-Credential=${X_AMZ_CREDENTIAL}" \
  -F "X-Amz-Algorithm=${X_AMZ_ALGORITHM}" \
  -F "X-Amz-Date=${X_AMZ_DATE}" \
  -F "Policy=${POLICY}" \
  -F "X-Amz-Signature=${X_AMZ_SIGNATURE}" \
  -F "X-Amz-Security-Token=${X_AMZ_SECURITY_TOKEN}" \
  -F "file=@./${FILE}" \
  -w "\n%{http_code}\n" https://${AWS_BUCKET}.s3.amazonaws.com/

実行結果(見やすさのため、改行などは適当に調整)

#### data.png という 10240 byte 以下のサイズのファイルを送信
$ sh ./post.sh
204

#### binary_file_10240.bin という 10240 byte ちょうどのファイルを送信
$ FILE=binary_file_10240.bin sh ./post.sh
204

#### binary_file_10241.bin という 10240 byte を超えるファイルを送信
$ FILE=binary_file_10241.bin sh ./post.sh
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>EntityTooLarge</Code>
    <Message>Your proposed upload exceeds the maximum allowed size</Message>
    <ProposedSize>10241</ProposedSize>
    <MaxSizeAllowed>10240</MaxSizeAllowed>
    <RequestId>...(略)...</RequestId>
    <HostId>...(略)...</HostId>
</Error>
400

#### policy document で許可されていない Content-Type を指定(前方一致)
$ CONTENT_TYPE="text/plan" sh ./post.sh
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>AccessDenied</Code>
    <Message>Invalid according to Policy: Policy Condition failed: ["starts-with", "$Content-Type", "image/"]</Message>
    <RequestId>...(略)...</RequestId>
    <HostId>...(略)...</HostId>
</Error>
403

#### policy document で許可されていないオブジェクトのキーを指定(完全一致)
$ OBJECT_KEY="my-key-123" sh ./post.sh
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>AccessDenied</Code>
    <Message>Invalid according to Policy: Policy Condition failed: ["eq", "$key", "20240218/6107cc12-d3f3-4544-8026-b94998ef999a"]</Message>
    <RequestId>...(略)...</RequestId>
    <HostId>...(略)...</HostId>
</Error>
403

#### policy document で conditions に指定されていないフィールド "x-amz-meta-uuid=hoge" を Form Field に追加して、実行
$ sh ./post.sh 
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>AccessDenied</Code>
    <Message>Invalid according to Policy: Extra input fields: x-amz-meta-uuid</Message>
    <RequestId>...(略)...</RequestId>
    <HostId>...(略)...</HostId>
</Error>
403

下記のような結果が得られたことがわかると思います。

  • content-length-range で指定したサイズの範囲内のファイルはアップロードでき、範囲を超えるサイズのファイルはアップロードできない
  • policy document の conditions で指定された形式を満たさないフィールドがある場合、アップロードできない
  • policy document の conditions で指定されていないフィールドが存在する場合、アップロードできない

個人的に嬉しいと思ったのが、policy document の conditions に指定がないフィールドは( 一部例外を除き )、POST 時に指定できない点です。つまり、policy document の conditions はホワイトリストになっているということです。 これにより、「 policy document に記載をし忘れて、意図しない指定でアップロードされてしまう」事態が発生しづらくなっていると思います。

一方で、以下のような点には注意が必要だと感じました。

  • policy document の conditions で、content-length-range の指定を忘れない。
  • policy document の conditions の条件指定は、できるだけ「完全一致」で記述する。特に、ワイルドカード=制限なし( 例:["starts-with", "$acl", ""])は、可能な限り避ける。

content-length-range について、この指定を行わなくてもアップロードは成功することから、Amazon S3 のリミットを超過しない限りアップロードは成功すると思われます(PUT と同様であるならば、5GB)。

実行してみる - おまけ

さらに2パターンほど試してみました。policy document を改変したパターンと、AssumeRole した IAM Role に適切な permission がないパターンです。

まずは、policy document の一部を書き換えて送信したパターンです。

#### policy document を書き換え
$ POLICY=${DUMMY_POLICY} sh ./post.sh
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>SignatureDoesNotMatch</Code>
    <Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
    <AWSAccessKeyId>YOURACCESSKEYIDHOGE</AWSAccessKeyId>
    <StringToSign>eyJl....cifV19</StringToSign>
    <SignatureProvided>1478....48ca</SignatureProvided>
    <StringToSignBytes>65 79 .... 31 39</StringToSignBytes>
    <RequestId>...(略)...</RequestId>
    <HostId>...(略)...</HostId>
</Error>
403

次に、AssumeRole した IAM Role に適切な permission がないパターンです。

今回作成したアプリケーションでは、以下の IAM Policy が付与された IAM Role を AssumeRole しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::{YOUR_BUCKET_NAME}/*",
            "Effect": "Allow"
        }
    ]
}

Signature などを生成したのち、この IAM Policy の Action の部分を s3:GetObject に変更して、POST 実行してみました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::{YOUR_BUCKET_NAME}/*",
            "Effect": "Allow"
        }
    ]
}
#### IAM Policy の Action 部分を s3:GetObject 更新した後に実行 
$ sh ./post.sh
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>AccessDenied</Code>
    <Message>Access Denied</Message>
    <RequestId>...(略)...</RequestId>
    <HostId>...(略)...</HostId>
</Error>
403

なお、この後、IAM Policy の内容を元に戻して実行すると、POST は成功しました。 Signature を生成した後で、IAM Policy を書き換えて実行すると、POST の成功・失敗が切り替わることから、AssumeRole した IAM Role の permission を、POST の実行時に評価していることがわかります。

まとめ

いかがでしたでしょうか? POST Policy を用いることで、Amazon S3 へのファイルアップロードの実装方法の選択肢が広がりそうですね!

最後に宣伝です📣

カミナシでは絶賛採用中です!一緒に最高のサービスを作っていく仲間を募集しています!

参考サイト

Appendix.1

本記事で使用した GO アプリケーションのコード全体です。

package main

import (
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "os"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/sts"
    "github.com/google/uuid"
    "github.com/joho/godotenv"
)

type Policy struct {
    Expiration string `json:"expiration"`
    Conditions []any  `json:"conditions"`
}

type Cond = map[string]string

type PostFormComponents struct {
    Bucket            string `json:"bucket"`
    ObjectKey         string `json:"objectKey"`
    Policy            string `json:"policy"`
    XAmzAlgorithm     string `json:"xAmzAlgorithm"`
    XAmzCredential    string `json:"xAmzCredential"`
    XAmzDate          string `json:"xAmzDate"`
    XAmzSignature     string `json:"xAmzSignature"`
    XAmzSecurityToken string `json:"xAmzSecurityToken"`
}

func main() {
    loadEnv()

    currentTime := time.Now()

    bucket := os.Getenv("AWS_BUCKET")
    objectKey := currentTime.Format("20060102") + "/" + generateUUIDv4()
    awsRegion := os.Getenv("AWS_REGION")
    awsService := "s3"
    algorithm := "AWS4-HMAC-SHA256"

    accessKeyId, secretAccessKey, sessionToken := getTemporarySecurityCredentials()

    xAmzCredential := createXAmzCredential(
        accessKeyId,
        currentTime,
        awsRegion,
        awsService,
    )
    xAmzDate := createXAmzDate(currentTime)

    /* Step1. Policy document の作成 */
    policy := Policy{
        Expiration: time.Now().Add(1 * time.Hour).Format("2006-01-02T15:04:05Z"),
        Conditions: []any{
            Cond{"bucket": bucket},
            Cond{"acl": "private"},
            []string{"eq", "$key", objectKey},
            []string{"starts-with", "$Content-Type", "image/"},
            []any{"content-length-range", 0, 10 * 1024},
            Cond{"x-amz-algorithm": algorithm},
            Cond{"x-amz-credential": xAmzCredential},
            Cond{"x-amz-date": xAmzDate},
            Cond{"x-amz-security-token": sessionToken},
        },
    }

    /* Step2. Signing Key の作成 */
    signingKey := createSigningKey(
        secretAccessKey,
        time.Now(),
        awsRegion,
        awsService,
    )

    /* Step3. policy document に対して署名 */
    base64EncodedSecurityPolicy := toBase64EncodedSecurityPolicy(policy)
    signature := createSignature(signingKey, base64EncodedSecurityPolicy)

    postBodyComponents := PostFormComponents{
        Bucket:            bucket,
        ObjectKey:         objectKey,
        Policy:            base64EncodedSecurityPolicy,
        XAmzAlgorithm:     algorithm,
        XAmzCredential:    xAmzCredential,
        XAmzDate:          xAmzDate,
        XAmzSignature:     signature,
        XAmzSecurityToken: sessionToken,
    }

    postBodyComponents.Output()
}

func getTemporarySecurityCredentials() (accessKeyId string, secretAccessKey string, sessionToken string) {
    input := &sts.AssumeRoleInput{
        RoleArn:         aws.String(os.Getenv("ROLE_ARN_TO_ASSUME")),
        RoleSessionName: aws.String("session-" + generateUUIDv4()),
    }
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        panic(err)
    }
    client := sts.NewFromConfig(cfg)
    result, err := client.AssumeRole(context.TODO(), input)
    if err != nil {
        panic(err)
    }

    return *result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken
}

func createXAmzCredential(accessKeyID string, date time.Time, awsRegion string, awsService string) string {
    return fmt.Sprintf("%s/%s/%s/%s/aws4_request", accessKeyID, date.Format("20060102"), awsRegion, awsService)
}

func createXAmzDate(date time.Time) string {
    return date.Format("20060102T000000Z")
}

func toBase64EncodedSecurityPolicy(policy Policy) string {
    policyJSON, err := json.Marshal(policy)
    if err != nil {
        panic(err)
    }
    return base64.StdEncoding.EncodeToString(policyJSON)
}

func createSigningKey(secretAccessKey string, date time.Time, awsRegion string, awsService string) []byte {
    // DateKey = HMAC-SHA256("AWS4" + "<SecretAccessKey>", "<yyyymmdd>")
    dateKey := hash([]byte("AWS4"+secretAccessKey), date.Format("20060102"))
    // DateRegionKey = HMAC-SHA256(DateKey, "<aws-region>")
    dateRegionKey := hash(dateKey, awsRegion)
    // DateRegionServiceKey = HMAC-SHA256(DateRegionKey, "<aws-service>")
    dateRegionServiceKey := hash(dateRegionKey, awsService)
    // SigningKey = HMAC-SHA256(DateRegionServiceKey, "aws4_request")
    signingKey := hash(dateRegionServiceKey, "aws4_request")
    return signingKey
}

func createSignature(signingKey []byte, stringToSign string) string {
    signature := hash(signingKey, stringToSign)
    return fmt.Sprintf("%x", signature)
}

func (p PostFormComponents) Output() {
    fmt.Println("==== PostFormComponents ====")
    exportToEnv("AWS_BUCKET", p.Bucket)
    exportToEnv("OBJECT_KEY", p.ObjectKey)
    exportToEnv("X_AMZ_CREDENTIAL", p.XAmzCredential)
    exportToEnv("X_AMZ_ALGORITHM", p.XAmzAlgorithm)
    exportToEnv("X_AMZ_DATE", p.XAmzDate)
    exportToEnv("POLICY", p.Policy)
    exportToEnv("X_AMZ_SIGNATURE", p.XAmzSignature)
    exportToEnv("X_AMZ_SECURITY_TOKEN", p.XAmzSecurityToken)
    fmt.Println("==== PostFormComponents ====")

}

func loadEnv() {
    err := godotenv.Load(".env")
    if err != nil {
        panic(err)
    }
}

func generateUUIDv4() string {
    return uuid.NewString()
}

func hash(secretKey []byte, message string) []byte {
    key := []byte(secretKey)
    h := hmac.New(sha256.New, key)
    h.Write([]byte(message))
    hashed := h.Sum(nil)
    return hashed
}

func exportToEnv(key, value string) {
    if value == "" {
        return
    }
    fmt.Println("export " + key + "=\"" + value + "\"")
}

Appendix.2

今回作成した Go アプリケーションの実行方法を紹介します。 今回は、AWS Cloud9 を利用して実行しました。

環境構築の大まかな手順は以下の通りです。

  1. Amazon S3 のバケット、AWS Cloud9 の EC2 にアタッチする IAM Role および、アプリケーションが、AssumeRole する IAM Role を作成する。
  2. Amazon Cloud9 環境を作成し、Go の実行環境を整える(参考
  3. Amazon Cloud9 の AWS Settings において、「AWS managed temporary credentials」を OFF にする。
  4. Amazon Cloud9 の EC2 インスタンスに、「1.」で作成した、EC2 用の IAM Role をアタッチする。
  5. Go アプリケーションの環境変数には、region および、上記までで作成した Amazon S3 のバケット名、アプリケーション内で AssumeRole するための IAM Role の ARN が設定されるようにする。

「1.」のリソースを作成する CloudFormation のテンプレート

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  S3BucketName:
    Type: String
  Cloud9EC2RoleName:
    Type: String
    Default: "my-post-policy-study-cloud9-ec2-role"
  PostObjectRoleName:
    Type: String
    Default: "my-post-policy-study-post-object-role"

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName
  Cloud9EC2Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      RoleName: !Ref Cloud9EC2RoleName
      Policies:
        - PolicyName: !Sub ${Cloud9EC2RoleName}-policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: 
                  - sts:AssumeRole
                Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/${PostObjectRoleName}
  Cloud9EC2RoleIAMInstanceProfile:
        Type: "AWS::IAM::InstanceProfile"
        Properties:
            Path: "/"
            InstanceProfileName: !Ref Cloud9EC2Role
            Roles:
              - !Ref Cloud9EC2Role
  PostObjectRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !GetAtt Cloud9EC2Role.Arn
            Action: sts:AssumeRole
      RoleName: !Ref PostObjectRoleName
      Policies:
        - PolicyName: !Sub ${PostObjectRoleName}-policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: 
                  - s3:PutObject
                Resource: !Sub arn:aws:s3:::${S3BucketName}/*

Outputs:
  RoleARN:
    Description: The ARN of the IAM role for your application to assume.
    Value: !GetAtt PostObjectRole.Arn

「5.」で設定する環境変数の例

AWS_REGION="ap-northeast-1"
AWS_BUCKET="{YOUR_BUCKET_NAME}"
ROLE_ARN_TO_ASSUME="arn:aws:iam::{YOUR_ACCOUNT_ID}:role/my-post-policy-study-post-object-role"