AWS day2: Amazon S3 × SDK for Go

f:id:dombri:20200505210310j:plain

こんにちは。
今回はAWS奮闘記第二弾!ということで、Amazon S3 の理解を深めようと思います。


本日の内容は以下です。


目標

  • Amazon S3 でファイルの保存と取得
    • AWS SDK for Go を前回 よりスムーズに(2/3 の時間)で書く
    • S3のサービス内容や構成、使い所を知る


参考


1. 10分間ハンズオンをやってみる

毎度ですが、まずはサービスの雰囲気を掴むに最適なハンズオンを進めていきます。

ハンズオン:ファイルの保存と取得

画像を貼り付けるのが面倒だったので、GUIも見たい!という方は上記ハンズオンで進めることをお勧めします(所々画像が古くなっているっぽいのでご注意)。

1) Amazon s3 コンソールに入る

検索バーから「s3」で検索する


2) S3バケットを作成する

バケット ファイルを保存するためのコンテナのこと。

  • [バケットを作成]をクリックし、バケット名を入力 バケット名はリージョンで一意であることなど、制約がいくつかある

  • リージョンを選択

  • [ブロックパブリックアクセスのバケット設定]
    デフォルト: [パブリックアクセスを全てブロック]
    ACL AzureでいうNSG(ネットワークに関する許可・拒否制限をかけることでセキュリティ強化を行う機能)のようなものと思われる

  • 詳細設定 ここではデフォルト[オブジェクトロック] [無効]

  • [バケットを作成] をクリックする

バケットはあっという間に作成できました。


3) ファイルのアップロード・ダウンロード

説明を書くまでもなさそう・・・画面に表示されている通りに操作すればバケットにファイルをアップロードできます。

アップロード

  • バケット名を選択し、バケットに移動
  • [アップロード]を選択
  • [ファイルを追加]をクリックし[次へ]を選択
  • アクセス許可に関する設定を行い(ここではデフォルトのまま)[次へ]
  • プロパティを設定する
    • ストレージクラス 冗長性やアクセスの頻度でクラス化され、それに基づいた料金体系となる
    • 暗号化
    • メタデータ
    • タグ
  • [アップロード]を選択

GUIで簡単にファイルをアップロードできました。

ダウンロード


4) オブジェクトとバケットを削除

どちらもs3コンソールから削除できる。

簡単に削除できました。


2. AWS SDK for Goでやってみる

例のごとくAWS SDK for Go でS3を操作してみようと思います。

ただ作るだけだと面白くない(ハードルを上げている)ので、以下の操作をS3バケットでやってみます。

  1. S3バケットをリスト表示
  2. S3バケットを作成
  3. ファイルをアップロード
  4. ファイルをダウンロード
  5. ファイルを削除
  6. S3バケットを削除

Reference は こちら

ここから先やりたいことは、ほとんど 公式Docs に記載されていました。さすがS3の情報は豊富だな(← Lightsail で苦戦した人)


前提条件

  • AWS SDK for Go のインストール
  • AWS Access Keys がある
  • 認証情報が credential file で設定されている

上記の手順については day1 で触れていますのでご参考までに。


1) S3バケットをリスト表示する

まずはS3バケットのリスト表示をGoでやってみます。処理の流れは以下。

  1. SDKが認証情報をロードし、S3サービスクライアントを作成するためにSessionをイニシャライズ
  2. S3サービスクライアントを作成
  3. ListBucketsバケット一覧を取得

list_s3bucket.go

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/s3"

  "fmt"
  "os"
)

func main() {
  // Initialize the session that the SDK uses to load credencials from the shared credencial file, and create a new Amazon S3 service client
  sess, err := session.NewSession(&aws.Config{
      Region: aws.String("ap-northeast-1")},
  )

  // Create S3 service client
  svc := s3.New(sess)

  // Call ListBuckets
  result, err := svc.ListBuckets(nil)
  if err != nil {
      exitErrorf("Unable to list buckets, %V", err)
  }

  fmt.Println("Buckets:")

  // loop through the buckets, printing the name and creation date of each bucket
  for _, b := range result.Buckets {
      fmt.Printf("* %s created on %s\n",
          aws.StringValue(b.Name), aws.TimeValue(b.CreationDate))
  }

}

// we use this function to display errors and exit
func exitErrorf(msg string, args ...interface{}) {
  // prefix F: specify the place where the function write
  fmt.Fprintf(os.Stderr, msg+"\n", args...)
  // exit with exit code 1
  os.Exit(1)
}

上記を実行してみます。

❯ go run list_s3bucket.go
Buckets:
* domb-ri-test-bk created on 2020-05-14 13:13:03 +0000 UTC

GUIで作ったバケットが表示されました。


2) S3 バケットの作成

次に、S3バケットを作成してみます。処理の流れは以下。

  1. SDKが認証情報をロードし、S3サービスクライアントを作成するためにSessionをイニシャライズ
  2. S3サービスクライアントを作成
  3. CreateBucketバケットを作成

create_s3bucket.go

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/s3"

  "fmt"
  "os"
)

func main() {
  // The program requires one argument, the name of the bucket to create
  if len(os.Args) != 2 {
      exitErrorf("Bucket name missing!\nUsage: %s bucket_name", os.Args[0])
  }

  bucket := os.Args[1]

  sess, err := session.NewSession(&aws.Config{
      Region: aws.String("ap-northeast-1")},
  )

  svc := s3.New(sess)

  // Call CreateBucket, passing in the bucket name defined previously
  _, err = svc.CreateBucket(&s3.CreateBucketInput{
      Bucket: aws.String(bucket),
  })
  if err != nil {
      exitErrorf("Unable to create bucket %q, %v", bucket, err)
  }

  // Wait until bucket is created before finishing
  fmt.Printf("Waiting for bucket %q to be created...\n", bucket)

  err = svc.WaitUntilBucketExists(&s3.HeadBucketInput{
      Bucket: aws.String(bucket),
  })

  if err != nil {
      exitErrorf("Error occurred while waiting for bucket to be created, %v", bucket)
  }
  fmt.Printf("Bucket %q successfully created\n", bucket)

}

func exitErrorf(msg string, args ...interface{}) {
  fmt.Fprintf(os.Stderr, msg+"\n", args...)
  os.Exit(1)
}
  • os.Argsコマンドラインのパラメータを受け取れる
    • [0] 実行したコマンド名
    • [1]~ コマンドに渡された引数
  • _(アンダースコア) 変数は、宣言はするけど使わない変数
    • Goでは宣言した変数を一度も使わないとエラーになる
  • 書式指定子(verb)%... で表す 参考: Qiita
    • %V値をデフォルトのフォーマットで出力
    • %q ここではGoの文法上のエスケープをした文字列を出力


上記を実行してみます。実行時に、バケット名を記載します。

> go run create_s3bucket.go new-bucket-domb-ri
Waiting for bucket "new-bucket-domb-ri" to be created...
Bucket "new-bucket-domb-ri" successfully created

❯ go run list_s3bucket.go
Buckets:
* domb-ri-test-bk created on 2020-05-14 13:13:03 +0000 UTC
* new-bucket-domb-ri created on 2020-05-20 01:51:13 +0000 UTC

新しいバケットが作成できていることを確認しました。

3) バケットにファイルをアップロード

バケットにファイルをアップロードする動作をSDKでやってみます。ファイル名は実行時に指定します。

処理の流れは以下。

  1. バケット名を受け取ったかをチェック
  2. SDKが認証情報をロードし、S3マネージャをセットアップするためにSessionをイニシャライズ
  3. バケットにファイルをアップロード


upload_s3obj.go

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/s3/s3manager"

  "fmt"
  "os"
)

func main() {
  // Get the bucket and file name from the command line arguments
  if len(os.Args) != 3 {
      exitErrorf("Bucket name missing!\nUsage: %s bucket_name", os.Args[0])
  }

  bucket := os.Args[1]
  filename := os.Args[2]
  
  file, err := os.Open(filename)
  if err != nil {
    exitErrorf("Unable to open file %q, %v", err)
  }
  
  // Defer the execution of Close()
  defer file.Close()

  sess, err := session.NewSession(&aws.Config{
      Region: aws.String("ap-northeast-1")},
  )

  // Setup the S3 Upload Manager
  // http://docs.aws.amazon.com/sdk-for-go/api/service/s3/s3manager/#NewUploader
  uploader := s3manager.NewUploader(sess)

  // Call CreateBucket, passing in the bucket name defined previously
  _, err = uploader.Upload(&s3manager.UploadInput{
      Bucket: aws.String(bucket),
      Key: aws.String(filename),
      Body: file,
  })
  if err != nil {
      exitErrorf("Unable to upload %q to %q, %v", filename, bucket, err)
  }
  
  fmt.Printf("Successfully uploaded %q to %q\n", filename, bucket)

}

func exitErrorf(msg string, args ...interface{}) {
  fmt.Fprintf(os.Stderr, msg+"\n", args...)
  os.Exit(1)
}
  • os.Open() で、引数で指定したファイルを読み込み専用モードでオープン
  • defer にクローズする処理を登録することで、ファイルのクローズ処理を確実に行わせる
    →リソース破棄の処理が漏れたり、分散する問題を防ぐ


上記を実行します。実行時には、バケット名・ファイル名を指定します。

# 先に作ったバケットと、適当に用意したファイルを指定
❯ go run upload_s3obj.go new-bucket-domb-ri up_test.txt
Successfully uploaded "up_test.txt" to "new-bucket-domb-ri"


AWSコンソールから up_test.txt がアップロードされていることを確認しました(恐らくSDKで表示する方法もあるがここでは割愛します) 。


4) バケットからファイルをダウンロードする

バケットに配置してあるファイルをダウンロードします。
処理の流れは以下。

  1. バケット名、ファイル名が指定されているか確認して設定
  2. 新規ファイルを作成
  3. SDKが認証情報をロードし、NewDownloader オブジェクト作成のためSessionをイニシャライズ
  4. バケットからファイルをダウンロード


download_s3obj.go

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/s3"
  "github.com/aws/aws-sdk-go/service/s3/s3manager"

  "fmt"
  "os"
)

func main() {
  // Get the bucket and file name from the command line arguments
  if len(os.Args) != 3 {
      exitErrorf("Bucket and item names required\nUsage: %s bucket_name, item_name", os.Args[0])
  }

  bucket := os.Args[1]
  item := os.Args[2]
  
  // Create a new file
  file, err := os.Create(item)
  if err != nil{
    exitErrorf("Unable to open file %q, %v", item, err)
  }
  
  // Defer the execution of Close()
  defer file.Close()

  sess, _ := session.NewSession(&aws.Config{
      Region: aws.String("ap-northeast-1")},
  )

  downloader := s3manager.NewDownloader(sess)

  // Download the item from the bucket
  numBytes, err := downloader.Download(file,
      &s3.GetObjectInput{
          Bucket: aws.String(bucket),
          Key: aws.String(item),
      })
  
  if err != nil {
      exitErrorf("Unable to download item %q, %v", item, err)
  }
  
  fmt.Printf("Downloaded", filename(), numBytes, "bytes")
}

func exitErrorf(msg string, args ...interface{}) {
  fmt.Fprintf(os.Stderr, msg+"\n", args...)
  os.Exit(1)
}
  • os.Create() で新規ファイルを作成する
    • 同名のファイルが存在する場合は上書きされるので注意


上記を実行します。バケット名・ファイル名の順に指定します。

※最終的に成功したファイルの中身を書いてますが、 file が undifined だと怒られてしばらく苦闘しました。単純にfileを宣言してなかっただけでした。

# ファイルをダウンロードする
❯ go run download_s3obj.go new-bucket-domb-ri up_test.txt
Downloaded%!(EXTRA string=up_test.txt, int64=6, string=bytes)
~/.aws
❯ ls
download_s3obj.go           up_test.txt    


ファイルがダウンロードできました。


5) バケット内ファイルを削除する

最後は一掃して、不要なお金がかからないようにしておきましょう。まずはファイルの削除から。

処理の流れは以下。

  1. バケット名、ファイル名が指定されているか確認して設定
  2. SDKが認証情報をロードし、S3 サービスクライアントを作成するためにSessionをイニシャライズ
  3. ファイルを削除


delete_file_s3.go

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/s3"
  
    "fmt"
    "os"
)

func main() {
  // Get the name of the bucket and object to delete
  if len(os.Args) != 3{
    exitErrorf("Bucket and obj name required¥nUsage: %s bucket_name object_name",
              os.Args[0])
  }

  bucket := os.Args[1]
  obj := os.Args[2]

  sess, err := session.NewSession(&aws.Config{
        Region: aws.String("ap-northeast-1")},
    )

  svc := s3.New(sess)

  // Call DeleteObject
  _, err = svc.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(obj)})
  if err != nil {
    exitErrorf("Unable to delete obj %q from bucket %q, %v", obj, bucket, err)
  }

  err = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{
    Bucket: aws.String(bucket),
    Key: aws.String(obj),
  })

  fmt.Printf("Obj %q successfully deleted¥n", obj)
}

func exitErrorf(msg string, args ...interface{}) {
  fmt.Fprintf(os.Stderr, msg+"\n", args...)
  os.Exit(1)
}
  • DeleteObject() により指定したファイルを削除する


上記を実行します。バケット名・ファイル名の順で指定します。

❯ go run delete_file_s3.go new-bucket-domb-ri up_test.txt
Obj "up_test.txt" successfully deleted¥n


ファイルが削除できました。


6) バケットの削除

綺麗さっぱり無くしたいので、バケットも消しちゃいます。
処理の流れは以下。

  1. バケット名を設定
  2. SDKが認証情報をロードし、S3 サービスクライアントを作成するためにSessionをイニシャライズ
  3. バケットを削除


delete_s3obj.go

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/s3"
  
    "fmt"
    "os"
)

func main() {
  // Get the name of the bucket to delete
  if len(os.Args) != 2{
    exitErrorf("bucket name required¥nUsage: %s bucket_name", os.Args[0])
  }
  
  bucket := os.Args[1]
  
  sess, err := session.NewSession(&aws.Config{
    Region: aws.String("ap-northeast-1")},
  )
  
  svc := s3.New(sess)
  
  _, err = svc.DeleteBucket(&s3.DeleteBucketInput{
    Bucket: aws.String(bucket),
  })
  if err != nil {
    exitErrorf("Unable to delete bucket %q, %v", bucket, err)
  }
  
  fmt.Printf("Waiting for bucket %q to be deleted...¥n", bucket)
  
  err = svc.WaitUntilBucketNotExists(&s3.HeadBucketInput{
    Bucket: aws.String(bucket),
  })
  
  if err != nil {
    exitErrorf("Error occurred while waiting for bucket to be deleted, %v", bucket)
  }
  
  fmt.Printf("Bucket %q successfully deleted¥n", bucket)
}

func exitErrorf(msg string, args ...interface{}) {
    fmt.Fprintf(os.Stderr, msg+"\n", args...)
    os.Exit(1)
}


上記を実行してみます。

❯ go run delete_s3obj.go new-bucket-domb-ri
Waiting for bucket "new-bucket-domb-ri" to be deleted...¥n
Error occurred while waiting for bucket to be deleted, new-bucket-domb-ri
exit status 1

あらら、エラーが発生?

ひとまずもう一度やってみます。

❯ go run delete_s3obj.go new-bucket-domb-ri
Unable to delete bucket "new-bucket-domb-ri", NoSuchBucket: The specified bucket does not exist
    status code: 404, request id: E46A45A26717EBE6, host id: fxDaSXZgSND1NttlTnU...
exit status 1

消えてる・・・謎だ・・・

❯ go run delete_s3obj.go domb-ri-test-bk
Wating for bucket "domb-ri-test-bk" to be deleted...¥nBucket "domb-ri-test-bk" successfully deleted¥n
~/.aws

別のバケットで試したら成功しました。なんだったんだろう・・・ 今回の目的は果たせたので、深追いはしないことにします。


終わりに

まずはS3バケットの使い勝手をGUIで確かめてから、S3バケットや配置するファイルに対して AWS SDK for Go を用いて操作してみました。
必ず必要になってくる操作手順は以下でした。

  • バケット名・ファイル名が指定されているか確認して情報を取得
  • SDKが認証情報をロード
  • Sessionをイニシャライズ
  • エラー時のハンドリング

前提条件を満たしているかチェックし、AWSを操作する権限のあるものからのアクセスであることを証明する認証情報をセットし、Sessionを開始して処理を行う・・・
と、当たり前っちゃあ当たり前なのですが「お作法」を学ぶのはプログラミング学習の基本だと思います。非常に勉強になりました。

当初の目標と実績を比較すると、

  • AWS SDK for Go を前回 よりスムーズに(2/3 の時間)で書く

まあ概ね達成できたかと思います。エラーの出力方法がわかったのは大きな前進でした。
そらで書けるようになるには道のりが長いので、しばらくは写経したり読み込んだりして目と手を慣らしていこうと思います。

  • S3のサービス内容や構成、使い所を知る

工夫次第で如何様にも使える便利なストレージサービス、といったところでしょうか。静的Webサイトに使えたり、大小様々なファイルを管理できるみたいなのでアプリの規模感にも柔軟に対応できそうです(お値段の幅もかなり広そう・・・)


というわけで、AWS奮闘記第二弾、これでおしまい! @kuromitsu_ka さん、めっちゃ遅れてごめんなさい!!!