タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

Lambda から EC2 インスタンス内でコマンドを実行する

Amazon SSM を利用することで、Lambda を使って、EC2インスタンス内で任意のコマンドを実行することができます。

これを CloudWatch Events と組み合わせると、従来CRONによって行っていたような定期実行タスクを、特定のEC2インスタンスをSPOFにすることなく実装することができ、オートスケーリングを有効にしたEC2クラスタ環境などで便利です。

blog.takeyuweb.co.jp

Amazon EC2 Simple Systems Manager (SSM)

対象のEC2インスタンスには事前にSSMエージェントをインストールしておく必要があります。 手動や、cloud-initなどでインストールしておきます。

docs.aws.amazon.com

CloudFormation テンプレート(抜粋)

f:id:uzuki05:20200501174049p:plain

Parameters:
  Command:
    Type: String

  # 実行したい Lambda Function
  # 起動中のEC2インスタンスから1件を取り出してコマンドを実行する
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |+
          const AWS = require('aws-sdk');
          const ssm = new AWS.SSM({apiVersion: '2014-11-06'});
          const ec2 = new AWS.EC2({apiVersion: '2016-11-15'});
          const { Command } = process.env;

          const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
          const debug = (key, object) => { console.log(`DEBUG: ${key}\n`, JSON.stringify(object)); }

          class InstanceNotFoundError extends Error {
            constructor(message) {
              super(message);
              this.name = 'InstanceNotFoundError';
            }
          }

          exports.handler = async (event, context) => {
            console.log("INFO: request Recieved.\nEvent:\n", JSON.stringify(event));

            const describeInstancesParams = {
              Filters: [
                {
                  Name: "tag:Service",
                  Values: [
                    "app"
                  ]
                },
                {
                  Name: "instance-state-name",
                  Values: [
                    "running"
                  ]
                }
              ]
            };
            debug("describeInstancesParams", describeInstancesParams);
            const describeInstancesResult = await ec2.describeInstances(describeInstancesParams).promise();
            debug("describeInstancesResult", describeInstancesResult);
            const reservation = describeInstancesResult.Reservations[0];
            if (!reservation) {
              throw new InstanceNotFoundError("App is Not Found");
            }

            const instanceStatus = reservation.Instances[0];
            const sendCommandParams = {
              DocumentName: 'AWS-RunShellScript',
              InstanceIds: [instanceStatus.InstanceId],
              Parameters: {
                commands: [Command],
                executionTimeout: ['3600']
              },
              MaxConcurrency: '1',
              MaxErrors: '0',
              TimeoutSeconds: 3600,
            };
            debug("sendCommandParams", sendCommandParams);
            const sendCommandResult = await ssm.sendCommand(sendCommandParams).promise();
            debug("sendCommandResult", sendCommandResult);

            const results = {
              ec2InstanceId: instanceStatus.InstanceId,
              sendCommandParams: sendCommandParams,
              sendCommandResult: sendCommandResult
            };
            debug("results", results);
            return results;
          };
      Environment:
        Variables:
          Command: !Ref Command
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: "nodejs10.x"
      MemorySize: 128
      Timeout: 60

  # Lambda の実行ロール
  # 標準の AWSLambdaBasicExecutionRole サービスロールポリシーに加えて、
  # SSMコマンド実行用の ssm:SendCommand
  # EC2インスタンス一覧取得用の ec2:describeInstances
  # をインラインポリシーに追加
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - "sts:AssumeRole"
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
        - PolicyName: run-command
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - ssm:SendCommand
                  - ec2:describeInstances
                Resource: "*"