Skip to content

Instantly share code, notes, and snippets.

@mattsan mattsan/20191223.md Secret
Last active Dec 26, 2019

Embed
What would you like to do?
ESM Real Lounge #4

どこにでもいるLambda

title

解決したいこと/実現したいこと

「EC2 のインスタンス上でもっと自在にコマンドを実行したい」

  • 簡単にバッチ処理を実行したい
  • 定期的に実行したい処理を追加したい
  • ワンショットで実行したいことがある

※ あんまり Lambda の話ぢゃなかった

SSM を使ってコマンドを実行する

  • コマンドを受け付ける側に必要なこと(EC2)
    • SSM からコマンドを受けられるロール/権限が設定されている
    • SSM Agent がインストールされている
  • コマンドを送る側に必要なこと(Lambda 等)
    • 権限 ssm:SendCommand が設定されている

詳細は AWS Systems Manager の権限リファレンス - AWS Systems Manager を参照してください。

SSM / AWS System Manager

AWS Systems Manager(運用時の洞察を改善し、実行)| AWS

ロールを作成する

EC2 インスタンスが SSM からのコマンドを受け付けられるようにするには、そのためのポリシーを持ったロールをインスタンスに設定する必要があります。 まずそのロールを用意します。

サービスに EC2 を選択する

"AmazonEC2RoleforSSM" を選択する

名前をつけて保存する

EC2 インスタンスを作成する

「IAM ロール」に先に作成したロールを設定します。

またインスタンスには SSM Agent がインストールされている必要があります。 SSM Agent をインストールするスクリプトを「ユーザーデータ」に記述しておきます。

詳細は Linux インスタンスでの起動時のコマンドの実行 - Amazon Elastic Compute Cloud を参照してください。

スクリプトの例です。 記述する内容はリージョンやインスタンスの種類に合わせる必要があります。

#!/bin/bash
yum update -y
curl https://amazon-ssm-ap-northeast-1.s3.amazonaws.com/latest/linux_amd64/amazon-ssm-agent.rpm \
       -o amazon-ssm-agent.rpm
yum install amazon-ssm-agent.rpm -y

コマンドを実行する

コマンドを実行できることをますコンソールから確認します。 SSM のコンソールに移動します。

サイドメニューの「Run Command」を選択します。

コマンドの種類を選択する

AWS-RunShellScript を選択します。

実行したいスクリプトを記述する

ここでは例として echo Hello と記述しています。

ターゲットを選択する

"Choose Instances manually" を選択すると先ほど作成した EC2 インスタンスが一覧に表示されるので、そのインスタンスを選択します。

出力オプションを設定する

実行結果の出力先を設定します。 ここでは S3 を出力先として設定しています。

実行

ログを確認する

コマンドの実行で指定した bucket に、同じく実行時に指定した prefix が付いた object が作成されます。

指定した prefix の後はコマンド ID になっているので、これで対応するログを見つけることができます。

SSM のコマンド実行を Ruby で記述する

コマンドを実行するコード

AWS のコンソールでできることは、だいたい AWS CLIAWS SDK for Ruby でも実現できるようになっています。

SSM には SDK に Aws::SSM::Client というクラスが用意されています。 これのメソッド send_command を利用してコマンドを実行します。

実行結果は、コンソールでの例と同様に、S3 に出力するように設定しています。

EC2 インスタンスはインスタンス ID で指定する必要があります。

require 'aws-sdk-ssm'

client = Aws::SSM::Client.new(region: 'ap-northeast-1')

client.send_command(
  document_name: 'AWS-RunShellScript',
  instance_ids: instance_ids, # How to get?
  parameters: {
    commands: [
      "runuser -l ec2-user -c 'echo Hello'"
    ]
  },
  output_s3_bucket_name: 'real-lounge',
  output_s3_key_prefix: 'emattsan'
)

インスタンス ID を取得する

EC2 のインスタンス ID の取得には Aws::EC2::Client のメソッド describe_instances を利用します。

事前にタグなどを設定してインスタンスを特定できるようにしておきます。

require 'aws-sdk-ec2'

client = Aws::EC2::Client.new(region: 'ap-northeast-1')

response =
  client.describe_instances(
    filters: [
      {name: 'instance-state-name', values: ['running']}, # 実行中のインスタンス
      {name: 'tag:component', values: ['esm']},           # タグ key: component, value: esm
      {name: 'tag:stage', values: ['staging']}            # タグ key: stage, value: staging
    ]
  )

instance_ids = response.reservations.flat_map(&:instances).flat_map(&:instance_id)

実行結果を取得する

S3 に保存された実行結果を取得します。

Aws::SSM::Client#send_command の戻り値にはコマンド ID が含まれるので、これと実行時に指定した prefix とを合わせて保存先の bucket の内容を検索し、結果を取得します。

# Aws::SSM::Client#send_command の実行結果
# response = ssm_client.send_command(...)

prefix = ['emattsan', response.command.command_id].join('/')

require 'aws-sdk-s3'

client = Aws::S3::Client.new(region: 'ap-northeast-1')

response =
  client.list_objects(
    bucket: 'real-lounge',
    prefix: prefix
  )

response.contents.map(&:key).each do |key|
  object =
    client.get_object(
      bucket: 'real-lounge',
      key: key
    )

  pp object.body.read
end

Lambda からコマンドを実行する

インスタンス ID の取得とコマンドの実行を Lambda のハンドラにまとめます。 ハンドラの記述やデプロイについては 以前の資料 を参照してください。

require 'aws-sdk-ec2'
require 'aws-sdk-ssm'

def get_instance_ids
  client = Aws::EC2::Client.new(region: 'ap-northeast-1')

  response =
    client.describe_instances(
      filters: [
        {name: 'instance-state-name', values: ['running']},
        {name: 'tag:component', values: ['esm']},
        {name: 'tag:stage', values: ['staging']}
      ]
    )

  response.reservations.flat_map(&:instances).flat_map(&:instance_id)
end

def send_command(instance_ids, command)
  client = Aws::SSM::Client.new(region: 'ap-northeast-1')

  client.send_command(
    document_name: 'AWS-RunShellScript',
    instance_ids: instance_ids,
    parameters: {
      commands: [
        "runuser -l ec2-user -c '#{command}'"
      ]
    },
    output_s3_bucket_name: 'real-lounge',
    output_s3_key_prefix: 'emattsan'
  )
end

def handler(event: event, context:)
  instance_ids = get_instance_ids

  send_command(instance_ids, event.command)
end

Lambda から実行することの恩恵

  • 多彩なイベントソース
    • API Gateway
    • AWS IoT
    • Alexa Skills Kit
    • Allexa Smart Home
    • Application Load Balancer
    • CloudWatch Events
    • CloudWatch Logs
    • CodeComment
    • Cognito Sync Trigger
    • DynamoDB
    • Kinesis
    • S3
    • SNS
    • SQS
  • 柔軟な構成
    • SSM Agent を用意してさえおけば、後から「しくみ」をいろいろ追加できる
  • インテリジェントなコマンド
    • Lambda 自体が Ruby のコマンドを実行する主体なので、実行の制御を記述できる

ダメ。ゼッタイ。

自在にバックドアを開けられるマスターキーになりうる。

用途

実際にプロジェクトで実践しているものとして:

  • CloudWatch Events を利用した定期バッチ処理の実行
  • データ構造の変更にともなうデータのマイグレーション(データの投入や加工)
    • rails db:migrate と実行を分離したいケース
  • データベースからのデータの抽出
    • データベースへの直接アクセスを回避しつつ、必要なデータを抽出する

まとめ

  • SSM を使うことで EC2 のコマンドを実行できる
  • 実行に Lambda を利用することで実行形態の選択肢が増える
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.