EC2インスタンス内のログをCloudWatch LogsとS3バケットに保存してみた

116件のシェア(ちょっぴり話題の記事)

はじめに

おはようございます、加藤です。EC2の上で動くアプリケーションログを一時的にCloudWatch Logsに保管、長期的にS3バケットに保存というアーキテクチャを試してみました。

こちらが概要図です。

EC2インスタンスでCloudWatchエージェントを動かし、ログをCloudWatch Logsに転送します。CloudWatch LogsのロググループからKinesis Data Firehose→S3と転送します。
Kinesisエージェントをインスタンスにインストールすれば直接Kinesis Data Firehoseにログを転送できますが、CloudWatch Logsに短期間はログを保存して置きたい・CloudWatch Logsで任意の文字列を検出した場合はアラートを上げたいというシチュエーションを想定し、このようなアーキテクチャになりました。
永続的に保存したい要件がない限りCloudWatch Logs・S3どちらもログの保存期限を設定した方が良いです(ブログでは設定していません)。

やってみた

Webサーバー

Apacheのログを取得する事を想定してみました。EC2インスタンスを立てて、Apacheのインストールと自動起動を設定します。

sudo yum -y install httpd
sudo systemctl enable httpd && sudo systemctl start httpd

CloudWatch エージェント

下記ブログを参考にCloudWatch AgentをEC2インスタンスにインストールと設定をします。
新しいCloudWatch Agentでメトリクスとログの収集が行なえます | DevelopersIO

EC2インスタンスに SSH or Systems Manager Session Manager で接続し、設定ウィザードを起動します。

sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard

Linux・EC2を選択します。

=============================================================
= Welcome to the AWS CloudWatch Agent Configuration Manager =
=============================================================
On which OS are you planning to use the agent?
1. linux
2. windows
default choice: [1]:
1
Trying to fetch the default region based on ec2 metadata...
Are you using EC2 or On-Premises hosts?
1. EC2
2. On-Premises
default choice: [1]:
1

今回はメトリクスは関係ないのでnoで進めます。

Do you want to turn on StatsD daemon?
1. yes
2. no
default choice: [1]:
2
Do you want to monitor metrics from CollectD?
1. yes
2. no
default choice: [1]:
2
Do you want to monitor any host metrics? e.g. CPU, memory, etc.
1. yes
2. no
default choice: [1]:
2

CloudWatch エージェントのコンフィグを新規作成したいのでnoを選択します。

Do you have any existing CloudWatch Log Agent (http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html) configuration file to import for migration?
1. yes
2. no
default choice: [2]:
2
Log file path Log group name Log stream name
/var/log/httpd/access_log* webserver/access_log {instance_id}
/var/log/httpd/error_log* webserver/error_log {instance_id}

上記の2種類のログを対象に設定します。ワイルドカードが使用できますが、1つのロググループには1種類のログが格納されるように設計する必要があるので注意してください。追加のWebサーバーが発生した時にaccess_logLog group name: webserver/access_logに格納するのはOKですが、access_log, error_logを1つのロググループにまとめるのはNGです。

file
CloudWatch Logs にプッシュするログファイルを指定します。ファイルは、特定のファイルまたは複数のファイルを指すことができます(/var/log/system.log* のようにワイルドカードを使用)。ファイルの変更時間に基づいて、最新のファイルのみが CloudWatch Logs にプッシュされます。access_log.2014-06-01-01 と access_log.2014-06-01-02 など同じ形式の一連のファイルを指定するにはワイルドカードの使用をお勧めします。ただし、access_log_80 と access_log_443 のように複数の種類のファイルには使用しないでください。複数の種類のファイルを指定するには、設定ファイルに別のストリームログのエントリを追加して、各種類のログファイルが異なるログストリームに行くようにします。圧縮ファイルはサポートされていません。
CloudWatch Logs エージェントのリファレンス - Amazon CloudWatch Logs

Do you want to monitor any log files?
1. yes
2. no
default choice: [1]:
1
Log file path:
/var/log/httpd/access_log*
Log group name:
default choice: [access_log*]
webserver/access_log
Log stream name:
default choice: [{instance_id}]

Do you want to specify any additional log files to monitor?
1. yes
2. no
default choice: [1]:
1
Log file path:
/var/log/httpd/error_log*
Log group name:
default choice: [error_log*]
webserver/error_log
Log stream name:
default choice: [{instance_id}]

Do you want to specify any additional log files to monitor?
1. yes
2. no
default choice: [1]:
2

コンフィグがプレビューされます。今回はSystems Manager Parameter Storeにコンフィグを保存せずに進めますが、特別な利用がなければ保存した方が良いでしょう。変更・他インスタンスでの利用が容易になります。

Saved config file to /opt/aws/amazon-cloudwatch-agent/bin/config.json successfully.
Current config as follows:
{
    "logs": {
        "logs_collected": {
            "files": {
                "collect_list": [
                    {
                        "file_path": "/var/log/httpd/access_log*",
                        "log_group_name": "webserver/access_log",
                        "log_stream_name": "{instance_id}"
                    },
                    {
                        "file_path": "/var/log/httpd/error_log*",
                        "log_group_name": "webserver/error_log",
                        "log_stream_name": "{instance_id}"
                    }
                ]
            }
        }
    }
}
Please check the above content of the config.
The config file is also located at /opt/aws/amazon-cloudwatch-agent/bin/config.json.
Edit it manually if needed.
Do you want to store the config in the SSM parameter store?
1. yes
2. no
default choice: [1]:
2
Program exits now.

作成したコンフィグを掲載します。

{
    "logs": {
        "logs_collected": {
            "files": {
                "collect_list": [
                    {
                        "file_path": "/var/log/httpd/access_log*",
                        "log_group_name": "webserver/access_log",
                        "log_stream_name": "{instance_id}"
                    },
                    {
                        "file_path": "/var/log/httpd/error_log*",
                        "log_group_name": "webserver/error_log",
                        "log_stream_name": "{instance_id}"
                    }
                ]
            }
        }
    }
}

CloudWatch エージェントに作成したコンフィグをセットします。その後、起動しているか・自動起動ONになっているかを確認します。

sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json -s
systemctl status amazon-cloudwatch-agent

CloudWatch Logsにログが格納されるか確認します。インスタンス内からcurlでアクセスしてログを作っておきます。

curl http://localhost

下記の作業はAWS 管理ポリシー: AdministratorAccessの権限を持ったローカル端末から行います。

LOG_GROUP_NAME="webserver/access_log"
LOG_STREAM_NAME=$(aws logs describe-log-streams --log-group-name ${LOG_GROUP_NAME} | jq -r '.logStreams[].logStreamName')
aws logs get-log-events --log-group-name ${LOG_GROUP_NAME} --log-stream-name ${LOG_STREAM_NAME} | jq -r '.events[].message'
127.0.0.1 - - [12/Feb/2019:12:02:55 +0000] "GET / HTTP/1.1" 403 3630 "-" "curl/7.55.1"
127.0.0.1 - - [12/Feb/2019:12:06:02 +0000] "GET / HTTP/1.1" 403 3630 "-" "curl/7.55.1"
127.0.0.1 - - [12/Feb/2019:12:06:09 +0000] "GET /aaa HTTP/1.1" 404 201 "-" "curl/7.55.1"
127.0.0.1 - - [12/Feb/2019:12:12:56 +0000] "GET / HTTP/1.1" 403 3630 "-" "curl/7.55.1"
127.0.0.1 - - [12/Feb/2019:12:20:28 +0000] "GET / HTTP/1.1" 403 3630 "-" "curl/7.55.1"
127.0.0.1 - - [12/Feb/2019:12:20:51 +0000] "GET / HTTP/1.1" 403 3630 "-" "curl/7.55.1"

ログを確認できました!!

ログ転送

下記の作業はAWS 管理ポリシー: AdministratorAccessの権限を持ったローカル端末から行います。

パラメータの設定

パラメータを環境変数に設定します。アカウント番号は使用する環境の値に置換してコマンドを実行してください。

ACCOUNT_ID='<<YOUR_AWS_ACCOUNT_ID>>'
REGION='ap-northeast-1'
KINESIS_TO_S3_ROLE_NAME='FirehosetoS3Role'
DELIVERY_STREAM_NAME='my-delivery-stream'
CWL_TO_KINESIS_ROLE_NAME='CWLtoKinesisFirehoseRole'
LOG_GROUP_NAME="webserver/access_log"
LOG_STREAM_NAME=$(aws logs describe-log-streams --log-group-name ${LOG_GROUP_NAME} | jq -r '.logStreams[].logStreamName')
aws logs get-log-events --log-group-name ${LOG_GROUP_NAME} --log-stream-name ${LOG_STREAM_NAME} | jq -r '.events[].message'

S3バケットの作成

Kinesis Data Firehoseがデータを送るS3バケットを作成します。

BUCKET_NAME=$(aws s3 mb s3://cwl-kinesis-s3-${ACCOUNT_ID} | sed -e "s/make_bucket: //g")

Kinesis Data Firehose

Kinesis Data Firehoseが使用するIAMロールを作成します。
信頼ポリシーを作成し、IAMロールを作成します。

cat << EOF > TrustPolicyForFirehose.json
{
  "Statement": {
    "Effect": "Allow",
    "Principal": { "Service": "firehose.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }
}
EOF
KINESIS_TO_S3_ROLE_ARN=$(aws iam create-role \
      --role-name ${KINESIS_TO_S3_ROLE_NAME} \
      --assume-role-policy-document file://TrustPolicyForFirehose.json \
      | jq -r '.Role.Arn')

権限ポリシーを作成し、IAMロールに関連付けます。

cat << EOF > PermissionsForFirehose.json
{
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [ 
          "s3:AbortMultipartUpload", 
          "s3:GetBucketLocation", 
          "s3:GetObject", 
          "s3:ListBucket", 
          "s3:ListBucketMultipartUploads", 
          "s3:PutObject" ],
      "Resource": [ 
          "arn:aws:s3:::${BUCKET_NAME}", 
          "arn:aws:s3:::${BUCKET_NAME}/*" ]
    }
  ]
}
EOF
aws iam put-role-policy \
  --role-name ${KINESIS_TO_S3_ROLE_NAME} \
  --policy-name Permissions-Policy-For-Firehose \
  --policy-document file://PermissionsForFirehose.json

Kinesis Data Firehoseの転送ストリームを作成します。

aws firehose create-delivery-stream \
   --delivery-stream-name ${DELIVERY_STREAM_NAME} \
   --s3-destination-configuration \
  RoleARN="${KINESIS_TO_S3_ROLE_ARN}",BucketARN="arn:aws:s3:::${BUCKET_NAME}"

作成した転送ストリームのARNを環境変数にセットします。

DELIVERY_STREAM_ARN=$(aws firehose describe-delivery-stream --delivery-stream-name "${DELIVERY_STREAM_NAME}" | jq -r '.DeliveryStreamDescription.DeliveryStreamARN')

CloudWatch Logs

CloudWatch Logsが使用するIAMロールを作成します。
信頼ポリシーを作成し、IAMロールを作成します。

cat << EOF > TrustPolicyForCWL.json
{
  "Statement": {
    "Effect": "Allow",
    "Principal": { "Service": "logs.${REGION}.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }
}
EOF
CWL_TO_KINESIS_ROLE_ARN=$(aws iam create-role \
      --role-name ${CWL_TO_KINESIS_ROLE_NAME} \
      --assume-role-policy-document file://TrustPolicyForCWL.json \
      | jq -r '.Role.Arn')

権限ポリシーを作成し、IAMロールに関連付けます。

cat << EOF > PermissionsForCWL.json
{
    "Statement":[
      {
        "Effect":"Allow",
        "Action":["firehose:*"],
        "Resource":["arn:aws:firehose:${REGION}:${ACCOUNT_ID}:*"]
      },
      {
        "Effect":"Allow",
        "Action":["iam:PassRole"],
        "Resource":["${CWL_TO_KINESIS_ROLE_ARN}"]
      }
    ]
}
EOF
aws iam put-role-policy \
  --role-name ${CWL_TO_KINESIS_ROLE_NAME} \
  --policy-name Permissions-Policy-For-CWL \
  --policy-document file://PermissionsForCWL.json

CloudWatch LogsからKinesis Data Firehoseへ転送を設定します。
今回は全てのログを転送したいので--filter-pattern ""としています。

例 1: すべてに一致するようにする
フィルターパターン "" はすべてのログイベントに一致します。
フィルターとパターンの構文 - Amazon CloudWatch Logs

aws logs put-subscription-filter \
    --log-group-name "${LOG_GROUP_NAME}" \
    --filter-name "Match everything" \
    --filter-pattern "" \
    --destination-arn "${DELIVERY_STREAM_ARN}" \
    --role-arn "${CWL_TO_KINESIS_ROLE_ARN}"

ログの確認

設定が完了したら、しばらくするとS3にログが転送されます。具体的には、データが5MB貯まる or 300秒経過するとで、これはKinesis Data FirehoseのBuffer conditions設定値です。
S3からデータを取得して中身を確認してみます。

OBJECT_PATH=$(aws s3 ls --recursive s3://${BUCKET_NAME} | head -n 1 | awk '{print $4}')
aws s3 cp s3://${BUCKET_NAME}/${OBJECT_PATH} ./test_log_data.gz
gzcat test_log_data.gz | jq -r
{
  "messageType": "DATA_MESSAGE",
  "owner": "<<YOUR_ACCOUNT_ID>>",
  "logGroup": "webserver/access_log",
  "logStream": "i-*****************",
  "subscriptionFilters": [
    "Match everything"
  ],
  "logEvents": [
    {
      "id": "34569591904754957625568969500040152563717672592479158272",
      "timestamp": 1550154113551,
      "message": "127.0.0.1 - - [14/Feb/2019:14:21:47 +0000] \"GET / HTTP/1.1\" 403 3630 \"-\" \"curl/7.55.1\""
    }
  ]
}

無事にログを確認できました!!

あとがき

無事に実装ができました...が、結構な操作量ですね。
複数リソースに設定する事が想定されるので、CloudFormationやTerraformを使用した方がトータルでみると早くなると思います。
CloudFormationはこちらのブログが参考になります。
CloudWatchLogsのログをFirehose経由でS3に出力してみた | DevelopersIO

参考