AD ユーザの追加や削除を承認も含めてやってくれる仕組みを考えた

この記事を書いたメンバー:

那須 隆

AD ユーザの追加や削除を承認も含めてやってくれる仕組みを考えた

目次

気がついたら 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 を使ってみてください。 え!?こんなことも自動でやってくれるの!?みたいな発見があって面白いですよ!

カテゴリー
タグ

Pick upピックアップ

Search記事を探す

キーワード

SAPシステムや基幹システムのクラウド移行・構築・保守、
DXに関して
お気軽にご相談ください

03-6260-6240 (受付時間 平日9:30〜18:00)