目次
気がついたら Systems Manager Automation(以下 SSM Automation)ばっかりさわってました、那須です。
ある業務で AD ユーザの追加や削除が運用で発生するんですが、それを承認履歴や作業履歴を残しつつ人が承認された内容以外の作業をしないような仕組みを作る、という仕事がありました。 これまではメールにユーザ一覧を添付してメール本文で作業内容を連絡して、承認者はそのメールに返信する形で承認や拒否を行っていました。
監査の時にこのフローに対して指摘があったので、これを機に真剣に仕組み化してしまおうということになって考えた末に出来上がったバージョン 1 をご紹介します(これがベスト!というわけではないので進化していくことでしょう)
フローチャート的なもの
こういう図を描くのは高校の授業以来でしょうか。 懐かしさと同時にあの頃から情報技術というものが少しずつ嫌いになっていったのを思い出しますw
今は好きなので思い出しながら描いてみました。 数字が後ほどご紹介する SSM Automation ドキュメントのステップを表しています。
作成するリソース
SSM Automation ドキュメント
申請者が実行する SSM Automation ドキュメントです。 GUI で設定するのは大変なので YAML でお伝えします。 ひとまず以下にコンテンツをそのまま貼ってみます。
description: |-
# 必須パラメータ
Action: ユーザ追加の場合は **add** 、ユーザ削除の場合は **del** を選択します。
Username: 追加するユーザ名を指定します。
Approver: 承認者を指定します。
# 任意パラメータ
Messages: 作業承認依頼に添付するコメントを記載します。
# ユーザ追加の場合のみ必須のパラメータ
Lastname: 追加するユーザの姓を指定します。
Firstname: 追加するユーザの名を指定します。
# 承認者リスト
以下から選択してください。
- 〇〇さん:approver1
- △△さん:approver2
- □□さん:approver3
schemaVersion: '0.3'
parameters:
Action:
type: String
description: (必須) ADユーザを追加するのか削除するのかを指定します。
allowedValues:
- add
- del
Username:
type: String
allowedPattern: .+
description: (必須) 追加するユーザ名を指定します。メールアドレスのアカウント部分を指定してください。
Lastname:
type: String
default: ''
description: (ユーザ追加のみ) 追加するユーザの姓を指定します。
Firstname:
type: String
default: ''
description: (ユーザ追加のみ) 追加するユーザの名を指定します。
Messages:
type: String
default: ''
description: (任意) 作業承認依頼に添付するコメントを記載します。
Approver:
type: String
description: (必須) 承認者を指定します。
allowedValues:
- approver1
- approver2
- approver3
mainSteps:
- name: GetApproverId
action: 'aws:executeAwsApi'
onFailure: Abort
inputs:
Service: ssm
Api: GetParameter
Name: '/approver/{{ Approver }}'
outputs:
- Name: approver
Selector: $.Parameter.Value
Type: String
- name: GetInstanceId
action: 'aws:executeAwsApi'
outputs:
- Name: InstanceId
Type: String
Selector: '$.Reservations[0].Instances[0].InstanceId'
inputs:
Service: ec2
Api: DescribeInstances
Filters:
- Name: 'tag:Name'
Values:
- AD-management
- name: GetAutomationExecutor
action: 'aws:executeAwsApi'
outputs:
- Name: Executor
Type: String
Selector: $.AutomationExecution.ExecutedBy
inputs:
Service: ssm
Api: GetAutomationExecution
AutomationExecutionId: '{{automation:EXECUTION_ID}}'
- name: CheckParameters
action: 'aws:branch'
inputs:
Choices:
- And:
- Variable: '{{Action}}'
StringEquals: add
- Not:
Variable: '{{Username}}'
StringEquals: ''
- Not:
Variable: '{{Lastname}}'
StringEquals: ''
- Not:
Variable: '{{Firstname}}'
StringEquals: ''
- Not:
Variable: '{{Approver}}'
StringEquals: ''
NextStep: NotifySlackForAdd
- And:
- Variable: '{{Action}}'
StringEquals: del
- Not:
Variable: '{{Username}}'
StringEquals: ''
- Not:
Variable: '{{Approver}}'
StringEquals: ''
NextStep: NotifySlackForDel
Default: NotEnoughParameters
- name: NotEnoughParameters
action: 'aws:invokeLambdaFunction'
isEnd: true
inputs:
FunctionName: 'arn:aws:lambda:ap-northeast-1:111111111111:function:NotifySlack'
Payload: >-
{"action": "NotEnoughParameters", "stop_action": "{{Action}}",
"execute_id": "{{automation:EXECUTION_ID}}", "comment": "{{Messages}}",
"username": "{{Username}}", "requester":
"{{GetAutomationExecutor.Executor}}" }
- name: NotifySlackForAdd
action: 'aws:invokeLambdaFunction'
nextStep: Approve
onFailure: 'step:Approve'
inputs:
FunctionName: 'arn:aws:lambda:ap-northeast-1:111111111111:function:NotifySlack'
Payload: >-
{"action": "{{Action}}", "execute_id": "{{automation:EXECUTION_ID}}",
"comment": "{{Messages}}", "lastname": "{{Lastname}}", "firstname":
"{{Firstname}}", "username": "{{Username}}", "approver": "{{Approver}}",
"requester": "{{GetAutomationExecutor.Executor}}" }
- name: NotifySlackForDel
action: 'aws:invokeLambdaFunction'
nextStep: Approve
onFailure: 'step:Approve'
inputs:
FunctionName: 'arn:aws:lambda:ap-northeast-1:111111111111:function:NotifySlack'
Payload: >-
{"action": "{{Action}}", "execute_id": "{{automation:EXECUTION_ID}}",
"comment": "{{Messages}}", "username": "{{Username}}", "approver":
"{{Approver}}", "requester": "{{GetAutomationExecutor.Executor}}" }
- name: Approve
action: 'aws:approve'
nextStep: DefineAction
inputs:
Approvers:
- >-
arn:aws:sts::111111111111:assumed-role/Role/{{GetApproverId.approver}}
NotificationArn: 'arn:aws:sns:ap-northeast-1:111111111111:send-email-to-{{Approver}}'
Message: '{{Messages}} '
outputs:
- Name: Comment
Selector: $
Type: MapList
- name: DefineAction
action: 'aws:branch'
inputs:
Choices:
- Variable: '{{Action}}'
StringEquals: add
NextStep: AddADUser
- Variable: '{{Action}}'
StringEquals: del
NextStep: DelADUser
isEnd: true
- name: AddADUser
action: 'aws:runCommand'
isEnd: true
inputs:
DocumentName: AWS-RunPowerShellScript
InstanceIds:
- '{{ GetInstanceId.InstanceId }}'
Parameters:
workingDirectory: 'C:\scripts'
commands:
- '. C:\scripts\functions\function.ps1'
- '. C:\scripts\functions\parameters.ps1'
- >-
$parameters_json = aws ssm get-parameter --name /ad-admin-pass
--with-decryption
- $parameters = $parameters_json | convertfrom-json
- >-
$Credential = CreateCredential -User $User -PasswordString
$parameters.Parameter.Value
- >-
$SlackUrl_json = aws ssm get-parameter --name slack_url
--with-decryption
- $SlackUrl_dict = $SlackUrl_json | convertfrom-json
- $SlackUrl = $SlackUrl_dict.Parameter.Value
- |-
try {
$Password = GeneratePassword
New-ADUser {{ Username }} `
-GivenName {{ Firstname }} -Surname {{ Lastname }} `
-Path "OU=Users,OU=ad,DC=ad,DC=beex,DC=test" `
-AccountPassword (ConvertTo-SecureString -AsPlainText $Password -Force) `
-PasswordNeverExpires $true -Enabled $true -Credential $Credential
Get-ADUser {{ Username }} -Credential $Credential
}
catch {
Write-Output "Creating the user {{ Username }} failed."
$payload = @{
attachments = @(
@{
color = "danger"
title = "Creating the user {{ Username }} failed.";
text = $error[0].Exception.Message
}
)
} | ConvertTo-Json
NotifyToSlack -SlackUrl $SlackUrl -Payload $payload
throw
}
- |-
$payload = @{
attachments = @(
@{
color = "good"
title = "Creating the user {{ Username }} finished successfully.";
text = "No detail."
}
)
} | ConvertTo-Json
- NotifyToSlack -SlackUrl $SlackUrl -Payload $payload
- >-
SendEmailToAddedUser -To {{Username}}@beex-inc.com -Username
{{Username}} -Password $Password
description: Add the specified AD user.
- name: DelADUser
action: 'aws:runCommand'
isEnd: true
inputs:
DocumentName: AWS-RunPowerShellScript
InstanceIds:
- '{{ GetInstanceId.InstanceId }}'
Parameters:
workingDirectory: 'C:\scripts'
commands:
- '. C:\scripts\functions\function.ps1'
- '. C:\scripts\functions\parameters.ps1'
- >-
$parameters_json = aws ssm get-parameter --name /ad-admin-pass
--with-decryption
- $parameters = $parameters_json | convertfrom-json
- >-
$Credential = CreateCredential -User $User -PasswordString
$parameters.Parameter.Value
- >-
$SlackUrl_json = aws ssm get-parameter --name slack_url
--with-decryption
- $SlackUrl_dict = $SlackUrl_json | convertfrom-json
- $SlackUrl = $SlackUrl_dict.Parameter.Value
- |-
try {
Remove-ADUser "CN={{Username}},OU=Users,OU=ad,DC=ad,DC=beex,DC=test" -Confirm:$False -Credential $Credential
}
catch {
Write-Output "Removing the user {{Username}} failed."
$payload = @{
attachments = @(
@{
color = "danger"
title = "Removing the user {{Username}} failed.";
text = $error[0].Exception.Message
}
)
} | ConvertTo-Json
NotifyToSlack -SlackUrl $SlackUrl -Payload $payload
throw
}
- 'Write-Output "User {{Username}} has removed successfully."'
- |-
$payload = @{
attachments = @(
@{
color = "good"
title = "Removing the user {{Username}} finished successfully.";
text = "No detail."
}
)
} | ConvertTo-Json
- NotifyToSlack -SlackUrl $SlackUrl -Payload $payload
description: Delete the specified AD user.
通知のための Lambda 関数
主に Slack に通知するための Lambda 関数です。 Slack で表示させたい内容を JSON で作ってそれを Request で送信しているだけです。
Slack に通知するためには Slack の URL が必要ですが、それは slack_url という名前の SSM パラメータストアに SecureString として保存しているのでそこから取ってきています。
from logging import getLogger, INFO
import boto3
import os
import json
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from base64 import b64decode
logger = getLogger()
logger.setLevel(INFO)
ssm = boto3.client('ssm')
def notify_slack(slack_message):
slack_url = ssm.get_parameter(
Name='slack_url',
WithDecryption=True
)["Parameter"]["Value"]
req = Request(slack_url, json.dumps(slack_message).encode('utf-8'))
try:
response = urlopen(req)
response.read()
logger.info("Message posted")
except HTTPError as e:
logger.error("Request failed: %d %s", e.code, e.reason)
except URLError as e:
logger.error("Server connection failed: %s", e.reason)
def lambda_handler(event, context):
execute_id = event["execute_id"]
approval_url = f"https://ap-northeast-1.console.aws.amazon.com/systems-manager/automation/execution/{execute_id}/approval?region=ap-northeast-1"
if event["action"] == "add":
name = event["lastname"] + ' ' + event["firstname"]
attachments_json = [
{
"color": "good",
"title": "XXX ADユーザ追加作業承認依頼",
"fields": [
{
"title": "追加するユーザID",
"value": event["username"],
"short": True
},
{
"title": "追加するユーザの名前",
"value": name,
"short": True
},
{
"title": "申請者コメント",
"value": event["comment"],
"short": False
},
{
"title": "承認URL",
"value": approval_url,
"short": False
},
{
"title": "承認者",
"value": event["approver"],
"short": True
},
{
"title": "依頼者",
"value": event["requester"],
"short": True
}
]
}
]
elif event["action"] == "del":
attachments_json = [
{
"color": "good",
"title": "XXX ADユーザ削除作業承認依頼",
"fields": [
{
"title": "削除するユーザID",
"value": event["username"],
"short": True
},
{
"title": "申請者コメント",
"value": event["comment"],
"short": False
},
{
"title": "承認URL",
"value": approval_url,
"short": False
},
{
"title": "承認者",
"value": event["approver"],
"short": True
},
{
"title": "依頼者",
"value": event["requester"],
"short": True
}
]
}
]
elif event["action"] == "NotEnoughParameters":
attachments_json = [
{
"color": "warning",
"title": "パラメータ不足の作業承認依頼がありました",
"fields": [
{
"title": "依頼アクション",
"value": event["stop_action"],
"short": True
},
{
"title": "削除するユーザID",
"value": event["username"],
"short": True
},
{
"title": "申請者コメント",
"value": event["comment"],
"short": False
},
{
"title": "Automationの実行ID",
"value": event["execute_id"],
"short": True
},
{
"title": "依頼者",
"value": event["requester"],
"short": True
}
]
}
]
slack_message = {
'attachments': attachments_json
}
notify_slack(
slack_message
)
簡単な解説
最初の 3 ステップで承認する人や申請する人を決めています。 また AD を操作する Windows Server インスタンスがどれなのかも定義しています。 その後に入力パラメータをチェックして、不足があれば情報が足りない旨を Slack に通知するために Lambda 関数を実行しています。
無事に入力パラメータのチェックに合格したら、その依頼がユーザの追加なのか削除を判定します。 判定する理由は Slack 通知の内容で追加なのか削除なのかを明記するためです。 実際の通知イメージは↓こんな感じです。
通知のための Lambda 関数実行のステップは成功しても失敗してもその後の承認ステップに移ります。 承認ステップでは、Amazon SNS を使って承認者にメール送信しています。 この承認ステップがあるので承認 URL が生成されます。
承認 URL をクリックすると承認/拒否を指定する AWS 管理コンソールに遷移します。
拒否すればここで終わりですが、承認されると再度ユーザの追加なのか削除なのかを判定して実際に AD にユーザを追加したり削除したりします。
ユーザの追加削除ですが、ステップ 2 で EC2 インスタンス ID を特定していてそこに対して SSM RunCommand を使って PowerShell で New-ADUser や Remove-ADUser を実行しています。 その時に必要なオプションの内容が、SSM Automation の入力パラメータになっています。
実際書いてみると全然簡単な説明になっていない気がしますが、YAML を読み解いたり実際にこれで SSM Automation ドキュメントを作ってもらうと動きがわかってもらえると思いますので、ぜひやってみてください!
さいごに
監査が関係するような運用作業が手作業だったりすると監査担当の人が監査の日につらい思いをすることが多いと思います。 今回ご紹介した仕組みはあくまで一例ですが、自動化や仕組み化するヒントにはなるんじゃないかなと思っています。
SSM Automation って最初は何が原因かわからないとっつきにくさがあったんですが、今ではこれを使いこなすことでいろいろな手作業がなくなっていっていますよ! 大変なのは最初だけなので、皆さんもぜひ SSM Automation を使ってみてください。 え!?こんなことも自動でやってくれるの!?みたいな発見があって面白いですよ!
- カテゴリー