はじめに
クラウドインテグレーション事業部の三木です。
既知の原因により継続的に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コンソールを開かないで済むので手間が省けました。