Jex’s Note

Golang - AWS

如何使用 AWS API

在使用它的任何一個 service 前要先準備好 credential 然候再建立 session,然候再跟 AWS services 互動,

Session 可以讓全部 AWS services 共用 (在使用各服務前會需要用 session 建立) ,最好 cache 起來,

每次要用之前再從 cache 拿出來, 避免每一次重新建立連線耗費資源。

[1] 初始化 credential

可以使用 aws-cli 指令 aws configure 幫你產生或手動建立檔案

~/.aws/config

[這裡填 profile name]
region = us-west-2
output = json

~/.aws/credentials

[這裡填 profile name]
aws_access_key_id = A******************A
aws_secret_access_key = 9**************************************V

常用的 credential 有幾種,以下會按照順序,哪個可以取到就使用

func GetAWSCredentialChain() (*credentials.Credentials, *aws.Config) {
    config := aws.NewConfig()
    var ProviderList []credentials.Provider = []credentials.Provider{
        &credentials.EnvProvider{},                                         # 讀取本機環境變數
        &credentials.SharedCredentialsProvider{                             # 讀取本機端實體檔案的 credentials
            Filename: "/Users/me/.aws/credentials",
            Profile:  "myProject",
        },
        &ec2rolecreds.EC2RoleProvider{                                      # IAM 賦與 EC2 Role 的權限
            Client: ec2metadata.New(session.New(), config),
        },
    }
    cred := credentials.NewChainCredentials(ProviderList)

    return cred, config
}

(或) credential 也可以直接帶入 access key 與 secret key

cred := credentials.NewStaticCredentials(accessKey, secretKey, ``)
svc := s3.New(session.New(),
    &aws.Config{
        Region:      aws.String(S3Region),
        Credentials: cred,
    },
)

[2] 初始化設定檔

func InitAWSConfig(region string) (*aws.Config, error) {
    cred, conf := GetAWSCredentialChain()
    val, err := cred.Get()
    if err != nil {
        logs.Error("InitAWSConfig error:", err)
    }
    logs.Debug("Cred ProviderName:", val.ProviderName)
    conf.WithRegion(region).WithCredentials(cred)
    return conf, nil
}

或直接返回 session

func NewSession(region string) *session.Session {
    cred, conf := GetAWSCredentialChain()
    conf.WithRegion(region).WithCredentials(cred)
    return session.New(conf)
}

[3] 建立 Session (e.g. dynamo db)

func GetDynamodbInstance() (*dynamodb.DynamoDB, error) {
    conf, err := InitAWSConfig("Dynamo DB 的 Region name e.g. us-west-2")
    if err != nil {
        logs.Error("GetDynamodbInstance error:", err)
        return nil, err
    }
    svc := dynamodb.New(session.New(), conf)

    return svc, nil
}

[4] 測試 (列出 DynamoDB 的 Table list)

svc, _ := services.GetDynamodbInstance()
result, err := svc.ListTables(&dynamodb.ListTablesInput{})
if err != nil {
    log.Println(err)
    return
}

log.Println("Tables:")
for _, table := range result.TableNames {
    log.Println(*table)
}

補充 :

sess := session.New(&aws.Config{
    Region:      aws.String("ap-northeast-1"),
    Credentials: credentials.NewSharedCredentials("/Users/home/.aws/credentials", "aws-cred-profile"),
    MaxRetries:  aws.Int(5),
})

svc := sqs.New(sess)

DynamoDB

型態

DynamoDB 有自定義的型態,像傳遞參數或接收參收要再將它的型態轉成我們熟悉的

B []byte `type:"blob"`

// A Boolean data type.
BOOL *bool `type:"boolean"`

// A Binary Set data type.
BS [][]byte `type:"list"`

// A List of attribute values.
L []*AttributeValue `type:"list"`

// A Map of attribute values.
M map[string]*AttributeValue `type:"map"`

// A Number data type.
N *string `type:"string"`

// A Number Set data type.
NS []*string `type:"list"`

// A Null data type.
NULL *bool `type:"boolean"`

// A String data type.
S *string `type:"string"`

// A String Set data type.
SS []*string `type:"list"`

GetItemInput

params = &dynamodb.GetItemInput{
    TableName: aws.String("user_contact"),
    Key: map[string]*dynamodb.AttributeValue{ // Required
        "user_id": { // Required
            S: aws.String(uid),
        },
    },
    AttributesToGet: []*string{             // 可省略,不加就是所有欄位都拿
        aws.String("contact_list"),
    },
}

svc, _ := services.GetDynamodbInstance()
resp, err := svc.GetItem(params)

contact_list := resp.Item["contact_list"].M
for key, val := range contact_list {
    logs.Info(key)
    logs.Info(val.S)
}

BatchGetItemInput

最多只可以取 100 筆

params = &dynamodb.BatchGetItemInput{
    RequestItems: map[string]*dynamodb.KeysAndAttributes{
        "user_contacts": {
            Keys: []map[string]*dynamodb.AttributeValue{
                {
                    "user_id": {S: aws.String("要查詢的 user_id")},
                },
                {
                    "user_id": {S: aws.String("要查詢的 user_id")},
                },
            },
            AttributesToGet: []*string{
                aws.String("contact_list"),
            },
        },
    },
}

svc, _ := services.GetDynamodbInstance()
resp, err := svc.BatchGetItem(params)

for _, val := range resp.Responses["user_contacts"] {
    contact_list := val["contact_list"].M           # 將型態轉回 Map
    for key, val := range contact_list {
        logs.Info(key)
        logs.Info(val.S)                            # 轉回 String
    }
}

補充 : 用迴圈組出 BatchGetItem 需要的參數

id_keys []map[string]*dynamodb.AttributeValue
for _, id := range ids {
    id_key := map[string]*dynamodb.AttributeValue{
        "id": {S: aws.String(id)},
    }
    id_keys = append(id_keys, id_key)
}

params := &dynamodb.BatchGetItemInput{
    RequestItems: map[string]*dynamodb.KeysAndAttributes{ // Required
        "users": { // Required
            Keys: id_keys,
            AttributesToGet: ...略...
        },
    },
}

PutItem

建立一個 list 裡面有兩個 map

var list []*dynamodb.AttributeValue
var item dynamodb.AttributeValue
item.M = map[string]*dynamodb.AttributeValue{
    "name":        {S: aws.String(name)},
}
list = append(list, &item)
list = append(list, &item)

params := &dynamodb.PutItemInput{
    TableName: aws.String("user_contacts"),
    Item: map[string]*dynamodb.AttributeValue{
        "name":         {S: aws.String("Tom")},
        "is_friend":    {BOOL: aws.Bool(true)},
        "contacts":     {L: list},
        "age":          {N: aws.String("15")},  // int 用 N (number), 但後面還是要轉成字串
    },
}

resp, err := svc.PutItem(params)    // 即使成功 resp 不會有回傳值

建立一個 Map{“time”: map{…}, “friends”: list: [map{…}]}

結構 :

    contacts : {
        "time": {
            "start": "",
            "end": "",
        },
        "friends": [
            {"name":"", "age":""},
            {"name":"", "age":""},
        ]
    }

var contacts, time map[string]*dynamodb.AttributeValue
var friends []*dynamodb.AttributeValue

// contacts - time
time = map[string]*dynamodb.AttributeValue{
    "start": {S: aws.String(start)},
    "end":   {S: aws.String(end)},
}

// contacts - friends
for _, d := range Friends {
    var item dynamodb.AttributeValue
    item.M = map[string]*dynamodb.AttributeValue{
        "name":     {S: aws.String(name)},
        "age":      {S: aws.String(age)},
    }
    friends = append(friends, &item)
}

// contacts (最外層)
contacts = map[string]*dynamodb.AttributeValue{
    "time":    {M: time},
    "friends": {L: friends},
}

Optional 的值,一定要宣告一個空物件,不要用 var

condition := map[string]*dynamodb.AttributeValue{}
if 是否有值 {
    time := &dynamodb.AttributeValue{
        M: map[string]*dynamodb.AttributeValue{
            "start": {S: aws.String(start)},
            "end":   {S: aws.String(end)},
        },
    }
    condition["time"] = time
}

params := &dynamodb.PutItemInput{
    TableName: aws.String("user_policy"),
    Item: map[string]*dynamodb.AttributeValue{
        "condition":   {M: condition},
    },
}

DeleteItem

params := &dynamodb.DeleteItemInput{
    TableName: aws.String("user_name"),
    Key: map[string]*dynamodb.AttributeValue{
        "user_id": {
            S: aws.String(t.UserID),
        },
    },
}

resp, err := svc.DeleteItem(params)     // 即使成功 resp 不會有回傳值

UpdateItem

Update 使用上每次都是整筆資料更新會比較簡單一點,也就是當沒有資料時,也要給它一個空物件,這樣 Update 時就可以把欄位刪除了,沒有空物件會引發錯誤

// info 為 optional 的值
info := map[string]*dynamodb.AttributeValue{}
friend_list []*string = []*string{aws.String(user_id)}

params := &dynamodb.UpdateItemInput{
    Key: map[string]*dynamodb.AttributeValue{ // Required
        "uid": { // Required
            S: aws.String(uid),
        },
    },
    TableName:        aws.String("user"), // Required
    UpdateExpression: aws.String(`
        SET map.#key = :key,
            #updated_at = :updated_at,
            #a_map = :a_map
        ADD #friend_list :friend_list
    `),
    ExpressionAttributeNames: map[string]*string{ // Required
        "#key":           aws.String("xxxkeyxxx"),      // map 如果沒有存在的 key 會新增, 但是 map 的欄位一定要先存在, 否則會有錯誤
        "#updated_at":    aws.String("updated_at"),
        "#a_map":         aws.String("a_map"),
        "#friend_list":   aws.String("friend_list"),
    },
    ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
        ":key":           {S: aws.String("xxxvalue")},
        ":updated_at":    {S: aws.String(update_at)},
        ":a_map":         {M: info},
        ":friend_list":   {SS: friend_list},
    },
    ReturnValues: aws.String("UPDATED_NEW"),            // (optional) 回傳更新後的資料
}

resp, err = svc.UpdateItem(params)      // 即使成功 resp 不會有回傳值

更新巢狀資料下的某個值, 假設 friends 下有很多 friend_id 為 key 的 map, UpdateExpression 這樣寫:

SET friends.#friend_id.name = :val

(optional) info map

if 當有資料再更新 {
    var time map[string]*dynamodb.AttributeValue
    time = map[string]*dynamodb.AttributeValue{
        "start": {S: aws.String(p.Condition.Time.Start)},
        "end":   {S: aws.String(p.Condition.Time.End)},
    }
    info["time"] = &dynamodb.AttributeValue{M: time}
}
  • 不存在的資料會新增
  • UpdateExpression 不能 ADD 一個 item 到 Map, 必須用 SET,但前提是欄位要先存在

update expression 其他說明

SET list[0] = :val1
REMOVE #m.nestedField1, #m.nestedField2
ADD aNumber :val2, anotherNumber :val3
DELETE aSet :val4

SET list[0] = :val1 REMOVE #m.nestedField1, #m.nestedField2 ADD aNumber :val2, anotherNumber :val3 DELETE aSet :val4

Query

第一種方法 : dynamodb 除了 GetItem (用 partition key 取得資料) 也可以使用其中某個欄位取得,不過要先到 Dynamodb 的 AWS Console 上對那個欄位建立 index

params := &dynamodb.QueryInput{
    TableName:              aws.String("users"), // Required
    IndexName:              aws.String("user_id-index"),
    ConsistentRead:         aws.Bool(false),
    KeyConditionExpression: aws.String("#user_id = :user_id"),
    ExpressionAttributeNames: map[string]*string{
        "#user_id": aws.String("user_id"), // Required
    },
    ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
        ":user_id": { // Required
            S: aws.String(user_id),
        },
    },
}

ExpressionAttributeNames 也可以只寫成這樣

KeyConditionExpression: aws.String("user_id = :user_id"),
ExpressionAttributeValues:  map[string]*dynamodb.AttributeValue{
    ':user_id': {
        S: aws.String(user_id),
    },
},

ExpressionAttributeNames 代入 int : 最然對程式來說他是 int 但是實際上還是要用 string,只不過要指定 N (Number)

ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
    ":num": {N: aws.String("1")}
}

resp, err = svc.Query(params)

如果沒有資料,不會引起 err 喔! 要記得判斷 resp 是否為 nil

第二種方法 : 假設你有設 partition key 及 sort key ,但你只知道 partition key 不知道 sort key 你會沒辦法用 GetItem,這時也可以用 Query 直接對 partition key 取資料

params := &dynamodb.QueryInput{
    TableName:      aws.String(table), // Required
    ConsistentRead: aws.Bool(false),
    KeyConditionExpression: aws.String("#user_id = :user_id"),
    ExpressionAttributeNames: map[string]*string{
        "#user_id": aws.String("user_id"), // Required
    },
    ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
        ":user_id": { // Required
            S: aws.String(user_id),
        },
    },
}

Scan

用來取這個 Table 的全部資料, 不可以設定條件 (where)

轉換 dynamodb 格式

從 dynamodb 取出來的資料有它自已的格式,在取的時候有時候蠻麻煩的,sdk 有提供轉換格式的函式

如果用 Marshal/Unmarshal User struct,它會優先依照 tag dynamodbav 將值 Map 到 struct 欄位,其次才是 tag json

GetItem (struct)

type User struct {
    Name string `json:"name" dynamodbav:"user_name"`  // 避開 dynamodb 保留字
}
var u User
err = dynamodbattribute.UnmarshalMap(resp.Item, &u)

var m map[string]interface{}
err = dynamodbattribute.UnmarshalMap(resp.Item, &m)

BatchGetItem

// Specific table (user_contacts)
var m []map[string]interface{}
err = dynamodbattribute.UnmarshalListOfMaps(resp.Responses["user_contacts"], &m)

// All table
var m map[string][]map[string]interface{}
m = make(map[string][]map[string]interface{})
for k, v := range resp.Responses {
    var tmp []map[string]interface{}
    err = dynamodbattribute.UnmarshalListOfMaps(v, &tmp)
    if err != nil {
        return nil, err
    }
    m[k] = tmp
}

Query

var m []map[string]interface{}
err = dynamodbattribute.UnmarshalListOfMaps(resp.Items, &m)

S3

PutObject

params := &s3.PutObjectInput{
    Bucket: aws.String("bucket_name"),
    Key:    aws.String("file_name"),
    Body:   bytes.NewReader([]byte("json_str")),
}
svc, err := GetS3Instance()
resp, err := svc.PutObject(params)

resp :
    {
      ETag: "\"b8468dbe0941b5164253860813663edf\""
    }

需要注意的是成功上傳後的檔案預設的 ACL 都是 private, 除非你的 bucket 有設定對放開放, 不然就要指定 ACL, 可參考官方文件

DeleteObject

params := &s3.DeleteObjectInput{
    Bucket: aws.String("bucket_name"),
    Key:    aws.String("file_name"),
}
svc, err := GetS3Instance()
resp, err := svc.DeleteObject(params)  // 即使成功 resp 不會有回傳值

DeleteObjects

params := &s3.DeleteObjectsInput{
    Bucket: aws.String(bucket),
    Delete: &s3.Delete{
    Objects: []*s3.ObjectIdentifier{
        {
            Key: aws.String("objectkey1"),
        },
        {
            Key: aws.String("objectkey2"),
        },
    },
}
svc, err := GetS3Instance()
resp, err = svc.DeleteObjects(params)

ListObject

params := &s3.ListObjectsInput{
    Bucket: aws.String(bucket),
    Prefix: aws.String(path),
    MaxKeys: aws.Int64(2),          // [Optional] 限制一次拿出來的數量, 最多 1,000 (同時也是預設值)
}
result, err = s.Service.ListObjects(params)

result:

{
  Contents: [
    {
      ETag: "\"e******************************8\"",
      Key: "test-dir/2.png",
      LastModified: 2017-11-20 10:20:50 +0000 UTC,
      Owner: {
        DisplayName: "testqa",
        ID: "b**************************************************************6"
      },
      Size: 14688,
      StorageClass: "STANDARD"
    },
    {
        ...
    }
  ],
  IsTruncated: false,
  Marker: "",
  MaxKeys: 1000,
  Name: "test-bucket",
  Prefix: "test-dir"
}

GetObject

params := &s3.GetObjectInput{
    Bucket: aws.String("bucket_name"),
    Key:    aws.String("file_name"),
}
svc, err := GetS3Instance()
resp, err = svc.GetObject(params)
json_str, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(json_str))

// Dump resp
(*s3.GetObjectOutput)(0xc420468000)({
  AcceptRanges: "bytes",
  Body: buffer(0xc42034e040),
  ContentLength: 633,
  ContentType: "binary/octet-stream",
  ETag: "\"34d8d42271944aa866145dbeb550dd86\"",
  LastModified: 2016-09-26 08:12:23 +0000 UTC,
  Metadata: {

  }
})

HeadObject

可以用來判斷 object 是否存在在 s3

params := &s3.HeadObjectInput{
    Bucket: aws.String(bucket),
    Key:    aws.String(key),
}
res, err = svc.HeadObject(params)

HeadObjectOutput

{
  AcceptRanges: "bytes",
  ContentLength: 80936,
  ContentType: "image/jpeg",
  ETag: "\"7a6e371115538ae1a8b836d1cfd8fc3b\"",
  LastModified: 2018-02-12 09:37:14 +0000 UTC,
  Metadata: {

  }
}

GetObjectRequest (pre-signed url for downloading - GET)

svc, err := GetS3Instance()
req, _ := svc.GetObjectRequest(&s3.GetObjectInput{
    Bucket: aws.String("bucket_name"),
    Key:    aws.String("file_path"),
})
pre_url, err = req.Presign(time.Duration(10) * time.Second)     // within 10 seconds for downloading file

PutObjectRequest (pre-signed url for uploading - PUT)

svc, err := GetS3Instance()
req, _ := svc.PutObjectRequest(&s3.PutObjectInput{
    Bucket: aws.String("bucket_name"),
    Key:    aws.String("file_path"),
})
pre_url, err := req.Presign(15 * time.Minute)                   // within 15 minutes for uploading file

關於 ACL, 不知道為什麼參數加上 ACL 指定 public-read 當上傳時 AWS 會回 403 錯誤訊息為 SignatureDoesNotMatch, 後來解法是上傳成功後再去 call PutObjectAcl 改變 ACL

curl 測試是否可以上傳

curl -v -T /tmp/test.mp4 "https://my_bucket.s3-us-west-2.amazonaws.com/videos/test.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAJXSKW3C6...略..."

CopyObject

可從一個 bucket 裡的檔案 copy 到另一個 bucket 下

svc, err := GetS3Instance()
_, err = svc.CopyObject(&s3.CopyObjectInput{
    CopySource: aws.String(from_path),          // 注意!! 來源的組成是 {bucket}/{file_path}
    Bucket:     aws.String(bucket),
    Key:        aws.String(to_path),
    ACL:        aws.String("public-read"),      // optional
})

需要注意的是成功上傳後的檔案預設的 ACL 都是 private, 除非你的 bucket 有設定對放開放, 不然就要指定 ACL, 可參考官方文件

PutObjectAcl

svc, err := GetS3Instance()
params := &s3.PutObjectAclInput{
    Bucket: aws.String(bucket),
    Key:    aws.String(file_path),
    ACL:    aws.String("public-read"),
}
_, err = svc.PutObjectAcl(params)

CloudFront

pre-signed url

產生的 pre-signed url 是 custom domain 而不是 aws s3 的 domain

前置作業請參考 aws cloudfront - pre-signed url + custom domain 設定

以上完成後,到這裡應該已經將 private key 上傳到主機了, 就可以開始實作 :

file_url := "https://cdn.your-custom-domain.com/test.jpg"                                                              // custom domain + S3 file
key_id := "APK**************Y2A"                                                                        // Access Key ID
priv_key, err := sign.LoadPEMPrivKeyFile("/tmp/cloudfront-private_key-pk-APK**************Y2A.pem")     // Private key path
if err != nil {
    logs.Debug("load pem err: %s", err.Error())
} else {
    signer := sign.NewURLSigner(key_id, priv_key)
    signed_url, err := signer.Sign(file_url, time.Now().Add(15*time.Second))
    if err != nil {
        logs.Debug("Failed to sign url, err: %s\n", err.Error())
    }
    logs.Info("signed_url: %s", signed_url)
}

signed url :

https://cdn.your-custom-domain.com/test.jpg?Expires=1500302808&Signature=mhh8YmrYMs91Cc4qoTeDSUjOeQChe-U7Ksm0Ue92WJufMlKkEAOHR3GeoEaoc3nSpitA5KV-4op6EePTfYG8DMqr-J8Oh55gCNGMjicaiMdz~VOCEoSUTeYgLFnj-dQT5OGjdg~iELDX5LROZ2UL~5vJgSKrlgiH2VLp4WMO~AoDe~CiZAWtQ49Jbrx1XZtVX3i9lCDAL4881psx8xt7W4dANJ0uo1oelBo5P0BhM0v400un9UT4FG-ZYrXB1iDYszwxhLx4TWZSa2MWXWTJyXzeZwcVcbulvdP7apokPC5aMrLaPfel6v22HSFAEP62Unety01SN4HWYtLCW7v9VQ__&Key-Pair-Id=APKAIZQ4PTQ4P7ZNQY2A

SQS

Send Message / Receive Message / Delete Message

參考 : golang-aws-sqs-example

Send Message Batch Request

var entries []*sqs.SendMessageBatchRequestEntry
for i := 1; i <= 5; i++ {
    f := sqs.SendMessageBatchRequestEntry{ // Required
        Id:          aws.String(fmt.Sprintf("Message_%d", i)),  // Required, 數字 英文 - _  而且 ID 不可重覆
        MessageBody: aws.String("message body"),                 // Required
    }
    entries = append(entries, &f)
}
params := &sqs.SendMessageBatchInput{
    Entries:  entries,
    QueueUrl: aws.String(QueueUrl), // Required
}
resp, err := svc.SendMessageBatch(params)

Get queue attributes

params := &sqs.GetQueueAttributesInput{
    QueueUrl: aws.String(QueueUrl), // Required
    AttributeNames: []*string{
        aws.String("All"), // Required, 填要取得的欄位,`All` 是全取
        // More values...
    },
}
resp, err := svc.GetQueueAttributes(params)

Set queue attributes

params := &sqs.SetQueueAttributesInput{
    Attributes: map[string]*string{
        "ReceiveMessageWaitTimeSeconds": aws.String("0"),
    },
    QueueUrl: aws.String(QueueUrl), // Required
}
_, err := svc.SetQueueAttributes(params) // 成功不會返回內容

設定 RedrivePolicy (retry 機制)

Attributes: map[string]*string{
    // 刪除
    "RedrivePolicy": aws.String(""),

    // 最多一個 message 收到 3 次,超過就會送到 dead letter queue
    "RedrivePolicy": aws.String("{\"maxReceiveCount\":\"3\", \"deadLetterTargetArn\":\"arn:aws:sqs:ap-northeast-1:3**********2:MyDeadLetterQueue\"}"),
},

Change visibility timeout

// Change visibility timeout
change_params := &sqs.ChangeMessageVisibilityInput{
    QueueUrl:          aws.String(QueueUrl),  // Required
    ReceiptHandle:     message.ReceiptHandle, // Required
    VisibilityTimeout: aws.Int64(0),          // Required
}
_, err = w.Svc.ChangeMessageVisibility(change_params) // 成功不會返回內容

CloudWatch

PutMetricData

自已推一些數據讓 cloudwatch 幫你監控;上報 metric,不需要去 CloudWatch 設定,如果是新的 metric,它自已會新增

支援一次上報多個 metric data, 所以以下設計成多個

// Metric collection
metric_collection := map[string]float64{}{
    "success": 1,
}

// (optional) Dimensions
dimensions := map[stirng]string{}{
    "job_type": "curl",
}

// New metrict data input
params := &cloudwatch.PutMetricDataInput{
    Namespace: aws.String(namespace), // Required
}

// Give value to every metric data.
for k, v := range metric_collection {
    metric_data := &cloudwatch.MetricDatum{
        MetricName: aws.String(k), // Required
        Timestamp:  aws.Time(time.Now()),
        Value:      aws.Float64(v),
    }

    if len(dimensions) > 0 {
        for k, v := range dimensions {
            dimension := &cloudwatch.Dimension{Name: aws.String(k), Value: aws.String(v)}
            metric_data.Dimensions = append(metric_data.Dimensions, dimension)
        }
    }
    params.MetricData = append(params.MetricData, metric_data)
}
_, err = c.Service.PutMetricData(params)

SES

AWS 為了防止自已的 mail server 被當作濫發 email 的工具,所以目前我們都是在 SES sandbox 模式下發信的,它有一些限制

  • 要先到 SES 後台 Email Addresses 新增你的 email,認證後才可以寄信, 收信也是
  • 一天最多 200 封信,每秒最多一封

要突破以上限制則需要另外向 AWS 申請

SendEmail

svc := ses.New(sess)
params := &ses.SendEmailInput{
    Destination: &ses.Destination{ // Required
        // BccAddresses: []*string{
        //  aws.String("Address"), // Required
        // },
        // CcAddresses: []*string{
        //  aws.String("Address"), // Required
        // },
        ToAddresses: []*string{
            aws.String("jex+to@gmail.com"), // Required, 如果傳進 slice 改用 aws.StringSlice
        },
    },
    Message: &ses.Message{ // Required
        Body: &ses.Body{ // Required !! Html / Text 擇一使用就好
            // Html: &ses.Content{
            //     Data:    aws.String("Test html content"), // Required
            //     Charset: aws.String("utf-8"),
            // },
            Text: &ses.Content{
                Data:    aws.String("Test raw content"), // Required
                Charset: aws.String("utf-8"),
            },
        },
        Subject: &ses.Content{ // Required
            Data:    aws.String("Test subject"), // Required
            Charset: aws.String("utf-8"),
        },
    },
    Source: aws.String("Jex Lin <jex@gmail.com>"), // Required
    ReplyToAddresses: []*string{
        aws.String("jex+reply@gmail.com"), // Required
    },
}

resp, err := svc.SendEmail(params)
if err != nil {
    return errors.New("SES response error: " + err.Error())
}

resp :

(*ses.SendEmailOutput)(0xc42002c0a0)({
  MessageId: "010101581e1837c2-e0c68369-e7c4-47e4-b01e-3f7f6afca529-000000"
})

Sendor (from) 只支援 Ascill, 如果要改用 utf-8 字元要改成

=?utf-8?B?V2ktRmnjgqvjg6Hjg6k=?= <noreply@example.com>

SNS

Topics - Publish to topic

先建立一個 Topic 然候再 Subscribe 它,選擇你要使用什麼收到你訂閱的東西, 最簡單是用 email 的方式 - 填上自已的 email 後,你需要收信驗證,驗證完後只要有人 publish 到這個 topic 就會收到 email 了

params := &sns.PublishInput{
    Message:  aws.String("message"), // Required
    TopicArn: aws.String("arn:aws:sns:ap-northeast-1:4**********7:event_update"),
}

resp, err := svc.Publish(params)

if err != nil {
    return
}

resp :

{
  MessageId: "f56bf715-2584-5fe4-8f0a-a7b9c0c2c757"
}

Applications - Push Notification

先去 SNS 的 Applications 註冊 Push Notification 的服務,並把 ARN 記下來, 手機裡的 App 會有個 UUID (app 跟 gcm/apns 註冊拿到的),帶這個上來到 Server, 拿這個 UUID 向 SNS 註冊 Token (createPlatformEndpoint 帶上面註冊 SNS 的 ARN, 及 app UUID, enabled: true (enabled 預設是 false, 所以要改成 true)),會拿到 EndpointArn, 建議把這個 Token 存下來,以便日後再發送時使用, 註冊完後 AWS 的 SNS web UI 後台就有一筆 record ,也可以直接用 web UI 發送 notification 做測試, 每筆 record 後面都有 enabled 值,如果是 false 就代表不能推送,只要 SNS 推送一次但送不成功後就會把它改成 false, 後端要推送只要對 EndpointArn 發送 message 就可以了,格式可以選擇 raw 或 json

GCM 可以帶 title, 但 APNS 的 title 預設是 application name

GCM

{ "GCM": "{ \"notification\": { \"body\": \"test body\",\"title\": \"test titel\",\"icon\": \"test icon\" },\"data\": { \"custom_field\": \"custom_value\" } }" }

APNS or APNS_SANDBOX (dev)

 { "APNS":"{\"aps\":{\"alert\":\"Hello World!!\"},\"custom_field\": \"custom_value\"}" }
 { "APNS_SANDBOX":"{\"aps\":{\"alert\":\"Hello World!!\"},\"custom_field\": \"custom_value\"}" }

上面 payload 要注意的是最後總共要 json encode 兩次 (最外層 GCM/APNS key 的值已經先被 json encode 過一次了)

Code :

params := &sns.PublishInput{
    Message:          aws.String(message), // Required
    TargetArn:        aws.String(target_arn),
    MessageStructure: aws.String("json"),
}

_, err = s.Service.Publish(params)

Rekognition

DetectLabels (image file)

ff, _ := os.Open("test.jpg")
defer ff.Close()
bin = make([]byte, 500000)
ff.Read(bin)

params := &rekognition.DetectLabelsInput{
    Image: &rekognition.Image{
        Bytes: []byte(bin),
    },
    MaxLabels:     aws.Int64(5),
    MinConfidence: aws.Float64(1.0),
}
resp, err = svc.DetectLabels(params)

DetectLabels (s3)

params := &rekognition.DetectLabelsInput{
    Image: &rekognition.Image{
        S3Object: &rekognition.S3Object{
            Bucket: aws.String("bucket"),
            Name:   aws.String("file_path"),
        },
    },
    MaxLabels:     aws.Int64(5),
    MinConfidence: aws.Float64(1.0),
}
resp, err = svc.DetectLabels(params)

Comments