継続は力なり

タイトル通り定期的な更新を心掛けるブログです。

SendGrid のバウンスの発生を Golang で取得する

タダです.

前回 SendGrid API を叩いてバウンスが発生したメールと原因を NodeJS で取得したのを記事に書きました.今回は Golang で同様のことをやってみたのでその模様を備忘録としてまとめます.

sadayoshi-tada.hatenablog.com

バウンスが発生したの取得のコード

バウンスを発生の取得するコードは次のような物を作りました.クエリパラメータでバウンスが発生期間の開始と終了を使えるので,start_time が10分前の UNIXTIME で end_time がコード実行時間の UNIXTIME を取得して渡しています.SendGrid の API を叩くライブラリとして sendgrid-go があるのでこれを使って取得します.

package main

import (
        "encoding/json"
    "fmt"
    "strconv"
    "time"

    sendgrid "github.com/sendgrid/sendgrid-go"
)
type bounceDetail struct {
    Created int    `json:"created"`
    Email   string `json:"email"`
    Reason  string `json:"reason"`
    Status  string `json:"status"`
}

func getBounceMail(apiKey string) ([]bounceDetail, error) {
    var bounceDetail []bounceDetail
    startTime := time.Now().Add(-10 * time.Minute).Unix()
    endTime := time.Now().Unix()
    req := sendgrid.GetRequest(apiKey, "/v3/suppression/bounces", "https://api.sendgrid.com")
    req.Method = "GET"
    queryParams := make(map[string]string)
    queryParams["start_time"] = strconv.FormatInt(startTime, 10)
    queryParams["end_time"] = strconv.FormatInt(endTime, 10)
    req.QueryParams = queryParams
    res, err := sendgrid.API(req)
    if err != nil {
        fmt.Println(err.Error())
        return nil, err
    }
    if err := json.Unmarshal([]byte(res.Body), &bounceDetail); err != nil {
        fmt.Println(err)
        return nil, err
    }
    return bounceDetail, nil
}

SendGrid の API を叩いた時にレスポンスが下記のように返ってくるので bounceDetail という構造体でレスポンスを定義して呼び出し元に返すことをしています.あとは呼び出し元で必要な情報を取得して処理する事が可能です.

[
        {
          "created": 1443651125,
          "email": "testemail1@test.com",
          "reason": "550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient's email address for typos or unnecessary spaces. Learn more at  https://support.google.com/mail/answer/6596 o186si2389584ioe.63 - gsmtp ",
          "status": "5.1.1"
        },
        {
          "created": 1433800303,
          "email": "testemail2@testing.com",
          "reason": "550 5.1.1 <testemail2@testing.com>: Recipient address rejected: User unknown in virtual alias table ",
          "status": "5.1.1"
        }
]

まとめ

SendGrid の APIGolang で利用したのがはじめてだったのでその模様をまとめました.Golang を使うこと自体転職してからになるため,表現や書きっぷりに慣れていきたいし,学びをこのブログに投稿していければと思います.

SendGrid のバウンスの発生を取得する

タダです.

SendGrid を利用していると遭遇するのはバウンスメールだと思います.そこで,バウンスが発生しているメールアドレスがあったら検知する仕組みを検証したのでこの記事にまとめます.

バウンスメール発生検知の仕組み構成

今回の仕組みでは,Event Webhookを使ってもいいかと思ったんですが,即時性はそこまで必要ではなかったので EventBridge -> Lambda -> SendGrid API -> Slack 通知 を実行するようにしました. 検証では Lambda -> SendGrid API 部分を確認したかったため本記事もこの部分に焦点を当てます.

バウンスメールの取得

バウンスメールの取得は Bounce API で行うことができます.バウンスメールが存在すると,レスポンスが次のように返ってきます.引数で start_timeend_time があるので EventBridge で指定した時間と前回実行の間で発生したバウンスメールを取得しようと考えました.

サンプルイベント

[
        {
          "created": 1443651125,
          "email": "testemail1@test.com",
          "reason": "550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient's email address for typos or unnecessary spaces. Learn more at  https://support.google.com/mail/answer/6596 o186si2389584ioe.63 - gsmtp ",
          "status": "5.1.1"
        },
        {
          "created": 1433800303,
          "email": "testemail2@testing.com",
          "reason": "550 5.1.1 <testemail2@testing.com>: Recipient address rejected: User unknown in virtual alias table ",
          "status": "5.1.1"
        }
]

バウンスメールの取得のコード

SendGrid API ドキュメントを見ると多くの言語がサポートされていましたが, NodeJS で検証してみました.

const client = require('@sendgrid/client');
client.setApiKey("YOUR_API_KEY");
let date = new Date();
const startTime = Math.floor( date.getTime());
const endTime = Math.floor( date.getTime() - 300000 )
const req = {
    method: "POST",
    url: "/v3/suppression/bounces",
    start_time: startTime,
    end_time: endTime
}
let bounceDetailText = ""

try {
    const res = client.request(req)
    if (res.length === 0) {
        console.log('No Bounce mail.')
    } else {
        res.map( bounce,i => {
            bounceDetailText = "```\n対象email: " + bounce[i].email + "\nバウンスの原因: " + bounce[i].reason +"\n```"
        })
    }
    console.log(bounceDetailText)
    console.log('Check Bounce mail sucessfully')
} catch (error) {
    console.error('Failed to chek Bounce mail.')
}

バウンスメールが無ければ,No Bounce mail. と表示され,バウンスメールが有った場合,Slack に投稿するようメッセージ形式を整形している感じです.

まとめ

SendGrid のバウンスメールを取得するコードを検証したので,そのコードをまとめました.次は Slack への投稿まで運用に載せてみて記事にしたいと思います.

Aurora の特定ユーザー接続検知をするために CloudWatch Logs サブスクリプションフィルターを使う

タダです.

Aurora への接続ユーザーとして管理者ユーザーを使わないように運用していることが多いと思いますが,そんな中で管理者ユーザーで接続された場合ちゃんと検知しておきたいといった要件から検証した内容をこの記事にまとめます.

Aurora のログについて

普段 Aurora MySQL を使っていて接続ユーザーのログは監査ログに残っています.そのため,監査ログを CloudWartch Logs に出力できているのが前提です.

aws.amazon.com

CloudWatch Logs サブスクリプションフィルタ

監査ログは /aws/rds/cluster/cluster-name/audit で出力されています.このログから検知したいユーザーの接続ログをフィルタリングします.フィルタリングには CloudWatch Logs のサブスクリプションフィルタを使います.

docs.aws.amazon.com

フィルタパターンで管理者ユーザーの接続を絞りたいのですが,例えば admin ユーザーであればフィルタパターンでは admin CONNECT などで絞っていきました.加えて rdsadmin というデフォルトで作成されるユーザーのログも記録されるためこのユーザーのログは除外するために admin CONNECT - rdsadmin としました.これで必要な接続ログを送ることができました.

まとめ

簡単ですが,Aurora の接続ユーザーをログから検知するためにやった検証をまとめました.

AWS Lambda のデプロイを Terraform で行う

タダです.

Terraform で Lambda のデプロイをはじめてやったので,備忘録として記事に書いておきます.

Terraform のコード

Terraform のコードは以下のように書きました.Lambda のコードは Node.js を書いたのですが,lambda/hoge ディレクトリに置いておきます.これで terraform apply をしたら Lambda がデプロイされることを確認できました.

resource "aws_lambda_function" "hoge_functions" {
  function_name = "hoge"
  handler       = "index.handler"
  role          = aws_iam_role.hoge.arn
  filename      = data.archive_file.hoge.output_path
  timeout       = 30
  publish       = true

  source_code_hash = data.archive_file.hoge.output_base64sha256
  runtime          = "nodejs14.x"
}

data "archive_file" "hoge" {
  type        = "zip"
  output_path = "${path.module}/lambda/hoge/hoge.zip"
  source_file = "${path.module}/lambda/hoge/index.js"
}

まとめ

今まで AWS SAM や CDK で Lambda のデプロイしてきたのですが,Terraform でデプロイしていけることをしれました.パッケージなどインストールする必要などないみたいな場合はこういう手段でやっていくのを考えていきます.

RDS のパッチの有無を確認する方法

タダです.

RDS 運用しているとパッチが出てきて気づかなかったみたいなことがあります.そのために必要なことを調査をしたのでこの記事にまとめます.

RDS のパッチ適用通知を確認する方法

調査する前は EventBridge か RDS のイベントサブスクリプションからうけとれるのかと思っていました.しかし,これができずパッチを確認するためには DescribePendingMaintenanceActions API 経由で行います.

docs.aws.amazon.com

AWS CLI でやる場合は describe-pending-maintenance-actions コマンドで確認できます.

# イベントがない場合のレスポンス
$ aws rds describe-pending-maintenance-actions 
{
  "PendingMaintenanceActions": [
    {
      "ResourceIdentifier": "arn:aws:rds:us-west-2:123456789012:cluster:global-db1-cl1",
      "PendingMaintenanceActionDetails": [
        {
          "Action": "system-update",
          "Description": "Upgrade to Aurora PostgreSQL 2.4.2"
        }
      ]
    }
  ]
}

# jq で抽出
aws rds describe-pending-maintenance-actions | jq -r '.PendingMaintenanceActions[].ResourceIdentifier' 
arn:aws:rds:us-west-2:123456789012:cluster:global-db1-cl1
aws rds describe-pending-maintenance-actions | jq -r '.PendingMaintenanceActions[].PendingMaintenanceActionDetails[].Action'
system-update

JavaScriptSDK 経由で describePendingMaintenanceActions API を呼び出した場合は次のようなのでいけました.Node.js 16.x の Lambda で検証しました.

const AWS = require('aws-sdk')
const rds = new AWS.RDS();
const params = {
    Filters:[
        {
            Name: "db-cluster-id",
            Values: [
                "arn:aws:rds:ap-northeast-1:xxxx:cluster:hogedb"
            ]
        }
    ]
}
exports.handler = async(event) => {
    console.log("start")
    const hoge = rds.describePendingMaintenanceActions(params,function(err, data) {
        if (err) {
            console.log(err, err.stack)
        } else {
            console.log(data)
        }
    });
    return 'Finish rds patch chek function'
}

レスポンスデータは次のような情報が返ってきます.dataのセクションでパッチ適用があったらそのデータが入ります.

<ref *1> Request {
  domain: null,
  service: Service {
    config: Config {
      credentials: [EnvironmentCredentials],
      credentialProvider: [CredentialProviderChain],
      region: 'ap-northeast-1',
      logger: null,
      apiVersions: {},
      apiVersion: null,
      endpoint: 'rds.ap-northeast-1.amazonaws.com',
      httpOptions: [Object],
      maxRetries: undefined,
      maxRedirects: 10,
      paramValidation: true,
      sslEnabled: true,
      s3ForcePathStyle: false,
      s3BucketEndpoint: false,
      s3DisableBodySigning: true,
      s3UsEast1RegionalEndpoint: 'legacy',
      s3UseArnRegion: undefined,
      computeChecksums: true,
      convertResponseTypes: true,
      correctClockSkew: false,
      customUserAgent: null,
      dynamoDbCrc32: true,
      systemClockOffset: 0,
      signatureVersion: 'v4',
      signatureCache: true,
      retryDelayOptions: {},
      useAccelerateEndpoint: false,
      clientSideMonitoring: false,
      endpointDiscoveryEnabled: undefined,
      endpointCacheSize: 1000,
      hostPrefixEnabled: true,
      stsRegionalEndpoints: 'legacy',
      useFipsEndpoint: false,
      useDualstackEndpoint: false
    },
    isGlobalEndpoint: false,
    endpoint: Endpoint {
      protocol: 'https:',
      host: 'rds.ap-northeast-1.amazonaws.com',
      port: 443,
      hostname: 'rds.ap-northeast-1.amazonaws.com',
      pathname: '/',
      path: '/',
      href: 'https://rds.ap-northeast-1.amazonaws.com/'
    },
    _events: { apiCallAttempt: [Array], apiCall: [Array] },
    MONITOR_EVENTS_BUBBLE: [Function: EVENTS_BUBBLE],
    CALL_EVENTS_BUBBLE: [Function: CALL_EVENTS_BUBBLE],
    _clientId: 1
  },
  operation: 'describePendingMaintenanceActions',
  params: { Filters: [ [Object] ] },
  httpRequest: HttpRequest {
    method: 'POST',
    path: '/',
    headers: {
      'User-Agent': 'aws-sdk-nodejs/2.1083.0 linux/v16.14.0 exec-env/AWS_Lambda_nodejs16.x'
    },
    body: '',
    endpoint: {
      protocol: 'https:',
      host: 'rds.ap-northeast-1.amazonaws.com',
      port: 443,
      hostname: 'rds.ap-northeast-1.amazonaws.com',
      pathname: '/',
      path: '/',
      href: 'https://rds.ap-northeast-1.amazonaws.com/',
      constructor: [Function]
    },
    region: 'ap-northeast-1',
    _userAgent: 'aws-sdk-nodejs/2.1083.0 linux/v16.14.0 exec-env/AWS_Lambda_nodejs16.x'
  },
  startTime: 2022-06-17T06:23:02.561Z,
  response: Response {
    request: [Circular *1],
    data: null,
    error: null,
    retryCount: 0,
    redirectCount: 0,
    httpResponse: HttpResponse {
      statusCode: undefined,
      headers: {},
      body: undefined,
      streaming: false,
      stream: null
    },
    maxRetries: 3,
    maxRedirects: 10
  },
  _asm: AcceptorStateMachine {
    currentState: 'validate',
    states: {
      validate: [Object],
      build: [Object],
      afterBuild: [Object],
      sign: [Object],
      retry: [Object],
      afterRetry: [Object],
      send: [Object],
      validateResponse: [Object],
      extractError: [Object],
      extractData: [Object],
      restart: [Object],
      success: [Object],
      error: [Object],
      complete: [Object]
    }
  },
  _haltHandlersOnError: false,
  _events: {
    validate: [
      [Function (anonymous)],
      [Function],
      [Function: VALIDATE_REGION],
      [Function: BUILD_IDEMPOTENCY_TOKENS],
      [Function: VALIDATE_PARAMETERS]
    ],
    afterBuild: [
      [Function: COMPUTE_CHECKSUM],
      [Function],
      [Function: SET_CONTENT_LENGTH],
      [Function: SET_HTTP_HOST]
    ],
    restart: [ [Function: RESTART] ],
    sign: [ [Function (anonymous)], [Function], [Function] ],
    validateResponse: [ [Function: VALIDATE_RESPONSE], [Function (anonymous)] ],
    send: [ [Function] ],
    httpHeaders: [ [Function: HTTP_HEADERS] ],
    httpData: [ [Function: HTTP_DATA] ],
    httpDone: [ [Function: HTTP_DONE] ],
    retry: [
      [Function: FINALIZE_ERROR],
      [Function: INVALIDATE_CREDENTIALS],
      [Function: EXPIRED_SIGNATURE],
      [Function: CLOCK_SKEWED],
      [Function: REDIRECT],
      [Function: RETRY_CHECK],
      [Function: API_CALL_ATTEMPT_RETRY]
    ],
    afterRetry: [ [Function] ],
    build: [ [Function: buildRequest] ],
    extractData: [ [Function: extractData], [Function: extractRequestId] ],
    extractError: [ [Function: extractError], [Function: extractRequestId] ],
    httpError: [ [Function: ENOTFOUND_ERROR] ],
    success: [ [Function: API_CALL_ATTEMPT] ],
    complete: [ [Function: API_CALL] ]
  },
  emit: [Function: emit],
  API_CALL_ATTEMPT: [Function: API_CALL_ATTEMPT],
  API_CALL_ATTEMPT_RETRY: [Function: API_CALL_ATTEMPT_RETRY],
  API_CALL: [Function: API_CALL]
}

まとめ

RDS のパッチの有無を確認する方法を調べたのでまとめました.こういうのも EventBridge 経由で送られたらいいのになと思います.