Software engineering notes

Go 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: ...略...
        },
    },
}

BatchGetItem & GetItem key 不支援用 index, 只能用 primary key

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}
}

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),
        },
    },
}

Query 支援用 index, GetItem 不支援用 index

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 不會有回傳值

無法直接刪除一個 folder

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)

如果要刪除整個 folder, 要用 loop 再搭配 listObject 刪除, 直到 listObject 取不到東西為止

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"
}

最多一次只能取出 1000 個 item

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 模式下發信的,它有一些限制

要突破以上限制則需要另外向 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("test+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("Test user <test_user@gmail.com>"), // Required
    ReplyToAddresses: []*string{
        aws.String("test+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)