CloudWatch メトリクスで指定期間の最初と最後の値を取り、差分を取る方法

やりたいこと

CloudWatch メトリクスで表示する期間を1時間とか1日とか選択するところがあるじゃん。

期間を選択するところ

で、これで表示されたグラフの最初と最後の値とその差を知りたいことあるじゃん、あるよね?
例えば、ALB の RequestCount っていうメトリクスで、先週と今日のピークの同時間(時分まで同一)の1分あたりの合計リクエスト数の増減を知りたいとかさ。
あとは溜まりまくった SQS の Queue があるとして、処理させて1日でどれくらい減ったとかさ。

どうやるか

そういう時はこうすれば良い。

  • 対象のメトリクスを選択 (m1)
    • スクショでは ALB の RequestCount を選択
  • 対象のメトリクス m1 を使って数式を2つ追加
    • 期間の最初の値: IF(m1==0, FIRST(m1), FIRST(m1)) (e1)
    • 期間の最後の値: IF(m1==0, LAST(m1), LAST(m1)) (e2)
  • e1 と e2 を使って差分を計算する数式を追加
    • e2-e1

正直なところ

この式でどうして最初と最後の値を取ってこれるのかはよくわかっていない。

docs.aws.amazon.com

ドキュメントを見ると、まぁなんとなく FIRST と LAST を使えば取れるっぽいな、でもこのまま FIRST(m1) とかって入れると The return value of the expression must be TimeSeries or Array[TimeSeries] って言って怒られるんだよな、戻り値が悪いのか?んー、でも FIRST の戻り値は TS って書いてあるから大丈夫に見えるんだけどなぁ…などと試行錯誤を繰り返しているうちに、この IF 文を使う式にたどり着いた。

多分、IF 文の条件はエラーにならなければ何でも良い。
(true でも false でも FIRST(m1) を返しているのだから)

とまぁ、よくわかっていないがとりあえず取れたので、これを使って差を見るグラフをダッシュボードに追加したりしている。

Elastic Beanstalk のサーバーに NewRelic をインストールする

Elastic Beanstalk 管理の EC2 に NewRelic をインストールしたい。

公式インストール方法

これらのファイルを .ebextensions 配下に配置してデプロイしろとのこと。

Infrastructure Agent
Configure the infrastructure agent on AWS Elastic Beanstalk | New Relic Documentation

files:
  "/etc/newrelic-infra.yml":
    mode: "000644"
    owner: root
    group: root
    content: |
      license_key: YOUR_LICENSE_KEY

commands:
  # Create the agent’s yum repository
  "01-agent-repository":
    command: sudo curl -o /etc/yum.repos.d/newrelic-infra.repo https://download.newrelic.com/infrastructure_agent/linux/yum/amazonlinux/2023/x86_64/newrelic-infra.repo
  #
  # Update your yum cache
  "02-update-yum-cache":
    command: yum -q makecache -y --disablerepo='*' --enablerepo='newrelic-infra'
  #
  # Run the installation script
  "03-run-installation-script":
    command: sudo yum install newrelic-infra -y

PHP APM Agent
AWS Elastic Beanstalk installation for PHP | New Relic Documentation

packages:
  yum:
    newrelic-php5: []
  rpm:
    newrelic: INSERT_LINK_TO_REPO
commands:
  configure_new_relic:
    command: newrelic-install install
    env:
      NR_INSTALL_SILENT: true
      NR_INSTALL_KEY: INSERT_LICENSE_KEY

  

この方法をそのまま実装すればいいk…
いやいやいやいや、YOUR_LICENSE_KEY , INSERT_LICENSE_KEY って、ライセンスキー ベタ書きやん。

.ebextensions に置けって書いてあるけど、git とかのバージョン管理にプッシュしたらライセンスキーそのまま載っちゃうやん。
これはアカンと思ったので、ライセンスキーをベタ書きせずに済むようにした。

非公式インストール方法

基本的には公式インストール方法をベースにカスタマイズ。

Infrastructure Agent

newrelic_infra.config と言うファイル名で .ebextensions 配下に配置。

commands:
  "00-set-newrelic-infra-yml":
    command: |
      APP_ENV=$(/opt/elasticbeanstalk/bin/get-config environment -k ENV_NAME)
      NR_INFRA_KEY=$(aws ssm get-parameter --region ap-northeast-1 --name /${APP_ENV}/newrelic/infrastructure/license_key --query 'Parameter.Value' --output text --with-decryption)
      echo -e "license_key: $NR_INFRA_KEY" | sudo tee /etc/newrelic-infra.yml

  # Create the agent’s yum repository
  "01-agent-repository":
    command: sudo curl -o /etc/yum.repos.d/newrelic-infra.repo https://download.newrelic.com/infrastructure_agent/linux/yum/amazonlinux/2/x86_64/newrelic-infra.repo

  # Update your yum cache
  "02-update-yum-cache":
    command: yum -q makecache -y --disablerepo='*' --enablerepo='newrelic-infra'

  # Run the installation script
  "03-run-installation-script":
    command: sudo yum install newrelic-infra -y

変更点は以下の通り。

  • files セクションを廃止
  • ライセンスキーは SSM Parameter Store に Secure String で格納
  • Elastic Beanstalk の環境プロパティに ENV_NAME を追加
    • ここには staging や production などを入れ、環境ごとにライセンスキーを分けることに対応
    • 環境ごとにキーを分けない場合は無くて良い
  • commands セクションで
    • 環境プロパティ ENV_NAME の値を取得
    • Parameter Store からライセンスキーを取得
    • 取得した値を tee コマンドで /etc/newrelic-infra.yml に出力

PHP APM Agent

newrelic_apm.config と言うファイル名で .ebextensions 配下に配置。

packages:
  yum:
    newrelic-php5: []
  rpm:
    newrelic: http://yum.newrelic.com/pub/newrelic/el5/x86_64/newrelic-repo-5-3.noarch.rpm
commands:
  "00-install-newrelic":
    command: |
      APP_ENV=$(/opt/elasticbeanstalk/bin/get-config environment -k ENV_NAME)
      export NR_INSTALL_KEY=$(aws ssm get-parameter --region ap-northeast-1 --name /${APP_ENV}/newrelic/apm/license_key --query 'Parameter.Value' --output text --with-decryption)
      export NR_INSTALL_SILENT=true
      newrelic-install install
  
  "01-setting-apm":
    command: |
      APP_ENV=$(/opt/elasticbeanstalk/bin/get-config environment -k ENV_NAME)
      sed -i "s/^newrelic.appname.*/newrelic.appname = \"YOUR APP NAME (${APP_ENV})\"/g" /etc/php.d/newrelic.ini
      sed -i "s/^;newrelic.error_collector.record_database_errors.*$/newrelic.error_collector.record_database_errors = true/g" /etc/php.d/newrelic.ini

変更点は以下の通り。

  • ライセンスキーは SSM Parameter Store に Secure String で格納
  • Elastic Beanstalk の環境プロパティに ENV_NAME を追加
    • ここには staging や production などを入れ、環境ごとにライセンスキーを分けることに対応
    • 環境ごとにキーを分けない場合は無くて良い
  • commands セクションで
    • 環境プロパティ ENV_NAME の値を取得
    • Parameter Store からライセンスキーを取得し、環境変数 NR_INSTALL_KEY に設定
    • 環境変数 NR_INSTALL_SILENTtrue を格納
      • サイレントモードでのインストールに必要
      • 通常は対話形式でのインストールとなる
    • その上で newrelic-install install を実行
  • "01-setting-apm"APM 設定周りをカスタマイズするコマンドを実行

NR_INSTALL_KEY , NR_INSTALL_SILENT については以下参照。
Silent mode for the install script (advanced) | New Relic Documentation

APM 設定についてはここを参照。
PHPエージェントの設定 | New Relic Documentation

注意事項

これを実行するためには以下の許可設定が必要。

  • 対象の EC2 の IAM Role に、SSM Parameter Store の値を取得するアクション( ssm:GetParameter )の許可が必要
  • Secure String の Decrypt のため、対象の EC2 から Secure String の暗号化キーに指定している KMS へのアクセス許可が必要
    • 大抵の場合は KMS 側のキーポリシーで許可してあるはず

その他言い訳など

  • 今回は SSM Parameter Store の Secure String にライセンスキーを格納して利用したけど、Secrets Manager でも良い
  • Elastic Beanstalk が Parameter Store や Secrets Manager をサポートしていれば良かったのだけど、無かったので仕方なく commands で対応
  • 環境プロパティを使わずに、Elastic Beanstalk が起動する EC2 に環境名を持ったタグを付けておいてそれを取っても良かったのだけど、今回対象とした EC2 が「インスタンスメタデータのタグを許可する」が無効だったので今回紹介した方法を採用

RDS Aurora 監査ログの確認

以下の条件の時の監査ログについて確認した時のメモ。

RDS Aurora の状態

  • Cluster の「監査ログ」にチェック済み
  • Cluster Parameter Group は以下の通り設定
    • server_audit_logging : 1
    • server_audit_logs_upload : 0
    • server_audit_events : CONNECT,QUERY_DDL,QUERY_DCL
    • server_audit_excl_users : rdsadmin,mysql.sys

確認のため実施した内容

  • 認証失敗
    • mysql クライアントで接続試行し、わざとパスワードを間違える
  • 認証成功
    • mysql クライアントで接続試行し、正しいパスワードを入れる
  • DDL, DCL の確認用 SQL
CREATE USER 'noo'@'%';
SET PASSWORD FOR 'noo'@'%' = 'hogehogehoge';
GRANT SELECT ON `*`.* TO 'noo'@'%' IDENTIFIED BY 'hogehogehoge';
DROP USER 'noo'@'%';
  • SELECT 等の DML クエリが記録されないことを確認するための SQL
SELECT * FROM mysql.user;

結果

1706705683981863,aurora-instance-01,admin,10.0.0.24,11759925,0,FAILED_CONNECT,,,1045
1706705683981894,aurora-instance-01,admin,10.0.0.24,11759925,0,DISCONNECT,,,0
1706705688849140,aurora-instance-01,admin,10.0.0.24,11759927,0,CONNECT,,,0
1706705716280568,aurora-instance-01,admin,10.0.0.24,11759927,340029750,QUERY,,'CREATE USER \'noo\'@\'%\' IDENTIFIED WITH \'mysql_native_password\'',0
1706705718993333,aurora-instance-01,admin,10.0.0.24,11759927,340029765,QUERY,,'SET PASSWORD FOR `noo`@`%`=<secret>',0
1706705721475686,aurora-instance-01,admin,10.0.0.24,11759927,340029785,QUERY,,'GRANT SELECT ON `*`.* TO \'noo\'@\'%\' IDENTIFIED WITH \'mysql_native_password\' AS \'<secret>\'',0
1706705724527190,aurora-instance-01,admin,10.0.0.24,11759927,340029813,QUERY,,'DROP USER \'noo\'@\'%\'',0
1706705754747281,aurora-instance-01,admin,10.0.0.24,11759927,0,DISCONNECT,,,0
  • 1行目: 接続失敗(FAILED_CONNECT)
  • 2行目: 切断(DISCONNECT)
  • 3行目: 接続成功(CONNECT)
  • 4〜7行目: 発行されたクエリのログ
    • パスワード部分は <secret> となってマスクされている
    • DROP USER の後に流した SELECT 文は記録されていない
  • 8行目: 切断(DISCONNECT)

OpenSearch (Elasticsearch) に CloudFront のアクセスログを連携するための Ingest Pipeline

OpenSearch (Elasticsearch) に CloudFront のアクセスログを連携するために必要な Ingest Pipeline についてのメモ。

Ingest Pipeline とは、Document を POST した時に、その Document を登録する前に加工することができる機能。
渡ってきた CloudFront のアクセスログをちょっと加工して、OpenSearch 上で扱いやすくする。

以下コマンド。

curl -H "Content-Type: application/json" -XPUT 'https://{domain}/_ingest/pipeline/cflog' -d '{
  "processors": [
    {
      "grok": {
        "field": "message",
        "pattern_definitions": {
          "CFDATETIME" : "\\d{4}-\\d{2}-\\d{2}\\t\\d{2}:\\d{2}:\\d{2}"
        },
        "patterns":[ "%{CFDATETIME:timestamp}\t%{NOTSPACE:edge_location}\t%{NOTSPACE:response_bytes}\t%{NOTSPACE:client_ip}\t%{NOTSPACE:http_method}\t%{NOTSPACE:distribution_host}\t%{NOTSPACE:http_path}\t%{NOTSPACE:http_status}\t%{NOTSPACE:http_referer}\t%{NOTSPACE:agent}\t%{NOTSPACE:query_strings}\t%{NOTSPACE:cookies}\t%{NOTSPACE:edge_result_type}\t%{NOTSPACE:edge_request_id}\t%{NOTSPACE:host_header}\t%{NOTSPACE:protocol}\t%{NOTSPACE:request_bytes}\t%{NOTSPACE:response_time:float}\t%{NOTSPACE:x_forwarded_for}\t%{NOTSPACE:tls_protocol}\t%{NOTSPACE:tls_cipher}\t%{NOTSPACE:edge_response_result_type}\t%{NOTSPACE:http_version}\t%{NOTSPACE:fle_status}\t%{NOTSPACE:fle_encrypted_fields}\t%{NOTSPACE:request_port}\t%{NOTSPACE:time_to_first_byte:float}\t%{NOTSPACE:edge_detailed_result_type}\t%{NOTSPACE:content_type}\t%{NOTSPACE:content_length}\t%{NOTSPACE:content_range_start}\t%{NOTSPACE:content_range_end}" ],
        "ignore_missing": true
      }
    },
    {
      "remove": {
        "field": "message"
      }
    },
    {
      "user_agent": {
        "field": "agent",
        "target_field": "user_agent",
        "ignore_failure": true
      }
    },
    {
      "remove": {
        "field": "agent",
        "ignore_failure": true
      }
    },
    {
      "gsub": {
        "field": "timestamp",
        "pattern": "(\\d{4}-\\d{2}-\\d{2})\\t(\\d{2}:\\d{2}:\\d{2})",
        "replacement": "$1T$2Z"
      }
    },
    {
      "date": {
        "field": "timestamp",
        "formats": [
          "date_time_no_millis"
        ]
      }
    }
  ]
}'

Ingest Pipeline は(恐らく)上から評価が行われる。
この設定について上から解説する。

1. grok processor

www.elastic.co

grok とは、定義済みの正規表現を使ってテキスト分割して指定した Field にマッピングするツール。
パターンはこの辺を参考に考えていく。
www.alibabacloud.com

また、CloudFront の標準ログは以下の定義を見ながら、パターンを考える。
docs.aws.amazon.com

grok processor では、最初に渡ってきた message という Field を、 patterns に従って各 Field に分割してマッピングするよう指示。

しかし、定義済みの正規表現の中に該当するものがないこともある。その場合、自分でカスタム正規表現を定義できる。 それが pattern_definitions である。

CloudFront の標準ログでは、日付と時間が別の項目として出力されているので、それを1つの timestamp という Field にマッピングするために CFDATETIME という正規表現を定義している。
それ以外は 面倒くさくて 特にこだわりは無いので、全て NOTSPACE (空白以外の文字列) で解析して各 Field にマッピングしている。

2. remove processor

www.elastic.co

grok processormessage Field を各 Filed に分割した後は不要になるので、 message Field を削除している。

3. user_agent processor

www.elastic.co

user_agent processor は指定した Field が User Agent であると認識させ、その User Agent を解析して更に詳細な Field にマッピングしてくれる。

4. remove processor

user_agent processor 処理後は agent Field そのものは不要になるので削除している。

5. gsub processor

www.elastic.co

grok でマッピングした timestamp に入っている文字列では、日付と時間がタブで区切られていて、そのままだと OpenSearch(Elasticsearch) 側に time field として認識してもらえない。
ISO8601 形式に変換するために、 timestampyyyy-MM-ddTHH:mm:ssZ 形式に変換している。

6. date processor

www.elastic.co

ISO8601 形式に変換した timestamp Field の文字列を date Field として認識させる。

Amazon OpenSearch Service でスナップショットリポジトリを登録する

AWS OpenSearch Service でスナップショットを新規に登録する場合、Kibana の Dev Tools からでは実行できない。
https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/managedomains-snapshots.html#managedomains-snapshot-register

上記ドキュメントに記載の Python サンプルコードを使って登録することで解決する。

import boto3
import requests
from requests_aws4auth import AWS4Auth

host = '' # domain endpoint with trailing /
region = '' # e.g. us-west-1
service = 'es'
credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)

# Register repository

path = '_snapshot/my-snapshot-repo-name' # the OpenSearch API endpoint
url = host + path

payload = {
  "type": "s3",
  "settings": {
    "bucket": "s3-bucket-name",
    "region": "us-west-1",
    "role_arn": "arn:aws:iam::123456789012:role/snapshot-role"
  }
}

headers = {"Content-Type": "application/json"}

r = requests.put(url, auth=awsauth, json=payload, headers=headers)

print(r.status_code)
print(r.text)

サンプルコードを自分の環境用に修正したら、自分のローカルで実施しても良いが、今回は CloudShell でやってみた。
コマンドは以下の通り。

$ vi register-repo.py # サンプルコード配置
$ sudo pip3 install boto3 # boto3 インストール
$ sudo pip3 install requests_aws4auth # requests_aws4auth インストール
$ python3 register-repo.py # 実行
200
{"acknowledged":true}