【Amazon Athena】パーティションを使ってマルチアカウントのCloudTrailログ検索を高速化する

記事タイトルとURLをコピーする

エンタープライズクラウド部の松田です。こんにちは。

今回はパーティションを活用して、マルチアカウント環境における、AWS CloudTrailログの検索を高速化する方法をご紹介します。

1. はじめに

Amazon Athenaを使用して、AWSマネジメントコンソールへのサインインログを抽出するという機会がありました。クエリ高速化のために、アカウントID・AWSリージョン・日付をキーとしてパーティション分割してみましたので、備忘として残しておきます。

今回はマルチアカウント環境で、ログ集約アカウントに複数AWSアカウントのCloudTrailログを集約していることを前提としております。ログ集約の手順については割愛しますのでご了承ください。構成自体は以下の様にイメージいただければ大丈夫です。

2. 事前準備

クエリ結果の場所を設定する

Athenaでクエリを実行する前に、クエリ実行結果ファイルを出力するS3バケットを準備します。
今回は athena-query-result-[アカウントID] としました。それ以外の設定はデフォルトで問題ないです。

S3バケットを作成したら、Athenaワークグループから出力先として指定します。
クエリエディタを開き「設定」タブから設定してください。

データベースを作成する

Athenaのクエリエディタに以下を入力して実行し、データベースを作成します。

create database sample_cloudtrail;

クエリが成功したら、「データベース」を作成したものに変更しておきます。クエリで明示的に対象のデータベースを指定しない場合、ここで指定したデータベースに対して実行されます。

3. テーブル作成(パーティション分割なし)

続いてテーブルを作成します。まずは、パーティションを使用せずに作ってみます。

クエリエディタに以下を入力し、実行します。
最終行の [CloudTrail用S3バケット名][Organizational ID] は適宜書き換えてください。

CREATE EXTERNAL TABLE IF NOT EXISTS `sample_cloudtrail`.`non_partitioned_table` (
`eventversion` string,
`useridentity` STRUCT <
type: STRING,
principalid: STRING,
arn: STRING,
accountid: STRING,
invokedby: STRING,
accesskeyid: STRING,
userName: STRING,
sessioncontext: STRUCT <
attributes: STRUCT <
mfaauthenticated: STRING,
creationdate: STRING
>,
sessionissuer: STRUCT <
type: STRING,
principalId: STRING,
arn: STRING,
accountId: STRING,
userName: STRING
>,
ec2RoleDelivery: string,
webIdFederationData: map <
string,
string >
>
>,
`eventtime` string,
`eventsource` string,
`eventname` string,
`awsregion` string,
`sourceipaddress` string,
`useragent` string,
`errorcode` string,
`errormessage` string,
`requestparameters` string,
`responseelements` string,
`additionaleventdata` string,
`requestid` string,
`eventid` string,
`resources` array <
STRUCT <
arn: STRING,
accountid: STRING,
type: STRING
>
>,
`eventtype` string,
`apiversion` string,
`readonly` string,
`recipientaccountid` string,
`serviceeventdetails` string,
`sharedeventid` string,
`vpcendpointid` string,
`tlsDetails` struct <
tlsVersion: string,
cipherSuite: string,
clientProvidedHostHeader: string
>
)
ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://[CloudTrail用S3バケット名]/AWSLogs/[Organizational ID]/';

クエリが正常終了すると、テーブルが作成されます。

4. テーブル作成(パーティション分割あり)

今度はAWSアカウントIDとAWSリージョン、日付でパーティション化したテーブルを作成してみます。
組織の証跡を保存するS3バケットは以下の様にパスが切られるため、変数となるこの3つの要素でパーティション化すると汎用的に利用できます。

S3バケット
└ AWSLogs
  └ Organization ID
    └ AWSアカウントID
      └ CloudTrail
        └ AWSリージョン
          └ 日付 (yyyy/mm/dd)

実行するクエリは以下の通りです。
こちらも、66行と77行の [CloudTrail用S3バケット名][Organizational ID] は適宜書き換えてください。

CREATE EXTERNAL TABLE IF NOT EXISTS `sample_cloudtrail`.`partitioned_table` (
`eventversion` string,
`useridentity` STRUCT <
type: STRING,
principalid: STRING,
arn: STRING,
accountid: STRING,
invokedby: STRING,
accesskeyid: STRING,
userName: STRING,
sessioncontext: STRUCT <
attributes: STRUCT <
mfaauthenticated: STRING,
creationdate: STRING
>,
sessionissuer: STRUCT <
type: STRING,
principalId: STRING,
arn: STRING,
accountId: STRING,
userName: STRING
>,
ec2RoleDelivery: string,
webIdFederationData: map <
string,
string >
>
>,
`eventtime` string,
`eventsource` string,
`eventname` string,
`awsregion` string,
`sourceipaddress` string,
`useragent` string,
`errorcode` string,
`errormessage` string,
`requestparameters` string,
`responseelements` string,
`additionaleventdata` string,
`requestid` string,
`eventid` string,
`resources` array <
STRUCT <
arn: STRING,
accountid: STRING,
type: STRING
>
>,
`eventtype` string,
`apiversion` string,
`readonly` string,
`recipientaccountid` string,
`serviceeventdetails` string,
`sharedeventid` string,
`vpcendpointid` string,
`tlsDetails` struct <
tlsVersion: string,
cipherSuite: string,
clientProvidedHostHeader: string
>
)
PARTITIONED BY (region string, date string, accountid string)
ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://[CloudTrail用S3バケット名]/AWSLogs/[Organizational ID]/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.date.type' = 'date',
'projection.date.range' = '2023/01/01,NOW',
'projection.date.format' = 'yyyy/MM/dd',
'projection.date.interval' = '1',
'projection.date.interval.unit' = 'DAYS',
'projection.region.type' = 'enum',
'projection.region.values'='us-east-1,us-east-2,us-west-1,us-west-2,af-south-1,ap-east-1,ap-south-1,ap-northeast-2,ap-southeast-1,ap-southeast-2,ap-northeast-1,ca-central-1,eu-central-1,eu-west-1,eu-west-2,eu-south-1,eu-west-3,eu-north-1,me-south-1,sa-east-1',
'projection.accountid.type' = 'injected',
'storage.location.template' = 's3://[CloudTrail用S3バケット名]/AWSLogs/[Organizational ID]/${accountid}/CloudTrail/${region}/${date}',
'classification'='cloudtrail',
'compressionType'='gzip',
'typeOfData'='file',
'classification'='cloudtrail'
);

クエリが正常終了すると、テーブルが作成されます。

5. クエリ実行

それでは、作成した2つのテーブルにクエリを実行してみます。

パーティションなし

以下のクエリを実行します。

IDが xxxxxxxxxxxx のAWSアカウントの東京リージョンで、2023年9月14日に発生したサインインのログを検索します(ちなみにですが当環境では、IAM Identity Center によるシングルサインオンを使用していますので、そのログを取得しています)。

select
eventtime as EventTime,
useridentity.accountid as AwsAccountId,
regexp_extract(useridentity.arn, '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})', 1) as UserName,
useridentity.sessioncontext.sessionissuer.userName as UserRole,
responseElements as LoginStatus
from non_partitioned_table
where
eventsource = 'signin.amazonaws.com' and
eventname = 'ConsoleLogin' and
eventtime >= '2023-09-14T00:00:00Z' and
eventtime < '2023-09-15T00:00:00Z' and
awsregion = 'ap-northeast-1' and
useridentity.accountid = '[AWSアカウントID]';

以下の結果が返ってきました。

クエリの実行時間は 約4分半、スキャンしたデータ量は 7.20GB となりました。ちなみに実行時のS3バケットのデータ量は 7.4GB でしたので、ほぼ全データがスキャンされたことになります。

Athenaはスキャンしたデータ量に応じた従量課金ですので、S3のデータ量が増えていくにつれ利用料が嵩むことになります(1TBにつき5.00USDの課金)。さらにクエリの実行時間も長くなりストレスフルです。

パーティションあり

以下のクエリを実行します。
WHERE でパーティション化された属性を指定しています。

select
eventtime as EventTime,
useridentity.accountid as AwsAccountId,
regexp_extract(useridentity.arn, '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})', 1) as UserName,
useridentity.sessioncontext.sessionissuer.userName as UserRole,
responseElements as LoginStatus
from partitioned_table
where
eventsource = 'signin.amazonaws.com' and
eventname = 'ConsoleLogin' and
date = '2023/09/14' and
region = 'ap-northeast-1' and
accountid = '[AWSアカウントID]';

返ってきた結果はパーティションなしのパターンと同じです。ここは期待通りですね。

一方、クエリの実行時間は 約0.8秒、スキャンしたデータ量は 741.18KB となりました。スキャン対象がS3バケット全体から一部だけに限定されたため、すさまじく高速になっています。

これでS3のデータが増えていっても、料金や実行時間がネックになることはなさそうです。

ちなみにスキャン対象のパスの合計ファイルサイズと、クエリのスキャンデータ量は一致していました。

6. おまけ

上記のクエリは単一の日付、リージョン、アカウントとしていたので、複数指定の場合の書き方を備忘として残しておきます。

単一の日付でなく、期間で検索したい

以下の様に、date between '[yyyy/mm/dd]' and '[yyyy/mm/dd]' で区間指定が可能です。

select *
from partitioned_table
where
date between '2023/09/14' and '2023/10/27' and
region = 'ap-northeast-1' and
accountid = '[AWSアカウントID]';

特定のリージョンでなく、複数リージョンで検索したい

以下の様に、region in ('[リージョン名]', '[リージョン名]',) で複数指定が可能です。
全リージョンを対象としたい場合は無指定にすればよいので、region in ~ を行ごと削除しましょう。厳密には「4. テーブル作成(パーティション分割あり)」のクエリの通り、region列挙型enum)で定義していますので、テーブル作成クエリで列挙した全てのリージョンが対象です。

select *
from partitioned_table
where
date = '2023/09/14' and
region in ('ap-northeast-1', 'ap-northeast-3', 'us-east-1') and
accountid = '[AWSアカウントID]';

利用可能な型については、以下ドキュメントをご参照ください。

docs.aws.amazon.com

特定のAWSアカウントでなく、複数のAWSアカウントで検索したい

リージョンと同様に accountid in ('AWSアカウントID', 'AWSアカウントID') で複数指定が可能です。
ただし、accountid挿入型injected)で定義していますので、region と異なり、無指定だとエラーとなる点に注意が必要です。全アカウントを検索対象にしたい場合は、全アカウントのIDを in に渡すようにしてください。

select *
from partitioned_table
where
date = '2023/09/14' and
region = 'ap-northeast-1' and
accountid in ('[AWSアカウントID]', '[AWSアカウントID]');

7. さいごに

今回はパーティションを使ったAthenaの高速化についてまとめました。
本記事がどなたかの参考になれば幸いです。

松田 渓(記事一覧)

2021年10月入社。散歩が得意です。