ã¯ããã« SREã®å¯ºå³¶ã§ãã MNTSQã§ã¯ãæ¬çªç°å¢ã§ã®AWSã®æåæäœã顧客æ
å ±ããŒã¿ãžã®ã¢ã¯ã»ã¹çãCloudTrailãã°ããæ€ç¥ããæäœè
ã«ç®çãçç±ã確èªããã»ãã¥ãªãã£ç£æ»ãéçšããŠããŸãïŒè©³çŽ°ã¯ ãã¡ãã®èšäº ãåç
§ïŒã ãããŸã§ã¯ãã°ããã®ç°åžžæ€ç¥ã¯èªååãããŠãããã®ã®ããã®åŸã®æäœå
容ã®ç¢ºèªãç®çãçç±ã確èªããéçšã人åã§è¡ãããŠãããéçšäžã®Toilãšãªã£ãŠããŸããã ãã®èª²é¡ã解決ãããããä»åãã»ãã¥ãªãã£ç£æ»éçšãèªååããSlack BotãéçºããŸãããæ¬èšäºã§ã¯ããã®ã¢ãŒããã¯ãã£ããå®è£
ã®è©³çްãŸã§ã玹ä»ããŸãã ã¯ããã« èæ¯ ç£æ»ã®ä»çµã¿ èª²é¡ ã©ã®ãããªãã®ãäœã£ãã åäœãã㌠éç¥ åç ãªãã€ã³ã ã¢ãŒããã¯ã㣠ã³ã³ããŒãã³ãäžèЧ Slack Appã®èšå® åæ©èœã®å®è£
éç¥ïŒnotify-audit-reportïŒ åçåãä»ãïŒaudit-response-handlerïŒ ãªãã€ã³ãïŒsend-audit-reminderïŒ å°å
¥å¹æ ã³ã¹ãé¢ æ©èœã»éçšé¢ ã»ãã¥ãªãã£é¢ã»ã¬ããã³ã¹é¢ æåŸã« èæ¯ ç£æ»ã®ä»çµã¿ MNTSQã§ã¯ãæ¬çªç°å¢ã«å¯Ÿãã以äžã®ãããªæäœãã»ãã¥ãªãã£ç£æ»ã®å¯Ÿè±¡ãšããŠããŸãã æ€ç¥å¯Ÿè±¡ ããŒã¿ãœãŒã¹ æ¬çªç³»AWSã¢ã«ãŠã³ãã§ã®æåå€æŽæäœ CloudTrail 顧客æ
å ±ãå容ããS3ãã±ãããžã®ã¢ã¯ã»ã¹ CloudTrail æ¥åæéå€ã®Redashæäœã»ãã°ã€ã³ RedashãµãŒããã° / CloudTrailïŒIAM Identity CenterïŒ ãããã®æ€ç¥ããSREã¡ã³ããŒãžã®éç¥ãŸã§ã¯èªååãããŠããŸãããEventBridgeãããªã¬ãŒã«é±æ¬¡ã§Athenaã®ååä»ãã¯ãšãªãå®è¡ããçµæãSREãã£ã³ãã«ã«éç¥ããä»çµã¿ã§ãã äŸãã°AWSã®æå倿޿äœã®å Žåã以äžã®ãããªAthenaã¯ãšãªãå®è¡ãããŸãã select ${ç°å¢å} as environment, eventtime, split_part(userIdentity.principalId, ' : ' , 2 ) as email, eventName, resources[ 1 ].arn as resource , requestParameters from ${ç°å¢å} where timestamp between date_format( current_date - interval ' 1 ' day, ' %Y/%m/%d ' ) and date_format( current_date , ' %Y/%m/%d ' ) -- åã
人ã®çŽæ¥æäœã察象ãšãã and userIdentity.principalId like ' %@mntsq.com ' -- æ°èš / å€æŽ / åé€ã«è©²åœããæäœã察象ãšãã and ( eventName like ' Update% ' or eventName like ' Put% ' or eventName like ' Create% ' or eventName like ' Delete% ' or eventName like ' Modify% ' ) -- CloudShell ãã®ãã®ã«é¢ããã€ãã³ãã¯å¯Ÿè±¡ããå€ã and eventsource != ' cloudshell.amazonaws.com ' åäººã®æåæäœã®ãã¡ããªãœãŒã¹ã®äœæã»å€æŽã»åé€ã«ãããã€ãã³ããæœåºããŠããŸãã äžå¯©ãªæäœãæ€ç¥ããããšã以äžã®ããã«Slackãžéç¥ãããŸãã èª²é¡ æ€ç¥ãŸã§ã¯èªååãããŠããäžæ¹ã§ããã®åŸã®æäœå
容ã®ç¢ºèªã»æäœè
ãžã®ãã¢ãªã³ã°ã¯æäœæ¥ãšãªã£ãŠããŸããã å
·äœçãªäœæ¥ãã㌠æ
åœè
ãAthenaã®çµæãç¢ºèª ãã°ãèªã¿è§£ããŠæäœå
å®¹ãææ¡ãèæ¯ãç¢ºèª æäœè
ã«åå¥ã«ãã¢ãªã³ã° åçããªããã°ãªãã€ã³ã ãã®äœæ¥ãæ
åœããŠããSREã¡ã³ããŒã¯ãé±ã«çŽ3æéããã®ç¢ºèªãããŒã«è²»ãããŠããŸããã幎éã«æç®ãããšçŽ1人æã®å·¥æ°ã§ããããã¯åŒç€Ÿã®SREããŒã ã®èŠæš¡ãèãããšç¡èŠã§ããªãã³ã¹ãã§ããã ã©ã®ãããªãã®ãäœã£ãã äžè¿°ã®èª²é¡ã解決ãããããã»ãã¥ãªãã£ç£æ»ã®éçšãèªååããSlack BotãéçºããŸããã ãã®Slack Botã«ãA2RM(Automatic Audit Reaction Mechanism)ããšåä»ããŸããã å¥åãç£æ»åç匷å¶ãã³ãã§ãã Slack Botã¯ãäž»ã«ä»¥äžã®4ã€ã®æ©èœãåããŠããŸãã Amazon Bedrockã«ãããã°èŠçŽ : æäœãã°ãèªç¶èšèªã«å€æããŠæäœå¯Ÿè±¡è
ã«éç¥ Slack UIã«ããåçåä» : ãã¿ã³éžæã«ããåçã®ç°¡ç¥å èªåãªãã€ã³ã : æªåçè
ãžã®è¿œè·¡éç¥ èšŒè·¡ç®¡ç : æ€ç¥å
容ãšåççµæã®DynamoDBä¿å åäœãã㌠éç¥ äžå¯©ãªæäœãæ€ç¥ããããšãSlackãã£ã³ãã«ãžæäœè
æ¬äººãã¡ã³ã·ã§ã³ããã¡ãã»ãŒãžãæçš¿ãããŸãã CloudTrailã®çãã°ã¯å¯èªæ§ãäœããå
å®¹ã®ææ¡ã«äžå®ã®ç¥èãèŠããŸãã äŸãã°ãS3ãã±ããããªã·ãŒã®å€æŽæäœãè¡ã£ãå ŽåãAthenaããåºåãããCSVã¯ä»¥äžã®ããã«ãªããŸãã "environment","eventtime","email","eventName","resource","requestParameters" "example-env","2026-03-06T07:57:17Z","user@example.com","PutBucketPolicy","arn:aws:s3:::example-tfstate","{""bucketPolicy"":{""Version"":""2012-10-17"",""Statement"":[{""Sid"":""AllowDatadogReadAccess"",""Effect"":""Allow"",""Principal"":{""AWS"":""arn:aws:iam::123456789012:root""},""Action"":[""s3:GetObject"",""s3:ListBucket""],""Resource"":[""arn:aws:s3:::example-tfstate/*"",""arn:aws:s3:::example-tfstate""]}]},""bucketName"":""example-tfstate"",""Host"":""example-tfstate.s3.ap-northeast-1.amazonaws.com"",""policy"":""""}" ãã®ãã°ã¯Datadogãžã®åç
§æš©éä»äžãšããå
容ã§ããããããšèŠãŠå€æããã®ã¯å°é£ã§ãã ããã§ãAmazon BedrockãçšããŠä»¥äžã®ããã«èªç¶èšèªã§èŠçŽããŠããŸãã ð¡ 倿޿ŠèŠ: S3ãã±ããã«å¯Ÿããããªã·ãŒæŽæ°æäœãæ€ç¥ãããŸããã example-tfstate ãã±ããã®ããªã·ãŒã倿Žãããå€éšAWSã¢ã«ãŠã³ãã«å¯Ÿã㊠s3:GetObject ãš s3:ListBucket ã®æš©éãä»äžãããŸãããããã¯ãDatadogããã®èªã¿åãã¢ã¯ã»ã¹ãèš±å¯ããããã®å€æŽãšæšæž¬ãããŸãã ð æ€ç¥ãããæäœ: ã»S3ãã±ãã example-tfstate ã®ããªã·ãŒæŽæ°ïŒDatadogããã®èªã¿åãã¢ã¯ã»ã¹èš±å¯ïŒ (16:57) ãã®ããã«èŠçŽãããå
容ãéç¥ããããšã§ãæäœè
ããèªåã®ã©ã®äœæ¥ãããç¬æã«å€æã§ããããã«ãªããŸãã åç ãã®ç£æ»ã®ä»çµã¿ã«ãã£ãŠæ€ç¥ãããæäœã®å€ãã¯ããããã€ãããé害察å¿ããªã©ã®å®åäœæ¥ã§ãããã®ããããã¿ã³éžæåŒã§åçã§ããããã«ããæäœè
ã®èšè¿°è² è·ãæå°éã«æããŠããŸãã å®åäœæ¥ã®å Žå : ãã¿ã³ãã¯ãªãã¯ããã ãã§åçãå®äºããŸãã ã€ã¬ã®ã¥ã©ãŒãªæäœã®å Žå : ããã®ä»ããéžæããå Žåã®ã¿ã¢ãŒãã«ãéããèªç±èšè¿°ãšé¢é£URLïŒãã±ããçïŒãå
¥åããŸãã åçãå®äºãããšãå
ã®Slackã¡ãã»ãŒãžã¯ãåçæžã¿ãã®ã¹ããŒã¿ã¹ã«æŽæ°ãããŸãã åçå
容ã¯SREããŒã ã®ãã£ã³ãã«ãžè»¢éã»DBã«èšé²ãããŸãã ãªãã€ã³ã åçããªããŸãŸç¿å¶æ¥æ¥ãè¿ããå Žåã該åœã¡ãã»ãŒãžã®ã¹ã¬ããã«å¯ŸããŠèªåã§ãªãã€ã³ããæçš¿ããŸãã3åãªãã€ã³ããè¡ã£ãŠãåçãåŸãããªãå Žåã¯ãSREããŒã ãžã®ãšã¹ã«ã¬ãŒã·ã§ã³éç¥ã«åãæ¿ãããŸãã ã¢ãŒããã¯ã㣠3ã€ã®Lambda颿°ãããããç¬ç«ãã責åãæã¡ãDynamoDBãä»ããŠç¶æ
ãå
±æããã€ãã³ãé§åã®ã¢ãŒããã¯ãã£ã§ãã ã³ã³ããŒãã³ãäžèЧ ã³ã³ããŒãã³ã çš®å¥ èª¬æ notify-audit-report Lambda AthenaçµæãBedrockã§èŠçŽãSlackéç¥ audit-response-handler Lambda Function URL Slackã€ã³ã¿ã©ã¯ãã£ããã£ã®åŠç send-audit-reminder Lambda æªåçè
ãžã®èªåãªãã€ã³ã audit_attestations DynamoDB åçç¶æ
ã®ç®¡ç,蚌跡ã®èç© Amazon Bedrock (Nova Pro) LLM æäœãã°ã®èªç¶èšèªèŠçŽ åã³ã³ããŒãã³ãã®å®è£
ã®è©³çްã¯åŸè¿°ããŸãã Slack Appã®èšå® æ¬ã·ã¹ãã ã®Slack Appã«å¿
èŠãªOAuth Scopesã¯æå°éã§ãã Scope çšé chat:write ã¡ãã»ãŒãžæçš¿ã»æŽæ° reactions:write ãªã¢ã¯ã·ã§ã³è¿œå users:read.email ã¡ãŒã«ã¢ãã¬ã¹ãããŠãŒã¶ãŒIDæ€çŽ¢ users:read users:read.email ã®åæãšããŠå¿
èŠ èªèšŒæ
å ±ïŒBot Token / Signing SecretïŒã¯SSM Parameter StoreïŒSecureStringïŒã«ä¿ç®¡ããLambdaå®è¡æã«ååŸããŸãã Slack Appã®Interactivityã®Request URLã«ã¯ãLambda Function URLãæå®ããŸãã åæ©èœã®å®è£
éç¥ïŒnotify-audit-reportïŒ æäœãã°ã®èŠçŽçæããSlackéç¥ãŸã§ã®åŠçã«ã€ããŠè§£èª¬ããŸãã åŠçãã㌠EventBridgeã§Athenaã¯ãšãªã®å®äºãæ€ç¥ããLambdaãèµ·å S3ããAthenaã®çµæCSVãååŸ ãŠãŒã¶ãŒïŒã¡ãŒã«ã¢ãã¬ã¹ïŒããšã«ã°ã«ãŒãã³ã° Bedrockã§æäœãã°ãèªç¶èšèªã«èŠçŽ æäœè
ã«ã¡ã³ã·ã§ã³ä»ãã§Slackéç¥ãæçš¿ DynamoDBã«ã¬ã³ãŒããä¿åïŒãªãã€ã³ãã»åç远跡çšïŒ EventBridgeã®ã€ãã³ããã¿ãŒã³ã§Athenaã®ã¯ãŒã¯ã°ã«ãŒãããšã«ã¯ãšãªå®äºãæ€ç¥ãã input_transformer ãçšããŠå¿
èŠãªã¡ã¿ããŒã¿ïŒS3ãã¹ãç£æ»çš®å¥ïŒãLambdaãžæž¡ããŠããŸãã resource "aws_cloudwatch_event_rule" "a2rm_notify_audit_report" { event_pattern = jsonencode ( { "source" : [ "aws.athena" ] , "detail-type" : [ "Athena Query State Change" ] , "detail" : { "workgroupName" : [ "manual-modification-report" ] , "currentState" : [ "SUCCEEDED" ] } } ) } resource "aws_cloudwatch_event_target" "a2rm_notify_audit_report" { input_transformer { input_paths = { queryExecutionId = "$.detail.queryExecutionId" } input_template = <<-EOT { "s3_bucket": "athena-results-bucket", "s3_key": "cloudtrail/manual_modification/<queryExecutionId>.csv", "audit_type": "manual_operation" } EOT } } CloudTrailãã°ããSlackã¡ã³ã·ã§ã³ãžã®å€æ MNTSQã§ã¯AWSãžã®ãã°ã€ã³ã« IAM Identity Center ãå©çšããŠããŸããããã«ãããCloudTrailã® userIdentity å
ã«ãŠãŒã¶ãŒã®ã¡ãŒã«ã¢ãã¬ã¹ãèšé²ãããããã«ãªã£ãŠããŸãã { " type ": " AssumedRole ", " principalid ": " AROA...:user@example.com ", " arn ": " arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_Administrator_.../user@example.com ", " sessioncontext ": { " sessionissuer ": { " type ": " Role ", " arn ": " arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/.../AWSReservedSSO_Administrator_... " } } } principalid ã arn ã«ã¡ãŒã«ã¢ãã¬ã¹ãå«ãŸãããããAthenaã¯ãšãªã§ãããæœåºããŸããSlack APIã® users.lookupByEmail ãçšããŠã¡ãŒã«ã¢ãã¬ã¹ãSlack User IDã«å€æããã¡ã³ã·ã§ã³æååãçæããŠããŸãã def lookup_user_by_email (slack: WebClient, email: str ) -> str | None : try : response = slack.users_lookupByEmail(email=email) return response[ "user" ][ "id" ] except SlackApiError as e: logger.warning( "SlackãŠãŒã¶ãŒã®æ€çŽ¢ã«å€±æããŸãã: %s: %s" , email, e.response[ "error" ]) return None Bedrockåšã æäœãã°ã®èŠçŽã«ã¯Amazon Bedrockã® Amazon Nova Pro ã䜿çšããŠããŸãã ãã®ã¢ãã«ãéžå®ããçç±ã¯ä»¥äžã®ãšããã§ãã Bedrockã§äœ¿ããä»ã®ã¢ãã«ãšæ¯èŒããŠå®äŸ¡ æ€èšŒæ®µéã§åºåå質ã«åé¡ããªãã£ã å®ç§ãªèŠçŽã¯æ±ããŠããããæäœè
ãå
容ãæãåºããçšåºŠã®å
容ã§ååã ã£ã Slack mrkdwnèšæ³ã§ã®æ§é ååºåãå®å®ããŠãã Bedrockã¯ã©ã€ã¢ã³ãã®å®è£
ã¯ã·ã³ãã«ã§ãã bedrock_client = boto3.client( "bedrock-runtime" , region_name= "us-east-1" ) def generate_summary (prompt: str , model_id: str ) -> str : response = bedrock_client.invoke_model( modelId=model_id, body=json.dumps({ "messages" : [{ "role" : "user" , "content" : [{ "text" : prompt}]}], "inferenceConfig" : { "maxTokens" : 2048 , "temperature" : 0.3 , # äžè²«æ§ã®ããåºåã®ããäœãã«èšå® }, }), ) response_body = json.loads(response[ "body" ].read()) return response_body[ "output" ][ "message" ][ "content" ][ 0 ][ "text" ] temperature ã¯LLMã®åºåã®ã©ã³ãã æ§ãå¶åŸ¡ãããã©ã¡ãŒã¿ã§ãå€ãäœãã»ã©æ±ºå®çãªåºåã«ãªããŸããç£æ»ã¬ããŒããšããæ§è³ªäžãåãå
¥åã«å¯ŸããŠã§ããã ãäžè²«ããåºåãåŸããããã 0.3 ãšäœãã«èšå®ããŠããŸãã å®éã«äœ¿çšããŠããããã³ããã®å
šæã以äžã«æ²èŒããŸãã {csv_data} ã«Athenaã®çµæCSVãåã蟌ãŸããŸãã ããªãã¯AWSæäœã®ç£æ»ã¬ããŒããçæããã¢ã·ã¹ã¿ã³ãã§ãã 以äžã®CSVããŒã¿ã¯AWSç°å¢ã§æ€ç¥ãããæäœãã°ã§ãããã®ããŒã¿ãåæããæå®ããããã©ãŒãããã§ã¬ããŒããçæããŠãã ããã ## CSVããŒã¿ {csv_data} ## åºåãã©ãŒããã 以äžã®ãã©ãŒãããã§åºåããŠãã ãããSlackã®mrkdwnèšæ³ã䜿çšããŠãã ããã ð *æçµæ€ç¥æ¥æ:* YYYY-MM-DD HH:MM (JST) ð¡ *倿޿ŠèŠ:* ïŒãªãœãŒã¹ã®ã«ããŽãªããšã«ãäœãè¡ãããã©ã®ãããªç¶æ
ã«ãªã£ãããç®çã®æšæž¬ãå«ããŠ2ã3è¡ãã€ç°¡æœã«èŠçŽããŠãã ãããè€æ°ãæ··åšããå Žåã¯ç®æ¡æžãã䜿çšããŠãã ããïŒ ð *æ€ç¥ãããæäœ:* ã»æäœã®èª¬æ (HH:MM) ã»æäœã®èª¬æ (HH:MM) ... ## ã«ãŒã« - 倿޿ŠèŠã¯ãè€æ°ã®ãªãœãŒã¹ãæŽæ°ãããŸããããšãã£ãæœè±¡çãªè¡šçŸãé¿ãããµãŒãã¹åäœïŒäŸïŒECSé¢é£ãRDSé¢é£ãACMé¢é£ãªã©ïŒã§ããããã®å
·äœçãªå€æŽå
容ãšç®çãèšè¿°ããŠãã ããã - eventtimeã¯UTCãªã®ã§JST(+9æé)ã«å€æããŠãã ãã - æçµæ€ç¥æ¥æã¯æãæ°ããeventtimeãJSTã§è¡šç€ºããŠãã ãã - æ€ç¥ãããæäœã¯åè¡ã®eventNameãšrequestParametersãã人éã«ããããããæ¥æ¬èªã®æäœèª¬æãçæããŠãã ãã - æäœèª¬æã«ã¯ãªãœãŒã¹åããµãŒãã¹åãå«ããŠãã ãã - åäžã®eventNameã倿°ããå Žåã¯ä»£è¡šçãªãã®ããŸãšããŠè¡šç€ºããŠãã ããïŒäŸïŒãECSãµãŒãã¹ã®æŽæ° x5ä»¶ãïŒ - æå€§20ä»¶ãŸã§è¡šç€ºãããã以äžã¯ãâŠä»Nä»¶ããšããŠãã ãã ## åºåäŸïŒå
¥åããŒã¿ãšã¯ç¡é¢ä¿ã®ãµã³ãã«ïŒ å
¥å: "environment","eventtime","email","eventName","resource","requestParameters" "production","2026-01-20T08:18:00Z","user@example.com","UpdateService",,"{{""cluster"":""prod-cluster"",""service"":""web-api"",""desiredCount"":3}}" "production","2026-01-20T08:15:00Z","user@example.com","UpdateService",,"{{""cluster"":""prod-cluster"",""service"":""worker"",""desiredCount"":0}}" åºå: ð *æçµæ€ç¥æ¥æ:* 2026-01-20 17:18 (JST) ð¡ *倿޿ŠèŠ:* productionç°å¢ã®ECSãµãŒãã¹ã«å¯ŸããæŽæ°æäœãæ€ç¥ãããŸãããweb-apiãµãŒãã¹ã®ã€ã³ã¹ã¿ã³ã¹æ°ã3å°ã«å¢å ããworkerãµãŒãã¹ã忢ïŒ0å°ïŒã«ãã倿Žãè¡ãããŠããããããã€ãŸãã¯ã¡ã³ããã³ã¹äœæ¥ã®äžç°ãšæšæž¬ãããŸãã ð *æ€ç¥ãããæäœ:* ã»ECSãµãŒãã¹ web-api ã®æŽæ°ïŒå°æ°: 3ïŒ (17:18) ã»ECSãµãŒãã¹ worker ã®æŽæ°ïŒå°æ°: 0ïŒ (17:15) --- äžèšã®ãã©ãŒãããã«åŸããã¬ããŒããåºåããŠãã ãããåºåã®ã¿ãè¿ããäœèšãªèª¬æãå眮ãã¯äžèŠã§ãã ããã€ã工倫ãããã€ã³ããèšèŒããŸãã åºåãã©ãŒããããSlack mrkdwnèšæ³ã§æå® : LLMã®åºåããã®ãŸãŸSlack Block Kitã® mrkdwn ããã¹ããšããŠäœ¿ããããã«ããŠããŸãã æœè±¡çãªè¡šçŸãçŠæ¢ããã«ãŒã« : æç€ºããªããšãè€æ°ã®ãªãœãŒã¹ãæŽæ°ãããŸãããã®ãããªç¡æå³ãªèŠçŽãè¿ãããšããã£ããããæç€ºçã«çŠæ¢ããŠããŸã å
¥åºåäŸã®æç€º : Few-shotçã«å
·äœäŸãäžããããšã§åºåãã©ãŒãããã®å®å®æ§ãåäžããŸãã ã¬ã³ãŒãæ°ã®äžé : æå€§50ä»¶ã®CSVè¡ãããã³ããã«å«ãïŒLLMåºåã¯20ä»¶ãŸã§è¡šç€ºïŒãè¶
éåã¯ãã®æšã泚èšããŸããã¬ã³ãŒãæ°ãå€ããããšããŒã¯ã³å¶éã«éããæãããããèŠçŽã®ç²ŸåºŠãäœäžããããã§ã LLMã®åŒã³åºãã«å€±æããå Žåãæ£ããSlack mrkdwnèšæ³ã§åºåãããªãã£ãå Žåã§ããéç¥èªäœã¯æ¢ããã詳现ãã°ã確èªããŠãã ããããšããFallbackããã¹ããéä¿¡ããããã«å®è£
ããŠããŸãããçŸæç¹ã§ã¯çºçããŠããŸããã åçãã¿ã³ã®çæ äžè¿°ããéããå®åçãªçç±ã¯ãã¿ã³1ã¯ãªãã¯ã§åçã§ããããã«ãã説æãå¿
èŠãªäŸå€çãªæäœã ãã¢ãŒãã«ã§èªç±èšè¿°ããŠãããèšèšã«ããŠããŸãã 以äžã®ããã«ç£æ»çš®å¥ããšã«åçãã¿ã³ãåºãåããŠããŸãã AWSæåæäœ : ãããã€ãå®åäœæ¥ãé害察å¿ã S3/Redashæäœ : äžèšã«å ãã顧客åãåãã察å¿ãã顧客ããŒã¿åæããªã©ã远å ã BUTTONS_MANUAL_OPERATION = [ { "action_id" : "deploy" , "label" : "ãããã€ã»ãªãªãŒã¹" , "emoji" : "ð" }, { "action_id" : "routine_work" , "label" : "å®åäœæ¥" , "emoji" : "ð«" }, { "action_id" : "incident_response" , "label" : "é害察å¿" , "emoji" : "ðš" }, { "action_id" : "other" , "label" : "ãã®ä»ïŒèšè¿°ããïŒ" , "emoji" : "ð" }, ] BUTTONS_S3_EVENT = [*_BASE_BUTTONS, _BUTTON_CUSTOMER_INQUIRY, _BUTTON_OTHER] BUTTONS_REDASH = [ _BUTTON_DEPLOY, _BUTTON_CUSTOMER_INQUIRY, _BUTTON_CUSTOMER_ANALYSIS, _BUTTON_INCIDENT_RESPONSE, _BUTTON_OTHER, ] ãã¿ã³ã¯ Slack Block Kit ã® actions ãããã¯ãšããŠæ§ç¯ããŸãã button_elements = [ { "type" : "button" , "text" : { "type" : "plain_text" , "text" : f "{btn['emoji']} {btn['label']}" }, "action_id" : btn[ "action_id" ], "value" : btn[ "action_id" ], } for btn in button_defs ] action_id ãåŸç¶ã®åçåŠçã§ã©ã®ãã¿ã³ãæŒãããããå€å®ããããŒã«ãªããŸãã DynamoDBãžã®ä¿å éç¥æçš¿åŸããªãã€ã³ããšåç远跡ã®ããã«DynamoDBã«ã¬ã³ãŒããä¿åããŸããå°æ¥çãªç£æ»èšŒè·¡ãšããŠã®æŽ»çšãèŠæ®ããæ€ç¥å
容ã®èŠçŽãå«ããŠä¿åããŠããŸãã 屿§ æžã蟌ã¿ã¿ã€ãã³ã° 説æ message_ts (PK) éç¥æ Slackã¡ãã»ãŒãžã®ã¿ã€ã ã¹ã¿ã³ã channel_id éç¥æ æçš¿å
ãã£ã³ãã« slack_user_id éç¥æ æäœè
ã®Slack User ID email éç¥æ æäœè
ã®ã¡ãŒã«ã¢ãã¬ã¹ status éç¥æ â åçæã«æŽæ° pending â completed created_at éç¥æ ã¬ã³ãŒãäœææ¥æ audit_type éç¥æ ç£æ»çš®å¥ïŒ manual_operation çïŒ detection_summary éç¥æ LLMã®èŠçŽ or ãã³ãã¬ãŒãããã¹ãïŒJSONïŒ s3_result_url éç¥æ AthenaçµæCSVã®S3ãã¹ responded_at åçæ åçæ¥æ response_reason åçæ éžæãããçç±ïŒ deploy çïŒ response_content åçæ åçå
容ã®è©³çްïŒJSONïŒ remind_count ãªãã€ã³ãæã«æŽæ° ãªãã€ã³ãåæ° table.put_item( Item={ "message_ts" : message_ts, # PK: Slackã¡ãã»ãŒãžã®ã¿ã€ã ã¹ã¿ã³ã "channel_id" : channel_id, "slack_user_id" : slack_user_id, "email" : email, "status" : "pending" , "created_at" : now.isoformat(), "remind_count" : 0 , "s3_result_url" : s3_log_url, "audit_type" : audit_type, "detection_summary" : detection_summary, # LLMã®èŠçŽ or ãã³ãã¬ãŒãããã¹ã } ) ããŒãã£ã·ã§ã³ããŒã«Slackã® message_ts ã䜿ã£ãŠããŸããSlackã®ã¡ãã»ãŒãžã¿ã€ã ã¹ã¿ã³ãã¯ãã£ã³ãã«å
ã§äžæã§ãããã¹ã¬ããè¿ä¿¡ãã¡ãã»ãŒãžæŽæ°ã®éã«ããã®å€ã§å
ã¡ãã»ãŒãžãç¹å®ã§ããããã§ãã â» message_tsåäœã®PKãªã®ã¯ãæ¬Botã¯åäžã®ç£æ»ãã£ã³ãã«ã«æçš¿ããèšèšã®ããã§ãã resource "aws_dynamodb_table" "a2rm_audit_attestations" { billing_mode = "PAY_PER_REQUEST" hash_key = "message_ts" global_secondary_index { name = "status-created_at-index" hash_key = "status" range_key = "created_at" projection_type = "ALL" } global_secondary_index { name = "email-created_at-index" hash_key = "email" range_key = "created_at" projection_type = "ALL" } } status ãš created_at ã«GSIïŒã°ããŒãã«ã»ã«ã³ããªã€ã³ããã¯ã¹ïŒã匵ãããšã§ããªãã€ã³ã察象ãšãªããæªåçïŒpendingïŒããã€ãäžå®æéçµéãããã¬ã³ãŒããå¹ççã«æœåºã§ããããã«ããŠããŸãã ãŸããå°æ¥çã«ãŠãŒã¶ãŒããšã®ç£æ»å±¥æŽãåç
§ã§ããããã« email ãš created_at ã«ãGSIã匵ã£ãŠããŸãã åçåãä»ãïŒaudit-response-handlerïŒ æäœè
ãSlackäžã§ãã¿ã³ãæŒäžããéã®ã€ã³ã¿ã©ã¯ã·ã§ã³åŠçã«ã€ããŠè§£èª¬ããŸãã Slack Boltãšlazy listener Slackã®ã€ã³ã¿ã©ã¯ãã£ãã€ãã³ãïŒãã¿ã³æŒäžãã¢ãŒãã«éä¿¡ïŒãåŠçããã«ã¯ãSlackããã®HTTPãªã¯ãšã¹ãã 3ç§ä»¥å
ã«ACK ããå¿
èŠããããŸããããããDynamoDBæŽæ°ãSlack APIã®è€æ°åŒã³åºããå«ãäžé£ã®åŠçã¯ã3ç§ãè¶
éããå¯èœæ§ããããŸãã ãã®åé¡ã Slack Bolt for Python ã® lazy listener ãã¿ãŒã³ã§è§£æ±ºããŠããŸãã app.action( "deploy" )(ack=ack_action, lazy=[process_standard_button]) app.action( "other" )(ack=ack_action, lazy=[process_other_button]) app.view( "audit_reason_other" )(ack=ack_view, lazy=[process_modal_submission]) ack 颿°ãå³åº§ã«200ã¬ã¹ãã³ã¹ãè¿ãã lazy ã«æå®ãã颿°ã¯ èªèº«ã®Lambdaãéåæã§åinvoke ããŠå¥ã®Lambdaå®è¡ã³ã³ããã¹ãã§åŠçãããŸãããã®ä»çµã¿ãå®çŸãããããLambdaã®IAMããªã·ãŒã«ã¯èªèº«ãžã®Invokeæš©éãä»äžããŠããŸãã # lazy listener ãèªèº«ãéåæåŒã³åºãããããã«å¿
èŠ statement { effect = "Allow" actions = [ "lambda:InvokeFunction" , "lambda:GetFunction" ] resources = [ "arn:aws:lambda:<ç¥>:function:audit-response-handler" ] } Lambda Function URL ã®æ¡çš åçåŠçã®ãšã³ããã€ã³ãã«ã¯ Lambda Function URL ãæ¡çšããŸããã Interactivity Webhookã®èªèšŒã¯ãSlackããªã¯ãšã¹ãããããŒã«ä»äžãã眲åã®æ€èšŒïŒSigning Secretã«ããæ€èšŒïŒã«ãã£ãŠè¡ããããããAPI Gatewayçã®å段ã眮ããã«Function URLã®ã¿ã§ååã§ãããšå€æããŸããã åçåŠçã®æµã ãã¿ã³ãæŒãããåŸã®åŠçã¯ä»¥äžã®é ã§è¡ããŸãã äºéåŠç鲿¢ : Slackã®ãªãã©ã€ä»æ§ãèæ
®ããåŠçã®åé ã§ãDynamoDBã®statusã確èªãããã§ã« completed ãªãåŠçãã¹ãããããŸãã å
ã¡ãã»ãŒãžãæŽæ° : æäœè
ãé£ç¶ããŠãã¿ã³ãæŒããªãããã chat.update ã§å
ã¡ãã»ãŒãžã®ãã¿ã³ããåçæžã¿ãã®ããã¹ã衚瀺ã«å·®ãæ¿ããŸãã SREãã£ã³ãã«ã«åçå
容ã転é : ç£æ»çµæã®éææ§ã確ä¿ãããããåçã¯å
¬éãã£ã³ãã«ã«æµããŸã ã¹ããŒã¿ã¹ã®æŽæ°: ãã¹ãŠã®åŠçãæåããæ®µéã§ãDynamoDBã® status ã completed ã«æŽæ°ããŸãã def complete_attestation (client, message_ts, channel_id, user_id, action_id, label, original_blocks, detail_text= None ): # 1. äºéåŠç鲿¢ item = table.get_item(Key={ "message_ts" : message_ts}).get( "Item" ) if item and item.get( "status" ) == "completed" : return # 2. å
ã¡ãã»ãŒãžãæŽæ°ïŒãã¿ã³ â åçæžã¿è¡šç€ºïŒ completed_blocks = build_completed_blocks(original_blocks, user_id, label, detail_text) client.chat_update(channel=channel_id, ts=message_ts, blocks=completed_blocks) # 3. SREãã£ã³ãã«ã«åçå
容ã転é forward_blocks = build_forward_blocks(user_id, label, original_blocks, channel_id, message_ts, detail_text) client.chat_postMessage(channel=FORWARD_CHANNEL_ID, blocks=forward_blocks) # 4. DynamoDBæŽæ°ïŒåçå
容ã蚌跡ãšããŠä¿åïŒ response_content = json.dumps( { "reason" : action_id, "detail_text" : detail_text}, ensure_ascii= False , ) table.update_item( Key={ "message_ts" : message_ts}, UpdateExpression= "SET #status = :s, responded_at = :r, response_reason = :reason, response_content = :rc" , ExpressionAttributeNames={ "#status" : "status" }, ExpressionAttributeValues={ ":s" : "completed" , ":r" : datetime.now(timezone.utc).isoformat(), ":reason" : action_id, ":rc" : response_content, }, ) ãªãã€ã³ãïŒsend-audit-reminderïŒ æªåçã®æäœè
ã«å¯Ÿããèªåã§åéç¥ããã³ãšã¹ã«ã¬ãŒã·ã§ã³ãè¡ãä»çµã¿ã«ã€ããŠè§£èª¬ããŸãã åŠçãã㌠ããªã¬ãŒ : EventBridgeã«ãããå¹³æ¥ã®åå10:00ïŒJSTïŒã«Lambdaãèµ·åã å¯Ÿè±¡ã®æœåº : DynamoDBã®GSIãçšããŠã忥以åã«äœæãããæªåçã¬ã³ãŒãïŒ status=pending ïŒãã¯ãšãªã ãªãã€ã³ãæçš¿ : 該åœããSlackã¡ãã»ãŒãžã®ã¹ã¬ããã«å¯ŸããŠãªãã€ã³ããæçš¿ã ã«ãŠã³ã¿æŽæ° : DynamoDBã® remind_count ãã€ã³ã¯ãªã¡ã³ãã ãšã¹ã«ã¬ãŒã·ã§ã³ : ãªãã€ã³ãåæ°ãèŠå®å€ïŒ3åïŒãè¶
ããå ŽåãSREããŒã ãžéç¥ã ãªãã€ã³ã察象ã®ç¹å® ãªãã€ã³ã察象ã®ã¬ã³ãŒã㯠status-created_at-index GSIã§ååŸããŸãã paginator = dynamodb_client.get_paginator( "query" ) pages = paginator.paginate( TableName=dynamodb_table_name, IndexName= "status-created_at-index" , KeyConditionExpression= "#s = :status AND created_at < :cutoff" , ExpressionAttributeNames={ "#s" : "status" }, ExpressionAttributeValues={ ":status" : { "S" : "pending" }, ":cutoff" : { "S" : cutoff}, }, ) ããã§ cutoff ã¯ãåœæ¥ã®0:00ïŒJSTïŒããåºæºãšããŠããŸããããã«ãããåœæ¥æ€ç¥ãããã°ããã®ã¬ã³ãŒãïŒãŸã åçã®ç¶äºããããã®ïŒããªãã€ã³ã察象ããé€å€ããŠããŸãã ãšã¹ã«ã¬ãŒã·ã§ã³ 3åãªãã€ã³ãããŠãåçããªãå Žåã¯ãSREããŒã ãžã®ãšã¹ã«ã¬ãŒã·ã§ã³ã«åãæ¿ããŸãã def _process_item (item, slack, channel_id, sre_group_id, table): remind_count = int (item.get( "remind_count" , 0 )) if remind_count >= 3 : text = _build_escalation_text(slack_user_id, sre_group_id) else : text = _build_reminder_text(slack_user_id) slack.chat_postMessage(channel=channel_id, thread_ts=message_ts, text=text) å°å
¥å¹æ ã³ã¹ãé¢ å
šäœããµãŒããŒã¬ã¹ã¢ãŒããã¯ãã£ã§æ§æããŠãããããæé¡ã®éçšã³ã¹ãã¯1,000å以äžã«æããããŠããŸãã LambdaãDynamoDBãAmazon Bedrockã¯ããããåŸé課éã§ããã鱿°åãšããå®è¡é »åºŠã§ã¯ã»ãšãã©ã³ã¹ããçºçããŸãããLLMã«å®äŸ¡ãª Amazon Nova Pro ãéžå®ããããšãäœã³ã¹ãåã«å¯äžããŠããŸãã æé¡1,000å以äžã®éçšã³ã¹ãã§å¹ŽéçŽ1人æåã«çžåœããå·¥æ°ãåæžããããšãã§ããŸããã æ©èœã»éçšé¢ å°å
¥ååŸã§ãç£æ»ç¢ºèªãããŒã¯ä»¥äžã®ããã«æ¹åãããŸããã é
ç® å°å
¥åïŒBeforeïŒ å°å
¥åŸïŒAfterïŒ ç¢ºèªé »åºŠ 鱿¬¡ïŒæåïŒ æ¥æ¬¡ïŒèªåïŒ åçæ¹æ³ Slackã§ã®èªç±èšè¿° ãã¿ã³1ã¯ãªã㯠/ ã¢ãŒãã«å
¥å ãªãã€ã³ã æåã§ãã©ããŒã¢ãã èªåãªãã€ã³ãïŒãšã¹ã«ã¬ãŒã·ã§ã³ 蚌跡管ç ãªã DynamoDBã«ä¿å å±äººå æ
åœè
ã®æäœæ¥ã«äŸå Botã«ããæšæºå ã»ãã¥ãªãã£é¢ã»ã¬ããã³ã¹é¢ æ€ç¥å
容ãåçå
容ãDynamoDBã«æ§é åãããç¶æ
ã§èç©ãããããããã®ãŸãŸç£æ»èšŒè·¡ãšããŠæŽ»çšå¯èœãªç¶æ
ã«ãªããŸããã ISO27001ïŒISMSïŒãªã©ã®ã»ãã¥ãªãã£èªèšŒã§ã¯ãã€ãã³ããã°ã®å®æçãªã¬ãã¥ãŒããéèŠãªå€æŽã®ç¹å®ã»èšé²ã»éç¥ãšãã£ã管ççãæ±ããããŸãã ä»åã®ã·ã¹ãã åã«ããããæ€ç¥ â ç¢ºèª â èšé²ãã®äžé£ã®ããã»ã¹ãèªååã»æšæºåãããŸãããä»çµã¿ãšããŠæŒãã®ãªãéçšãè¡ãããŠããããšã客芳çã«ç€ºããããã«ãªã£ãããšã¯ãä»åŸã®ã»ãã¥ãªãã£èªèšŒã®ç¶æã»ååŸã«åããŠãè¯ã圱é¿ãããã®ã§ã¯ãªãããšèããŠããŸãã æåŸã« æ¬èšäºã§ã¯ãSlack BotãæŽ»çšããã»ãã¥ãªãã£ç£æ»éçšã®èªååäºäŸã«ã€ããŠç޹ä»ããŸããã ã»ãã¥ãªãã£ç£æ»éçšã¯éåžžã«éèŠã§ãããæ€ç¥åŸã®ç¢ºèªãããŒãããã«å¹çåãã圢骞åãããã«ç¶ç¶ããããšããç¹ã«ã€ããŠã¯ãŸã å€ãã®çµç¹ã§æš¡çŽ¢ãç¶ããŠããéšåã ãšæããŠããŸãã ä»åããµãŒããŒã¬ã¹ã¢ãŒããã¯ãã£ãšLLMãçµã¿åãããããšã§ãäœã³ã¹ããã€éçšè² è·ã®äœã圢ã§ãã®èª²é¡ã解決ããããšãã§ããŸããã æ¬èšäºã®å
容ããåæ§ã®èª²é¡ãæ±ããŠããæ¹ããã»ãã¥ãªãã£éçšã®èªååãæ€èšãããŠããæ¹ã
ã«ãšã£ãŠãåèã«ãªãã°å¹žãã§ãã