はじめに
クラウドインテグレーション事業部の三木です。
既知の原因により継続的にALBで5xxエラーが発生している状況がありました。
5xxエラー発生時に、CloudWatch Logs ログストリーム上に特定の文字列が出力されていないか、随時確認を行っていました。
毎回ログストリームを確認するのが手間に感じたため、特定の文字列を検知した際に特定の文字列を含むログをSlack通知できる様にしました。
構成
以下の様に、EC2へインストールしているApacheのアクセスログをCloudWatch Logs へ出力していました。
今回Terraformで以下のAWSリソースを作成します。
- CloudWatch Logs ロググループ
- CloudWatch Logs ログストリーム
- CloudWatch Logs サブスクリプションフィルター
- Lambda
- IAM Role
- IAM Policy
TerraformでAWSリソースを作成した後の構成は以下の様になります。
赤で囲っている部分がTerraformで作成するAWSリソースとなります。
準備
Terraformに必要なファイルの作成
ディレクトリ構造
miki1:Downloads miki$ tree . ├── backend.tf ├── functions │ └── lambda_function.py ├── main.tf └── variable.tf 1 directory, 4 files miki1:Downloads miki$
backend.tfの内容
backend.tfでは「プロバイダー情報」を記載しています。
miki1:Downloads miki$ cat backend.tf terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.44.0" } } required_version = "~> 1.3.1" } provider "aws" { region = "ap-northeast-1" access_key = var.aws_access_key secret_key = var.aws_secret_key } miki1:Downloads miki$
variable.tfの内容
variable.tfでは他のファイルで利用している変数定義を記載しています。
miki1:Downloads miki$ cat variable.tf variable "aws_access_key" {} variable "aws_secret_key" {} miki1:Downloads miki$
lambda_function.pyの内容
Webhook URLは以下リンク先より作成できます。[1]
https://slack.com/signin?redir=%2Fservices%2Fnew%2Fincoming-webhook
miki1:Downloads miki$ cat functions/lambda_function.py import logging import json import urllib.request import base64 import gzip logger = logging.getLogger() logger.setLevel(logging.INFO) def post_slack(argStr): message = argStr send_data = { <span class="loom_code_cs_string">"username"</span>: <span class="loom_code_cs_string">"alert_not<span class="loom_code_cs_reserved">if</span>ication"</span>, <span class="loom_code_cs_string">"icon_emoji"</span>: <span class="loom_code_cs_string">":bell:"</span>, <span class="loom_code_cs_string">"text"</span>: message, } send_text = <span class="loom_code_cs_string">"payload="</span> + json.dumps(send_data) # URLにはご自分のWebhook URLを入力してください request = urllib.request.Request( <span class="loom_code_cs_string">"https:<span class="loom_code_cs_linecomment">//hooks.slack.com/services<span class="loom_code_cs_blockcomment">/*********/</span>***********/************************"</span>,</span> data=send_text.encode(<span class="loom_code_cs_string">"utf-8"</span>), method=<span class="loom_code_cs_string">"POST"</span> ) with urllib.request.urlopen(request) <span class="loom_code_cs_reserved">as </span>response: response_body = response.read().decode(<span class="loom_code_cs_string">"utf-8"</span>) def lambda_handler(event, context): # CloudWatch Logsからのデータはbase64エンコードされているのでデコード decoded_data = base64.b64decode(event['awslogs']['data']) # バイナリに圧縮されているため展開 json_data = json.loads(gzip.decompress(decoded_data)) # ログデータ取得 error_log = json_data['logEvents'][0]['message'] post_slack(error_log) miki1:Downloads miki$
main.tfの内容
「************」には自身のAWS アカウントIDを記載します。
miki1:Downloads miki$ cat main.tf resource "aws_cloudwatch_log_group" "log_group" { name = "iret-media" } resource "aws_cloudwatch_log_stream" "log_stream" { name = "iret-media" log_group_name = aws_cloudwatch_log_group.log_group.name } resource "aws_cloudwatch_log_subscription_filter" "logfilter" { distribution = "ByLogStream" filter_pattern = "ERROR" log_group_name = aws_cloudwatch_log_group.log_group.name name = "iret-media" destination_arn = aws_lambda_function.lambda.arn } resource "aws_lambda_function" "lambda" { function_name = "iret-media" handler = "lambda_function.lambda_handler" memory_size = "128" role = aws_iam_role.role.arn runtime = "python3.8" filename = data.archive_file.function_source.output_path source_code_hash = data.archive_file.function_source.output_base64sha256 timeout = "60" } data "archive_file" "function_source" { type = "zip" source_dir = "${path.module}/functions" output_path = "${path.module}/archive/function.zip" } resource "aws_lambda_permission" "lambda_permission" { statement_id = "AllowExecutionFromCloudWatch" action = "lambda:InvokeFunction" function_name = aws_lambda_function.lambda.function_name principal = "logs.amazonaws.com" source_account = "************" source_arn = "arn:aws:logs:ap-northeast-1:************:log-group:iret-media:*" } resource "aws_iam_role" "role" { name = "iret-media" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } }, ] }) } resource "aws_iam_policy" "policy" { name = "iret-media" policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "logs:CreateLogGroup", "Resource": "arn:aws:logs:ap-northeast-1:************:*" }, { "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": [ "arn:aws:logs:ap-northeast-1:************:log-group:/aws/lambda/iret-media:*", "arn:aws:logs:ap-northeast-1:************:log-group:/aws/lambda/iret-media:*" ] } ] } EOF } resource "aws_iam_role_policy_attachment" "CloudWatchLogsFullAccess" { role = aws_iam_role.role.name policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" } resource "aws_iam_role_policy_attachment" "LambdaAccess" { role = aws_iam_role.role.name policy_arn = aws_iam_policy.policy.arn } miki1:Downloads miki$
TerraformでAWSリソースの作成
main.tfがあるディレクトリ上で、terraform initを実行します。
miki1:Downloads miki$ ls backend.tf functions main.tf variable.tf miki1:Downloads miki$
miki1:Downloads miki$ terraform init Initializing the backend... Initializing provider plugins... - Finding hashicorp/aws versions matching "~> 3.44.0"... - Finding latest version of hashicorp/archive... .....省略 miki1:Downloads miki$
terraform applyを実行するとアクセスキー/シークレットアクセスキーが求められるので入力します。
miki1:Downloads miki$ terraform apply var.aws_access_key Enter a value: var.aws_secret_key Enter a value: .....省略 Plan: 8 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes .....省略 aws_lambda_permission.lambda_permission: Creation complete after 0s [id=****************************] Apply complete! Resources: 8 added, 0 changed, 0 destroyed. miki1:Downloads miki$
通知確認
CloudWatchのコンソールでキャプチャに記載している数字の順番で画面遷移を行います。
「ログイベントの作成」をクリックします。
「ERR0R」と入力します。
Webhook URLを作成する時に指定したSlack チャンネルに通知が飛ぶことを確認できます。
参考記事
[1]:https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008
まとめ
CloudWatch Logs ログストリーム上に特定文字列を検知した際に、Slackに通知する方法について解説しました。
これで毎回AWSコンソールを開かないで済むので手間が省けました。