オートスケール起動のEC2のリモート操作(Run Command)を、権限限定したIAMロールで行ってみた

オートスケールで起動した EC2インスタンス(Amazon Linux2)のセキュアなリモート操作を、AWS Systems Manager (Run Command)で実現してみました。
2020.09.08

AWSチームのすずきです。

AWS Systems Manager の 実行コマンド(Run Command)を利用する事で、複数インスタンスのリモート操作が可能になります。

今回、SSMドキュメントと実行対象のEC2インスタンスを限定した SSM (Run Command) 操作用のIAMロールを用意、 オートスケールで 起動した EC2インスタンス(Amazon Linux2)の セキュアなリモート操作を実現する機会がありましたので、紹介させていただきます。

構成図

AWS設定

以下のCloudFormationテンプレートを利用して、EC2、IAM、SSMの設定を実施しました。

ssm-sendcomand-ec2-autoscalling-al2.yaml

EC2

AMI

Amazon Linux2 (X86) の最新AMIをパラメータストアから取得して利用しました。

Parameters:
  Ec2ImageId:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-ebs
Resources:
  Ec2LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        ImageId: !Ref 'Ec2ImageId'

UserData

Amazon Linux 2 のAMI、SSMエージェントはプリインストールされていますが、 最新バージョンへのアップデートを実施して利用しました。

        UserData: !Base64
          Fn::Sub: |
            #cloud-config
            runcmd:
              - yum update amazon-ssm-agent

IAMロール

管理ポリシー「AmazonSSMManagedInstanceCore」をインスタンスプロファイルとして設定しました。

    Properties:
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

VPC

VPC指定は省略、Internet Gateway(IGW)が利用できるデフォルトVPCを利用する指定としました。

SSM

リモート操作の内容を定義したドキュメントを用意しました。

Document

「systemctl」を実行し、エラー発生時はエラー応答(1)を戻す指定としました。

  SsmDocument:
    Type: AWS::SSM::Document
    Properties:
      Content:
        schemaVersion: '2.2'
        description: Run a script on Linux (systemctl)
        mainSteps:
          - action: aws:runShellScript
            name: runCommands
            inputs:
              timeoutSeconds: '60'
              runCommand:
                - systemctl status amazon-ssm-agent.service
                - if [ $? -ne 0 ]; then exit 1; fi
                - sleep 10
      DocumentType: Command

IAM

User

SSM実行用ロールへのスイッチを許可したIAMユーザを用意しました。

許可対象のリソースはCloudFormationのリソース間での循環参照を回避するため、前方一致での指定としています。

  IamUserAssumeRole:
    Type: AWS::IAM::User
    Properties:
      Policies:
        - PolicyName: AllowAssumeSsmSendCommand
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource:
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-*'

Role

スイッチロール用のIAMユーザを信頼先とした、IAMロールを用意しました。

リモート操作対象のEC2は、タグが指定したオートスケール名(aws:autoscaling:groupName)に完全一致する事。 指定したSSMドキュメントのみに限定した許可設定としました。

  IamRoleSendCommand:
    Type: AWS::IAM::Role
    DependsOn: IamUserAssumeRole
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !GetAtt 'IamUserAssumeRole.Arn'
            Action: 
              - sts:AssumeRole
              - sts:TagSession
      Policies:
        - PolicyName: allow_ssm_sendcomand
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ssm:ListCommandInvocations
                Resource:
                  - '*'
              - Effect: Allow
                Action:
                  - ssm:SendCommand
                Resource:
                  - !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:document/${SsmDocument}'
              - Effect: Allow
                Action:
                  - ssm:SendCommand
                Resource:
                  - !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*'
                Condition:
                  StringEquals:
                    ssm:ResourceTag/aws:autoscaling:groupName: !Ref 'Ec2InstanceAutoScalingGroup'

Outputs

後述のCLI、実行スクリプトとして引数などを反映した内容を、CloudFormationのOutputできる設定としました。

Parameters:
  OutputCliScripts:
    Description: Output Cli Scripts
    Type: String
Conditions:
  IsOutputCliScripts: !Equals [ !Ref OutputCliScripts, "Enable" ]
Outputs:
  step5:
    Description: ssm send-command
    Value: !Sub 'TMP=`aws --region ${AWS::Region} 
      ssm send-command --document-name "${SsmDocument}"
      --targets "Key=tag:aws:autoscaling:groupName,Values=${Ec2InstanceAutoScalingGroup}"
      --max-concurrency=1 --max-errors=0`; 
      echo $TMP | jq .;
      CommandId=`echo $TMP | jq -r .Command.CommandId`'
    Condition: IsOutputCliScripts
  step6:
    Description: ssm list-command-invocations
    Value: !Sub 'TMP=`aws --region ${AWS::Region}
      ssm list-command-invocations --command-id $CommandId`;
      echo $TMP | jq ''.CommandInvocations[]|{InstanceId:.InstanceId,Status:.Status,StatusDetails:.StatusDetails,RequestedDateTime:.RequestedDateTime}'''
    Condition: IsOutputCliScripts

疎通確認後はアクセスキーは無効化、ローテーションを実施する前提でご利用ください。

実行

Amazon Linux2 付属パッケージの「awscli」と「jq」をインストールして利用しました。

$ aws --version
aws-cli/1.18.107 Python/2.7.18 Linux/4.14.193-149.317.amzn2.aarch64 botocore/1.17.31
$ jq --version
jq-1.5

STS

スイッチロール用のIAMアクセスキー、シークレットキーを利用してスイッチロール(AssumeRole)を行います。

export AWS_ACCESS_KEY_ID=aaaa
export AWS_SECRET_ACCESS_KEY=aaaaaaa
export AWS_DEFAULT_REGION=ap-northeast-1
TMP=`aws sts assume-role --role-arn arn:aws:iam::000000000000:role/aaa-IamRoleSendCommand-aaa --role-session-name cli-session`
export AWS_ACCESS_KEY_ID=`echo $TMP| jq -r .Credentials.AccessKeyId`
export AWS_SECRET_ACCESS_KEY=`echo $TMP| jq -r .Credentials.SecretAccessKey`
export AWS_SESSION_TOKEN=`echo $TMP| jq -r .Credentials.SessionToken`

SSM

SSMドキュメントと、操作対象のEC2インスタンスのタグを指定してRun Commandを実行します。

send-command

TMP=`aws ssm send-command --document-name "aaa-SsmDocument-aaa" --targets "Key=tag:aws:autoscaling:groupName,Values=aaa-Ec2InstanceAutoScalingGroup-aaa" --max-concurrency=1`;
CommandId=`echo $TMP | jq -r .Command.CommandId`; 
echo $TMP | jq .
  • 実行結果
{
  "Command": {
    "MaxErrors": "0",
    "Parameters": {},
    "DocumentName": "aa-SsmDocument-aaa",
    "OutputS3BucketName": "",
    "OutputS3KeyPrefix": "",
    "StatusDetails": "Pending",
    "RequestedDateTime": 1599492314.094,
    "Status": "Pending",
    "TimeoutSeconds": 3600,
    "TargetCount": 0,
    "NotificationConfig": {
      "NotificationArn": "",
      "NotificationEvents": [],
      "NotificationType": ""
    },
    "InstanceIds": [],
    "ErrorCount": 0,
    "MaxConcurrency": "1",
    "ServiceRole": "",
    "CloudWatchOutputConfig": {
      "CloudWatchLogGroupName": "",
      "CloudWatchOutputEnabled": false
    },
    "DocumentVersion": "",
    "CompletedCount": 0,
    "Comment": "",
    "ExpiresAfter": 1599495974.094,
    "DeliveryTimedOutCount": 0,
    "CommandId": "aa-aa-aa-aa-aa",
    "Targets": [
      {
        "Values": [
          "aaa-Ec2InstanceAutoScalingGroup-aaa"
        ],
        "Key": "tag:aws:autoscaling:groupName"
      }
    ]
  }
}

list-command-invocations

操作対象EC2インスタンス、のステータスが「成功」である事が確認できました。

TMP=`aws ssm list-command-invocations --command-id $CommandId`
echo $TMP | jq '.CommandInvocations[]|{InstanceId:.InstanceId,Status:.Status,StatusDetails:.StatusDetails,RequestedDateTime:.RequestedDateTime}'
{
  "InstanceId": "i-aaaaaaaa",
  "Status": "Success",
  "StatusDetails": "Success",
  "RequestedDateTime": 1599492314.476
}
{
  "InstanceId": "i-bbbbbbbb",
  "Status": "Success",
  "StatusDetails": "Success",
  "RequestedDateTime": 1599492314.36
}

例外

許可対象以外

SSMドキュメント、EC2インスタンスが事前に許可されたもの以外である場合、SSM操作はエラーとなります。

$ aws ssm send-command --document-name "xx-SsmDocument-xx"
An error occurred (AccessDeniedException) when calling the SendCommand operation: User: arn:aws:sts::000000000000:assumed-role/aa-IamRoleSendCommand-aaa/cli-session is not authorized to perform: ssm:SendCommand on resource: arn:aws:ssm:ap-northeast-1:784693731708:document/xx-SsmDocument-xx
$ aws ssm send-command --document-name "aa-SsmDocument-aaa" --instance-ids i-cccccccc
An error occurred (AccessDeniedException) when calling the SendCommand operation: User: arn:aws:sts::000000000000:assumed-role/aa-IamRoleSendCommand-aaa/cli-session is not authorized to perform: ssm:SendCommand on resource: arn:aws:ssm:ap-northeast-1:784693731708:document/xx-SsmDocument-xx

ドキュメント

SSMドキュメントに定義した、リモート操作コマンドが失敗した場合の動作を確認しました。

              runCommand:
                - systemctl status dummy
                - if [ $? -ne 0 ]; then exit 1; fi
                - sleep 10

systemctl、無効なサービスを指定しました。

$ systemctl status dummy
Unit dummy.service could not be found.
$ echo $?
4

ssm list-command-invocations より、1台目の処理で「Failed」、2台目は「Terminated」で処理が中断。

「max-concurrency=1、「max-errors=0」の指定が正しく機能し、エラーの影響範囲が限定した利用が可能な事を確認できました。

{
  "InstanceId": "i-aaaaaaaa",
  "Status": "Failed",
  "StatusDetails": "Terminated",
  "RequestedDateTime": 1599492639.381
}
{
  "InstanceId": "i-bbbbbbbb",
  "Status": "Failed",
  "StatusDetails": "Failed",
  "RequestedDateTime": 1599492639.291
}
  • System Managerダッシュボード(Run Command)

まとめ

EC2 Auto Scaling で実行中のEC2インスタンス、リモート操作をSSM経由で実施する事ができました。

SSM を利用する事で OSの特権操作が可能となりますが、適切なIAM権限設定を行う事で意図せぬ誤操作の回避や、CloudTrail による証跡記録も期待できます。

大きな変更を実施する場合、より多機能 B/Gデプロイなどに対応する AWS CodeDeploy や、UpdateStack を利用した EC2 の交換 が望ましい場合もあると思われますが、 OSやミドルウェアの設定変更などで 小規模な変更を行う必要がある場合、Amazon Linux 2 のAMIで起動したEC2であれば、EC2ロールの設定のみで利用可能な SSM をご活用ください。