こんにちは( ̄^ ̄)ゞ
最近、スプラトゥーン3とPhasmophobiaにハマっている大久保です。
次回のスプラトゥーンフェスは「くさ」で行こうかと思っていますー

タイトルの通り、AuroraのMySQLとPostgreSQLで利用できる「データベースアクティビティストリーミング」を有効化し、Athenaで良い感じに検索できる設定方法について書きたいと思います。

ただ1つの記事にするにはボリュームがありすぎるため、

の3つにわけたいと思います。

今回は「~パート1~データベースアクティビティストリーミングとは」になります。

DASとは?


Database Activity Streamsの略

DASはAuroraRDS for Oracleで利用できる監査ログの出力機能になります。
ほぼリアルタイムで監査ログをストリーミングすることが出来、他ツールと連携を行い監査・監視を行うことが出来ます。
仕組みとしては、以下の画像のように複数のAWSサービスを利用することで監査ログのストリーミングを行っています。

画像引用元:データベースアクティビティストリーミングの機能

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/DBActivityStreams.Overview.html

Kinesis Data Streamsは、Auroraの設定からKMSのカスタマー管理キーを指定しDASを有効化することで、自動的に作成されます。
ログの保存を行いたい場合は、別途Kinesis Data FirehoseとS3バケットを手動で作成、連携させることで実現することが出来ます。

特徴

私が思うDASの特徴は以下になります。

  1. MySQL、PostgreSQLのどちらでもほぼ同様の設定で監査ログの取得ができる。
  2. 他ツールと連携することで、監査ログの分析をほぼリアルタイムで行うことができる。
  3. 非同期、同期モードがある。
  4. KMSを用いてログが暗号化されるため、どこかのタイミングで復号化処理を実装する必要がある

4の特徴について、復号化処理は自前で作成する必要があります。
復号化のタイミングはKinesisFirehoseに連携後、Labmdaで行ったり、ファイルに保存した後で行うことも可能なようです。
公式のブログに復号化のサンプルコードがあるため、複雑な要件でなければサンプルコードを少し編集するだけで実装出来そうです。

Lambda function
The Lambda function is invoked by Kinesis Data Firehose. We configure the Lambda event source after we create the function. Aurora activity data is Base64 encoded and encrypted using an AWS Key Management Service (AWS KMS) key. For this post, we decrypt the data and store it in an appropriate folder in the S3 bucket based on activity type. However, we recommend encrypting the activity data before storing it in the folder, or enabling encryption at the bucket level. We also discard heartbeat events and store other events in specific folders like CONNECT, DISCONNECT, ERROR, and AUTH FAILURE. Pay attention to comments within the Lambda code because they highlight activities like decryption, filtering, and putting data in a specific folder. For instructions on creating a Lambda function, see the AWS Lambda Developer Guide.

The following is the sample Lambda function code in Python 3.8 using Boto3. This code depends on the aws_encryption_sdk library, which needs to be packaged as part of the Lambda code. aws_encryption_sdk depends on the cryptography library, which is OS dependent. For Lambda, Linux cryptography libraries should be included in the Lambda package.

When choosing an existing or new Lambda execution role, make sure role has access to the S3 bucket, Kinesis Data Firehose, and AWS KMS. It’s always a best practice to follow the principle of least privileges.

The following Lambda code can be found at AWS Samples on GitHub:

https://aws.amazon.com/jp/blogs/database/filter-amazon-aurora-database-activity-stream-data-for-segregation-and-monitoring/

DASの要件


Auroraの場合

DASを利用するには、いくつかの要件があります。
下記にAuroraでDASを利用する場合の要件を記載しました。 ※2022/10/17日時点

  • KMSのカスタマー管理キーを事前に作成している。
  • 対象のAuroraクラスターがKMSにアクセス出来る必要がある。
  • 利用しているAuroraのエンジンバージョンが、以下のいずれかに該当している。
    • PostgreSQL
      • 14、13 および 12 のすべてのバージョン
      • バージョン 11.6、バージョン 11 以降
      • バージョン 10.11、バージョン 10 以降
    • MySQL
      • 2.08 以上。
  • 利用しているインスタンスタイプが以下のうちのいずれかに該当している。
    • PostgreSQL
      • db.r6g
      • db.r5
      • db.r4
      • db.x2g
    • MySQL
      • db.r6g
      • db.r5
      • db.r4
      • db.r3
      • db.x2g

うっかりポイントは、インスタンスタイプです。
検証環境はコストがかからないようにT系のインスタンスをつかっていることが多いと思います。
T系を使っていることを忘れ、DASが有効化出来ない…どうしてだろう…と私はなったので、みなさんも気をつけてください。
より詳細な要件は下記のドキュメントを参照してください。

データベースアクティビティストリームの要件

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/DBActivityStreams.Overview.html#DBActivityStreams.Overview.requirements

ハマるポイントは、KMSのアクセスについてです。
ログを暗号化する際にKMSを使うのですが、KMSを使うためにはAuroraがKMSにアクセス出来る必要があります。
KMSへアクセスが出来ないと、ログの暗号化が行えずエラーになり、ストリーミングが失敗します。
基本的にDBはプライベートサブネットに配置すると思いますが、この場合、KMSへアクセスさせるため以下のどちらかの設定が必要になります。

  • Auroraクラスターが配置されているサブネットのルーティングテーブルに、NATゲートウェイのルートを追加する。
  • Auroraクラスターが配置されているサブネットに、KMSのVPCエンドポイントを作成する。

詳しい情報については下記のドキュメントを参照してください。

プライベートアベイラビリティの前提条件

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/DBActivityStreams.Prereqs.html

DASのログ


DASのログはJSON形式で出力されます。
MySQLとPostgreSQLで一部項目が異なりますが、ほぼ同様なログが出力されます。
また公式のドキュメントにも記載がありますが、将来的に新しいフィールドの追加、既存のフィールドの削除等の変更の可能性を考慮しておく必要があります。

MySQLの場合

MySQLの場合は以下のようなログになります。

{
    "logTime": "2022-10-26 08:46:59.892621+00",
    "type": "record",
    "clientApplication": null,
    "pid": 8468,
    "dbUserName": "test",
    "databaseName": "test",
    "remoteHost": "172.16.1.193",
    "remotePort": "0",
    "command": "QUERY",
    "commandText": "SELECT * FROM `settings`",
    "paramList": null,
    "objectType": "TABLE",
    "objectName": "",
    "statementId": 159717094,
    "substatementId": 1,
    "exitCode": "0",
    "sessionId": "2365080",
    "rowCount": 1,
    "serverHost": "test-db",
    "serverType": "MySQL",
    "serviceName": "Amazon Aurora MySQL",
    "serverVersion": "8.0.mysql_aurora.3.02.0",
    "startTime": "2022-10-26 08:46:59.892378+00",
    "endTime": "2022-10-26 08:46:59.892576+00",
    "transactionId": "0",
    "dbProtocol": "MySQL",
    "netProtocol": "TCP",
    "errorMessage": "",
    "class": "MAIN"
}

各項目については、公式ドキュメントから引用した情報を記載します。

項目説明
logtime監査コードパスに記録されているタイムスタンプ。これは、協定世界時 (UTC) 形式で表されます。
ステートメントの期間を計算する最も正確な方法については、startTime および endTime フィールドを参照してください。
typeイベントタイプ。
有効な値は record または heartbeat です。
ClientApplicationクライアントのレポートどおりにクライアントが接続に使用していたアプリケーション。
クライアントはこの情報を指定する必要はないため、値は null でも問題ありません。
pidクライアント接続を処理するために割り当てられているバックエンドプロセスのプロセス ID。
データベースサーバー再起動すると、pid が変更され、statementId フィールドのカウンターが初期からやり直されます。
dbUserNameクライアントが認証したデータベースユーザー。
databaseNameユーザーが接続したデータベース。
remoteHostSQL ステートメントを発行したクライアントの IP アドレスまたはホスト名。
Aurora MySQL では、どちらが使用されるかは、データベースの skip_name_resolve パラメータ設定によって異なります。
この値 localhost は、rdsadmin スペシャルユーザーからのアクティビティを示します。
remotePortクライアントのポート番号。
commandSQL ステートメントの一般的なカテゴリ。このフィールドの値は class の値によって異なります。

class が MAIN の場合の値には、次の値が含まれます。
・CONNECT – クライアントセッションが接続されている場合。
・QUERY – SQL ステートメント。class の AUX 値が 1 つ以上のイベントを伴います。
・DISCONNECT – クライアントセッションが切断されている場合。
・FAILED_CONNECT – クライアントが接続を試みたが、接続できない場合。
・CHANGEUSER – 発行するステートメントではなく、MySQL ネットワークプロトコルの一部である状態の変更。

class が AUX の場合の値には、次の値が含まれます。
・READ – SELECT ステートメントまたは COPY ステートメント (ソースがリレーションまたはクエリの場合)。
・WRITE – INSERT、UPDATE、DELETE、TRUNCATE、COPY ステートメント (送信先がリレーションの場合)。
・DROP – オブジェクトの削除。
・CREATE – オブジェクトの作成。
・RENAME – オブジェクトの名前の変更。
・ALTER – オブジェクトのプロパティの変更。
commandTextclass の MAIN 値を持つイベントの場合、このフィールドはユーザーが渡した実際の SQL ステートメントを表します。
このフィールドは、接続レコードまたは切断レコードを除くすべてのタイプのレコードに使用されます。
この場合、値は null です。
class の AUX 値を持つイベントの場合、このフィールドには、イベントに関係するオブジェクトに関する補足情報が含まれます。
Aurora MySQL では、引用符などの文字の前には、エスケープ文字を表すバックスラッシュが付きます。

【重要
各ステートメントの SQL テキスト全体が、機密データを含む監査ログに表示されます。
ただし、Aurora が次の SQL ステートメントのようにコンテキストから判断できる場合、データベースユーザーのパスワードは編集されます。
paramListこのフィールドは Aurora MySQL には使用されず、常に null です。
objectTypeテーブル、インデックスなどのデータベースオブジェクトタイプ。
このフィールドは、SQL ステートメントがデータベースオブジェクトに対して機能する場合にのみ使用されます。
SQL ステートメントがオブジェクトに対して機能していない場合、この値は null です。

Aurora MySQL の有効な値には次のようなものがあります。
・INDEX
・TABLE
・UNKNOWN
objectNameデータベースオブジェクトの名前 (SQL ステートメントを使用している場合)。
このフィールドは、SQL ステートメントがデータベースオブジェクトに対して機能する場合にのみ使用されます。
SQL ステートメントがオブジェクトに対して機能していない場合、この値は空白になります。
オブジェクトの完全修飾名を作成するには、databaseName と objectName を結合します。
クエリに複数のオブジェクトが含まれる場合、このフィールドはカンマで区切られた名前のリストにすることができます。
statementIdクライアントの SQL ステートメントの識別子。
カウンターは、クライアントによって SQL ステートメントが入力される度に増加します。
DB インスタンスが再起動されると、カウンターがリセットされます。
substatementIdSQL サブステートメントの識別子。
この値は、クラス MAIN を持つイベントの場合は 1、クラス AUX を持つイベントの場合は 2 です。
statementId フィールドを使用して、同じステートメントによって生成されたすべてのイベントを識別します。
exitCodeセッション終了レコードに使用される値。
clean exit では、終了コードが含まれます。
エラーシナリオによっては、終了コードが常に得られるとは限りません。
このような場合、この値はゼロになるか、空白になる可能性があります。
sessionId一意の疑似セッション識別子。
rowCountSQL ステートメントによって影響を受けた、または取得されたテーブルの行数。
このフィールドは、データ操作言語 (DML) ステートメントである SQL ステートメントでのみ使用されます。
SQL ステートメントが DML ステートメントではない場合、この値は null です。
serverHostデータベースサーバーインスタンス識別子。
この値は、Aurora MySQL とAurora PostgreSQLでは異なって表されます。
Aurora PostgreSQL は、識別子の代わりに IP アドレスを使用します。
serverTypeデータベースサーバーのタイプ (例: MySQL)。
serviceNameサービスの名前。
現在、この値は常に Aurora MySQL の Amazon Aurora MySQL です
serverVersionデータベースサーバーのバージョン。
startTimeSQL ステートメントの実行がスタートされた時刻。
これは、協定世界時 (UTC) 形式で表されます。
SQL ステートメントの実行時間を計算するには、endTime - startTime を使用します。
endTime フィールドも参照してください。
endTimeSQL ステートメントの実行が終了した時刻。
これは、協定世界時 (UTC) 形式で表されます。
SQL ステートメントの実行時間を計算するには、endTime - startTime を使用します。
startTime フィールドも参照してください。
transactionIdトランザクションの識別子。
dbProtocolデータベースプロトコル。
現在、この値は常に Aurora MySQL の MySQL です。
netProtocolネットワーク通信プロトコル。
現在、この値は常に Aurora MySQL の TCP です。
errorMessageエラーがあった場合、このフィールドには DB サーバーによって生成されるはずのエラーメッセージが表示されます。
エラーにならなかった通常のステートメントの場合、errorMessage の値は null です。

エラーは、重要度が ERROR 以上の、クライアントで表示される MySQL エラーログイベントを生成するアクティビティとして定義されます。
詳細については、MySQL リファレンスマニュアルの「エラーログ」を参照してください。
例えば、構文エラーやクエリのキャンセルは、エラーメッセージを生成します。

バックグラウンドのチェックポインタープロセスエラーなどの内部の MySQL サーバーエラーは、エラーメッセージを生成しません。
ただし、ログの重要度レベルの設定に関係なく、このようなイベントのレコードは引き続き出力されます。
これにより、攻撃者がログをオフにして検出を回避することを防ぎます。
exitCode フィールドも参照してください。
classアクティビティイベントのクラス。
Aurora MySQL の有効な値は以下のとおりです。

MAIN – SQL ステートメントを表すプライマリイベントです。

AUX – 追加の詳細を含む補足イベント。例えば、オブジェクトの名前を変更したステートメントには、新しい名前を反映するクラス AUX を持つイベントがあります。

同じステートメントに対応する MAIN イベントと AUX イベントを検索するには、pid フィールドと statementId フィールドの値が同じである、異なるイベントがないか確認します。

Aurora MySQL の databaseActivityEventList フィールド

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/DBActivityStreams.Monitoring.html#DBActivityStreams.AuditLog

個人的なポイントは、タイムスタンプ系項目がUTC固定のところです。
これはクラスターパラメーターグループのタイムゾーンを変更しても固定になるため、必要に応じて分析する際に変換しないといけないです。

PostgreSQLの場合

PostgreSQLの場合は以下のようなログになります。

{
    "logTime": "2022-10-27 14:00:53.674229+09",
    "statementId": 35306,
    "substatementId": 6,
    "objectType": "TABLE",
    "command": "SELECT",
    "objectName": "public.queries",
    "databaseName": "test",
    "dbUserName": "test",
    "remoteHost": "172.16.49.165",
    "remotePort": "35914",
    "sessionId": "6359cba0.54df",
    "rowCount": 1,
    "commandText": "SELECT $1,$2,$3,$4 FROM list",
    "paramList": [
        "1_id",
        "2_name",
        "3_age",
        "4_address"
    ],
    "pid": 21727,
    "clientApplication": "Passenger RubyApp: test",
    "exitCode": null,
    "class": "READ",
    "serverVersion": "12.8.2",
    "serverType": "PostgreSQL",
    "serviceName": "Amazon Aurora PostgreSQL-Compatible edition",
    "serverHost": "172.16.252.130",
    "netProtocol": "TCP",
    "dbProtocol": "Postgres 3.0",
    "type": "record",
    "startTime": "2022-10-27 14:00:53.674127+09",
    "errorMessage": null
}

各項目については、公式ドキュメントから引用した情報を記載します。

項目説明
logTime監査コードパスに記録されているタイムスタンプ。
これは、SQL ステートメントの実行終了時刻を表します。
startTime フィールドも参照してください。
statementIdクライアントの SQL ステートメントの識別子。
カウンターはセッションレベルであり、クライアントによって SQL ステートメントが入力される度に増加します。
substatementIdSQL サブステートメントの識別子。
この値は、statementId フィールドで識別された各 SQL ステートメントに含まれるサブステートメントをカウントします。
objectTypeテーブル、インデックス、ビューなどのデータベースオブジェクトタイプ。
このフィールドは、SQL ステートメントがデータベースオブジェクトに対して機能する場合にのみ使用されます。
SQL ステートメントがオブジェクトに対して機能していない場合、この値は null です。
有効な値には次のようなものがあります。

・COMPOSITE TYPE
・FOREIGN TABLE
・FUNCTION
・INDEX
・MATERIALIZED VIEW
・SEQUENCE
・TABLE
・TOAST TABLE
・VIEW
・UNKNOWN
commandSQL コマンドの名前 (コマンドの詳細は含まない)。
objectNameデータベースオブジェクトの名前 (SQL ステートメントを使用している場合)。
このフィールドは、SQL ステートメントがデータベースオブジェクトに対して機能する場合にのみ使用されます。
SQL ステートメントがオブジェクトに対して機能していない場合、この値は null です。
databaseNameユーザーが接続したデータベース。
dbUserNameクライアントが認証したデータベースユーザー。
remoteHostクライアントの IP アドレスまたはホスト名。
Aurora PostgreSQL では、どちらが使用されるかは、データベースの log_hostname パラメータ設定によって異なります。
remotePortクライアントのポート番号。
sessionId一意の疑似セッション識別子。
rowCountSQL 文によって返された行数。
例えば、SELECT ステートメントが 10 行を返す場合、rowCount は 10 になります。
INSERT ステートメントまたは UPDATE ステートメントの場合、RowCount は 0 です。
commandTextユーザーによって渡された実際の SQL ステートメント。
Aurora PostgreSQL の場合、値は元の SQL ステートメントと同じです。
このフィールドは、接続レコードまたは切断レコードを除くすべてのタイプのレコードに使用されます。
この場合、値は null です。
paramListSQL ステートメントに渡されるカンマ区切りのパラメータの配列。
SQL ステートメントにパラメータがない場合、この値は空の配列です。
pidクライアント接続を処理するために割り当てられているバックエンドプロセスのプロセス ID。
clientApplicationクライアントのレポートどおりにクライアントが接続に使用していたアプリケーション。
クライアントはこの情報を指定する必要はないため、値は null でも問題ありません。
exitCodeセッション終了レコードに使用される値。
clean exit では、終了コードが含まれます。
エラーシナリオによっては、終了コードが常に得られるとは限りません。
例えば、PostgreSQL で exit() を実行している場合や、オペレーターが kill -9 などのコマンドを実行している場合があります。
エラーが発生した場合は、PostgreSQL エラーコードにリストされている SQL エラーコード (SQLSTATE) が exitCode フィールドに表示されます。
errorMessage フィールドも参照してください。
classアクティビティイベントのクラス。Aurora PostgreSQL の有効な値は以下のとおりです。

・ALL
・CONNECT – 接続イベントまたは切断イベント。
・DDL – ROLE クラスのステートメントのリストに含まれていない DDL ステートメント。
・FUNCTION – 関数の呼び出し、または DO ブロック。
・MISC – さまざまなコマンド (例: DISCARD、FETCH、CHECKPOINT、VACUUM)。
・NONE
・READ – SELECT ステートメントまたは COPY ステートメント (ソースがリレーションまたはクエリの場合)。
・ROLE – ロールと特権に関するステートメント (例: GRANT、REVOKE、CREATE、ALTER、DROP、ROLE)。
・WRITE – INSERT、UPDATE、DELETE、TRUNCATE、COPY ステートメント (送信先がリレーションの場合)。
serverVersionデータベースサーバーのバージョン (例: Aurora PostgreSQL の 2.3.1)。
serverTypeデータベースサーバーのタイプ (例: PostgreSQL)。
serviceNameサービスの名前 (例: Amazon Aurora PostgreSQL-Compatible edition)。
serverHostデータベースサーバーのホスト IP アドレス。
netProtocolネットワーク通信プロトコル。
dbProtocolデータベースプロトコル (例: Postgres 3.0)。
typeイベントタイプ。
有効な値は record または heartbeat です。
startTimeSQL ステートメントの実行がスタートされた時刻。
SQL ステートメントのおおよその実行時間を計算するには、logTime - startTime を使用します。
logTime フィールドも参照してください。
errorMessageエラーがあった場合、このフィールドには DB サーバーによって生成されるはずのエラーメッセージが表示されます。
エラーにならなかった通常のステートメントの場合、errorMessage の値は null です。

エラーは、重要度が ERROR 以上の、クライアントで表示される PostgreSQL エラーログイベントを生成するアクティビティとして定義されます。
詳細については、「PostgreSQL メッセージの重要度」を参照してください。
例えば、構文エラーやクエリのキャンセルは、エラーメッセージを生成します。

バックグラウンドのチェックポインタープロセスエラーなどの内部の PostgreSQL サーバーエラーは、エラーメッセージを生成しません。
ただし、ログの重要度レベルの設定に関係なく、このようなイベントのレコードは引き続き出力されます。
これにより、攻撃者がログをオフにして検出を回避することを防ぎます。

exitCode フィールドも参照してください。

Aurora PostgreSQL の databaseActivityEventList フィールド

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/DBActivityStreams.Monitoring.html#DBActivityStreams.AuditLog

個人的には以下の2点がMySQLと違っていると思います。

  • パラメーターの詳細が出力される。
  • タイムスタンプ系がUTC固定ではない。

タイムスタンプ系について、正しいかは不明ですがクラスターパラメーターグループで指定したタイムゾーンに準拠する形になると思います。

DASのコスト【東京リージョンの場合】


DASを利用することで以下のサービスのコストがかかると想定されます。
気にするべきところは赤文字になっている部分です。

  • Kinesis
    • Data Firehose
      • Tier 1 $0.036 per GB of data read from Kinesis Data Streams
      • Tier 1 $0.036 per GB of data ingested
    • Data Streams ※オンデマンドの場合
      • $0.052 per Stream Hour Used
      • $0.104 per GB of data written
      • $0.052 per GB of data read using GetRecords Consumer
  • Lambda
  • KMS
    • $1 per customer managed KMS key version in Asia Pacific (Tokyo)
    • $0.03 per 10000 KMS requests in Asia Pacific (Tokyo)
  • S3
  • VPC
    • KMSエンドポイント ※作成していれば
      • $0.014 per VPC Endpoint Hour
  • GuardDuty ※有効にしていれば
    • $0.00000472 per CloudTrail event analyzed in Asia Pacific (Tokyo) region
  • Detective ※有効にしていれば
    • USD 2.70 per GB for the first 1,000GB / month of data analyzed Asia Pacific (Tokyo) region

Kinesis Data Streamsについては、シャードの起動時間に対して課金が発生します。
大体月に$40ほど固定で掛かるイメージです。
データの取り込み、転送の料金についてはお安めの金額なため、ログの量が多くなっても爆発的な課金の増加はないと思います。

Amazon Kinesis Data Streams の料金

https://aws.amazon.com/jp/kinesis/data-streams/pricing/

KMSについては、リクエスト数に対する課金がネックになりそうな気がしています。
DASでは、暗号化と復号化のどちらでもリクエストを行い、またリクエストは一つのログごとに行っていると見受けられます。
そのためKMSのリクエストがかなり増加しやすく、課金が高額になりやすいです。

AWS Key Management Service の料金

https://aws.amazon.com/jp/kms/pricing/

GuardDutyとDetectiveについて、設定によってはかなり高額な課金が発生する場合があります。
先ほど話した記載したKMSのリクエストの増加に伴い、CloudTrail上でKMSへのリクエストログが膨大になります。
GuardDutyやDetectiveはCloudTrailのログを参照するため、CloudTrail上のログの増加に伴い爆発的にコストが増加する可能性があります。
対策としては、Detectiveの無効化するなどが必要になってくると思います。

Amazon GuardDuty の料金

https://aws.amazon.com/jp/guardduty/pricing/

Amazon Detective の料金

https://aws.amazon.com/jp/detective/pricing/

Kinesis Data Streamだけを見るとCloudWatchLogsを使うより安く済みそうに見えますが、KMSなどの周辺のサービスを含めると高額になる可能性があるため注意が必要です。

まとめ


今回はDASの概要について、ざっくりと説明してみました。
AWS上で簡単に設定でき、復号化処理も提供されているサンプルコードを利用することで、比較的に簡単に検証が出来ると思います。

次回は実際にログをS3へ保存するまでの手順について説明したいと思います。

参考にした記事


By okubo

主にAWS上でのインフラ構築を担当してます。 ・AWS Certified Solutions Architect - Professional ・AWS Certified DevOps Engineer - Professional ・AWS Certified Database - Specialty ・AWS Certified Security - Specialty