クマ松です。
皆さん、AWS Systems Managerはお使いでしょうか。
AWS Systems ManagerはAWS環境のEC2やオンプレのサーバを効率よく運用することが出来るサービスです。
サーバ構成のインベントリをコンソールから確認したり、決められた時間にパッチを適用したり等、よくある定型作業の自動化を実現してくれます。
今回はこのSystems Managerの機能の1つである「State Manager」を用いて、EC2にインストールしたSystems Manager AgentとCloudWatch Agentを定期的にアップデートしてくれる構成を作ろうと思います。
前提知識
State Managerについて
Black Beltの説明
AWSのBlack Beltの文言を借りると、State Managerは「サーバの状態を定義された状態に保つ/是正するプロセスを自動化」するサービスです。
AWS Black Belt Online Seminar AWS Systems Manager 資料及び QA 公開
「定義された状態」と「是正」
「定義された状態」と「是正」を説明する為に一つ例を挙げます。
AさんがEC2の初期設定でプロキシのサーバとポートを10.0.0.0:8080等と設定したとします。
そしてEC2をBさんに引き渡したとしましょう。
Bさんはインフラに不慣れで、勉強の為のコマンド実行によりプロキシのサーバとポートをリセット(設定されていない状態)してしまいました。
BさんはEC2からプロキシを通って社内のサービスにアクセスできなくなってしまい、何が何だかわからずAさんに問い合わせをすることになりました。
Aさんは再度プロキシのサーバとポートの設定を行いました。
上記の例とState Managerの紹介文とを照らし合わせると、定義された状態が**「プロキシのサーバとポートの設定」に当たります。
そして是正が「Aさんによる再設定」**です。
State Managerで何が出来るか
プロキシのサーバとポートの設定が変更されたかされていないか、人の目で定期的にチェックするのは現実的ではありません。
Systems ManagerとStateManagerを使うことで、変更されたくない状態を定義し、設定を定期的にチェックし、変更があった場合に元に戻す*ことが出来ます。
State ManagerとSSMドキュメント
SSMドキュメントとは
ここまでStateManagerの話をしていましたが、少しSSMドキュメントについて触れなくてはなりません。
「変更したくない状態を定義」する為のAWSサービスがSSMドキュメントです。
SSMドキュメントとは、定期的に実行したいコマンドを定義・管理することが出来るもので、JSONかYAMLで定義されます。
例えば先ほどのプロキシサーバとポートの例で言うと、プロキシサーバを設定するコマンドを定義したカスタムドキュメントを作成できます。
{
"schemaVersion": "2.2",
"description": "Linux Process Monitoring",
"parameters": {
"Service": {
"type": "String",
"default": "",
"description": "(Required) Input Linux Process Name."
}
},
"mainSteps": [
{
"action": "aws:runShellScript",
"name": "resetProxy",
"inputs": {
"runCommand": [
"$env:http_proxy=\"http://10.0.0.0:8080\"",
"",
"$env:https_proxy=\"http://10.0.0.0:8080\"",
]
}
}
]
}
SSMドキュメントはあくまでコマンドが定義できるだけです。
作成したSSMドキュメントは、Systems Managerの他の機能と組み合わせることで実行できます。
SSMドキュメントと他のSystems Managerのサービスの関連
Systems ManagerにはPatch ManagerやDistributor、SSM Inventory等様々なサービスがあります。
これらの実態は「SSMドキュメントを実行する為のフレームワーク」です。
「ボタン数クリックで出来たけど、裏ではSSMドキュメントが実行されている」というものもあります。
SSMドキュメントだけでは定義しきれない設定や、ドキュメントに渡すパラメータの指定、ドキュメントを実行するタイミングを指定することが出来ます。
今回の構成では
AWSが事前に作ってくれたAWS-ConfigureAWSPackageとAWS-UpdateSSMAgentというSSMドキュメントを使い
State Managerで定義したスケジュールに従い
Systems Managerのコマンドを実行する為の機能であるRunCommandが実行されます。
設計
State Manager
実行頻度
SSM Agentはリリースノートが2週間に1度という頻度で更新されるので、なるべく頻繁にアップデートをした方が良いです。
State Managerのスケジュールでは「2週間に1度」は定義できないので、毎週火曜日の日本時間14時にしましょう。
CloudWatch Agentはそこまでリリース頻度は多くないので、30日に1度(State Managerで指定できる頻度のMAX)とします。
アップデートするEC2の対象
対象をEC2のタグで絞ることにします。
Key:Valueの組み合わせがope-type:autoのものに絞ります。
アップデート失敗時に手オペに切り替えたい
SSM AgentやCloudWatch Agentのアップデートに失敗した場合、迅速に気付く必要があります。
でないと気づかないうちにSession ManagerでSSH出来なくなったり、CloudWatchにメトリクスが送信されなくなったりという事態が発生します。
なので特定のドキュメントを実行し、失敗した際に検知できるようEvent Bridge・Lambda・SNSを使います。
構築
Lambda用IAMロール
CloudFormationテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: 'IAMロール'
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: role-lambda
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: /
Policies:
- PolicyName: policy-lambda
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sns:Publish
- ssm:GetParameters
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: '*'
運用担当者用SNS
CloudFormationテンプレート
AWSTemplateFormatVersion: "2010-09-09"
Description: SNS Sample Stack
Resources:
NotifyMailSNSTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: OPE
Subscription:
- Endpoint: xxx+sample@hoge.jp
Protocol: email
StateManager
CloudFormationテンプレート
複数サーバが対象の場合、MaxConcurrencyを100%にすることで同時にアップデートを実行する事が出来ます。
またMaxErrorsを0%にすることで、1台でも失敗した場合はアラートメールを配信することが出来ます。
AWSTemplateFormatVersion: "2010-09-09"
Description:
"State Manager for MiddleWare Auto Update"
Resources:
# ------------------------------------------------------------#
# State Manager for SSM Agent
# ------------------------------------------------------------#
TaggedInstancesAssociation:
Type: AWS::SSM::Association
Properties:
AssociationName: UpdateSSMAgent
Name: AWS-UpdateSSMAgent
ScheduleExpression: cron(0 5 ? * TUE *)
Targets:
- Key: tag:ope-type
Values:
- auto
WaitForSuccessTimeoutSeconds: 300
MaxConcurrency: 100%
MaxErrors: 0%
CloudWatchAgentAssociation:
Type: AWS::SSM::Association
Properties:
AssociationName: UpdateCloudWatchAgent
Name: AWS-ConfigureAWSPackage
ScheduleExpression: rate(30 days)
Targets:
- Key: tag:ope-type
Values:
- auto
Parameters:
action:
- Install
installationType:
- "Uninstall and reinstall"
name:
- AmazonCloudWatchAgent
WaitForSuccessTimeoutSeconds: 300
MaxConcurrency: 100%
MaxErrors: 0%
SSM Parameter Store
AWSアカウントの和名をパラメータストアで指定します。
これでどこのAWSアカウントからメールが配信されたかを識別します。
EventBridge・Lambda・SNS
CloudFormationテンプレート(SAMバージョン)
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: メール送信用Lambda(RunCommandの失敗)
Resources:
SendSNSRunCommandFailure:
Type: 'AWS::Serverless::Function'
Properties:
FunctionName: SendSNSRunCommandFailure
Handler: lambda_function.lambda_handler
Runtime: python3.8
CodeUri: #適宜修正
Description: メール送信用Lambda(RunCommandの失敗)
MemorySize: 128
Timeout: 300
Role: !Sub 'arn:aws:iam::${AWS::AccountId}:role/role-lambda' #IAM Roleは事前に作る
Events:
CloudWatchEvent1:
Type: CloudWatchEvent
Properties:
Pattern:
detail-type:
- EC2 Command Invocation Status-change Notification
source:
- aws.ssm
detail:
document-name:
- AWS-UpdateSSMAgent
- AWS-ConfigureAWSPackage
status:
- Failed
SNS1:
Type: SNS
Properties:
Topic:
Ref: SNSTopic1
SNSTopic1:
Type: 'AWS::SNS::Topic'
CloudFormationテンプレート(not SAMバージョン)
Lambdaを手で構築した場合は、SAMバージョンではないCloudFormationを使ってEventBridgeだけ構築します。
こちらのテンプレートにLambdaを含めてデプロイすることも可能ですが、コードが長いので今回は省略しています。
AWSTemplateFormatVersion: "2010-09-09"
Description:
"not SAM version"
Resources:
# ------------------------------------------------------------#
# EventBridgeとLambdaを呼ぶためのPermission
# ------------------------------------------------------------#
EventRule:
Type: 'AWS::Events::Rule'
Properties:
Description: EventRule
EventPattern:
detail-type:
- EC2 Command Invocation Status-change Notification
source:
- aws.ssm
detail:
document-name:
- AWS-UpdateSSMAgent
- AWS-ConfigureAWSPackage
status:
- Failed
State: ENABLED
Targets:
- Arn: #作成したLambdaのARN
Id: lambda
PermissionForEventsToInvokeLambda:
Type: 'AWS::Lambda::Permission'
Properties:
FunctionName: SendSNSRunCommandFailure
Action: 'lambda:InvokeFunction'
Principal: events.amazonaws.com
SourceArn: !GetAtt
- EventRule
- Arn
Lambdaの構成
英語が苦手な人の為に、日本語のメールが配信されるようにテンプレートを用意しました。
またLambda自体が何らかの理由で異常終了したにもメールを配信するようにします。
SendSNSRunCommandFailure
┣lambda_function.py
┗template
┣mail_context.txt ---RunCommandメールのテンプレ
┗error_mail_context.txt ---Lambda自体が異常終了した際に送信するメールのテンプレ
mail_context.txtの中身
RunCommandが失敗した時刻、コマンドID、インスタンスIDをメールに表示します。
英語が苦手な人は、SNSのunsubscription URLを訳もわからず押してしまうこともあるかもしれません。
優しい文言を加えておきます。
運用チーム各位
以下のアラートを検知しました。
------------------------------------------
発生日時 :ver_incident_date
コマンドID :ver_command_id
ドキュメント :ver_document
インスタンスID :ver_instance_id
------------------------------------------
RunCommandが異常終了しました。
下記リンクからコマンド履歴を確認の上調査をお願い致します
https://ap-northeast-1.console.aws.amazon.com/systems-manager/run-command/complete-commands?region=ap-northeast-1
******************************************************
本メールは自動送信です。
以下のリンク(※)をクリックしますとアラートメールが発報されなくなりますのでご注意ください。
※https://sns.ap-northeast-1.amazonaws.com/unsubscribe.html~
******************************************************
error_mail_context.txtの中身
Lambda自体が何らかの理由で異常終了した際のメールのテンプレートです。
運用チーム各位
下記のLambdaが異常終了しました。
ご確認をお願い致します。
【発生時刻】ver_error_date
【Lambda名称】ver_lambda
【エラー内容】
ver_error
******************************************************
本メールは自動送信です。
以下のリンク(※)をクリックしますとアラートメールが発報されなくなりますのでご注意ください。
※https://sns.ap-northeast-1.amazonaws.com/unsubscribe.html~
******************************************************
Lambdaのコード
Lambda
# coding: UTF-8
import boto3
import json
import dateutil.parser
import datetime
from dateutil.relativedelta import relativedelta
mail_template = './template/mail_context.txt'
error_mail_template = './template/error_mail_context.txt'
ssm = boto3.client('ssm')
sns = boto3.client('sns')
ope_team_sns_topic = 'OPE'
def lambda_handler(event, context):
"""
処理内容:最初に呼ばれる関数
Parameters
--------
event : dict
from StepFunction
context : dict
builtin args of Lambda
"""
global account_id
account_id = event['account']
command_id = event['detail']['command-id']
instance_id = event['detail']['instance-id']
document_name = event['detail']['document-name']
time = event['time']
try:
send_sns(time, command_id, instance_id, document_name)
except Exception as e:
send_error_sns(str(e), context.function_name)
raise e
def send_sns(time, command_id, instance_id, document_name):
"""
処理内容:send mail by SNS
Parameters
----------
time : datetime
イベント発生時刻
command_id : string
Systems MangerのコマンドID
instance_id : string
EC2
document_name : string
失敗したSSMドキュメント
"""
# アカウント名(和名取得)
account_name = get_account_name()
#メール件名の指定
subject = '[' + account_name + ']' + '['+ document_name + ']' + 'ERROR'
#メール本文の指定
sns_body = create_text(time, command_id, instance_id, document_name)
#送信先SNSトピックの指定
ope_topic_arn = 'arn:aws:sns:ap-northeast-1:'+ account_id + ':' + ope_team_sns_topic
#SNSへのパブリッシュ
response = sns.publish(
TopicArn = ope_topic_arn,
Subject = subject,
Message = json.dumps(sns_body, ensure_ascii=False),
MessageStructure='json'
)
def get_account_name():
"""
処理内容:パラメータストアからアカウント名(和名)を取得
Returns
--------
account_name : string
アカウント名(和名)
"""
ssm_response = ssm.get_parameters(
Names = [
"AccountName_ja"
],
WithDecryption=True
)
return ssm_response['Parameters'][0]['Value']
def create_text(time, command_id, instance_id, document_name):
"""
処理内容:本文を作る
Parameters
----------
time : datetime
イベント発生時刻
command_id : string
Systems MangerのコマンドID
instance_id : string
EC2
document_name : string
失敗したSSMドキュメント
"""
#時刻の整形(UTC→JST)
date_before = dateutil.parser.parse(time) + datetime.timedelta(hours=9)
tmp_incident_date = date_before.strftime('%Y/%m/%d %H:%M:%S')
with open(mail_template) as f:
mail_context = f.read()
# アラート発報時刻
mail_context = mail_context.replace('ver_incident_date', tmp_incident_date)
# 異常終了したコマンド
mail_context = mail_context.replace('ver_command_id', command_id)
# 異常終了したインスタンス
mail_context = mail_context.replace('ver_instance_id', instance_id)
# 異常終了したインスタンス
mail_context = mail_context.replace('ver_document', document_name)
#メール文の整形
tmp_sns_body = {}
tmp_sns_body["default"] = mail_context + "\n"
return tmp_sns_body
def send_error_sns(error, lambda_name):
"""
Lambdaが異常終了した際にSNSメールを送信
Parameters
----------
error : string
error message when error occurs
lambda_name : string
"""
now = datetime.datetime.now() + datetime.timedelta(hours=9)
now = now.strftime('%Y/%m/%d %H:%M:%S')
with open(error_mail_template) as f:
data_lines = f.read()
data_lines = data_lines.replace('ver_error_date', now)
data_lines = data_lines.replace('ver_lambda', lambda_name)
data_lines = data_lines.replace('ver_error', error)
#メール文の整形
error_sns_body = {}
error_sns_body["default"] = data_lines + "\n"
#送信先SNSトピックの指定
topic = 'arn:aws:sns:ap-northeast-1:'+ account_id + ':' + ope_team_sns_topic
#メール件名の指定
subject = '[Lambda Error] ['+ account_id +'][' + lambda_name +']'
#SNSへのパブリッシュ
try:
response = sns.publish(
TopicArn = topic,
Message = json.dumps(error_sns_body, ensure_ascii=False),
Subject = subject,
MessageStructure='json'
)
except Exception as e:
print(str(e))
raise e
テスト
RunCommandを失敗させる
StateManagerの関連付けが適用されると、SSM Agent・CloudWatch Agentがアップデートされます。
Agentのアップデートが失敗したらメールが配信されるのですが、StateManagerを失敗させるのは面倒です。
AWS-UpdateSSMAgent・AWS-ConfigureAWSPackageの失敗だけでなくAWS-RunShellScriptというSSMドキュメントもEventBridge Ruleの監視対象にして、敢えて失敗するRunCommandを実行してみます。