2018年2月12日月曜日

AutoScaling のライフサイクルフックで Lambda関数 を実行する


AutoScalingグループにライフサイクルフックを設定し、スケールアウト時に、Lambda関数を実行してみます。
このLambda関数では、ライフサイクルフックで保留状態のインスタンスにSSH接続して、コマンドを実行するようにします。
コマンド実行後は、保留を解除してライフサイクルを続行します。

ライフサイクルフックの詳細は下記URLを参照。
https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/lifecycle-hooks.html

1.lambda関数 の用意


AutoScalingでインスタンスが起動したときに実行する lambda関数を作成します。
ソースコードは以下のとおり。
paramiko をLambdaで使用する方法は、こちらの記事を参照。
import boto3
import paramiko

def scaleout_handler(event, context):

    # instance-id -> private ip address
    print "get ip"
    ec2 = boto3.resource('ec2')
    instance = ec2.Instance(event['detail']['EC2InstanceId'])
    ip = instance.private_ip_address
    print "ip=" + ip

    # Download private key file from S3 bucket
    print "get key from s3"
    s3 = boto3.client('s3')
    s3.download_file('blue21.dev.local','ssh_key.pem', '/tmp/ssh_key.pem')

    # ssh connect
    print "connecting ssh"
    k = paramiko.RSAKey.from_private_key_file("/tmp/ssh_key.pem")
    c = paramiko.SSHClient()
    c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    c.connect( hostname = ip, username = "centos", pkey = k )
    print "connected"

    # execute command
    commands = [
        "ps -ef"
    ]
    for command in commands:
        print "Executing {}".format(command)
        stdin , stdout, stderr = c.exec_command(command)
        print stdout.read()

    # lifecycle continue
    print "lifecycle continue"
    as_client = boto3.client('autoscaling')
    res = as_client.complete_lifecycle_action(
        LifecycleHookName=event['detail']['LifecycleHookName'],
        AutoScalingGroupName=event['detail']['AutoScalingGroupName'],
        LifecycleActionToken=event['detail']['LifecycleActionToken'],
        LifecycleActionResult='CONTINUE'
    )

    return {
        'message' : "scaleout function finished"
    }
この Lambda関数では以下の処理を実行します。
(1) イベント情報のインスタンスIDからプライベートIPを取得
(2) S3バケットからSSH秘密鍵(ssh_key.pem)をダウンロード
(3) プライベートIPのサーバにsshログイン
(4) ps -ef コマンドを実行
(5) lifecycle-hook で待機状態になったインスタンス状態を続行する。

このLambda関数が受信する LifecycleHook のイベント情報は以下のとおり。
{
  "account": "xxxxx",
  "region": "us-east-1",
  "detail": {
    "EC2InstanceId": "i-03e42db2057f658b0",
    "AutoScalingGroupName": "centos7_scale_test",
    "LifecycleActionToken": "d9436277-fcbc-4a41-b197-fe4c044aeb7f",
    "LifecycleHookName": "scaleout_fook",
    "NotificationMetadata": "a,b,c",
    "LifecycleTransition": "autoscaling:EC2_INSTANCE_LAUNCHING"
  },
  "detail-type": "EC2 Instance-launch Lifecycle Action",
  "source": "aws.autoscaling",
  "version": "0",
  "time": "2018-02-11T03:47:27Z",
  "id": "5b94f10c-5b57-d22f-96d1-99e914f3ba0b",
  "resources": [
    "arn:aws:autoscaling:us-east-1:xxxxx:autoScalingGroup:1179ba6e-dce9-465e-ab4f-45c02ccab751:autoScalingGroupName/centos7_scale_test"
  ]
}

AWSへのデプロイに使用する template.yaml は以下のとおり。
関数名は、ScaleoutFunction にしました。
AWSにデプロイする方法は、こちらの記事を参照。
AWSTemplateFormatVersion: '2010-09-09'
Description: scaleout lifecycle hook
Transform: AWS::Serverless-2016-10-31
Resources:
  ScaleoutFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      FunctionName: ScaleoutFunction
      Handler: scaleout_function.scaleout_handler
      MemorySize: 128
      Role: arn:aws:iam::xxxxx:role/Lambda01_Role
      Runtime: python2.7
      Timeout: 10
      VpcConfig:
        SubnetIds:
          - subnet-2ea4d166
        SecurityGroupIds:
          - sg-bbf176df

このLambda関数は、プライベートサブネットで実行しますが、インスタンスIDからプライベートIPを取得するのにインターネットとの通信が必要なので、NATゲートウェイを使用します。
また、S3からファイルをダウンロードするので VPCエンドポイントも使用します。
Lambda関数が使用するサブネットのルーティングテーブルは下図のようになります。


2.起動設定の用意


起動設定を作成します。
名称は、"launch-centos7-02" にしました。
AMIは CentOS7 を使用します。
プライベートIPのみ使用して、パブリックIPは設定しません。


3.AutoScalingグループの用意


AutoScalingグループを作成します。
名称は、"as-group-01" にしました。
[ターゲットグループ]の[希望],[最小]は 0 にし、[最大]は 1 にしてます。
あとで、動作確認するときに、[希望],[最小]を 1 にします。
[サブネット]は、Lambdaと同じプライベートサブネットを使用します。


3.ライフサイクルフックの設定


上記で用意した AutoScalingグループにライフサイクルフックを定義します。
名称は、"hook-scaleout" にしました。
[ライフサイクルフックタイプ]は、”インスタンスの作成”を選択して、インスタンス作成時にフックされるようにします。



4.CloudWatchEvent の設定


ライフサイクルフック発動時に、Lambda関数を実行するイベントのルールを定義します。
[イベントソース]
 サービス名 ⇒ AutoScaling
 イベントタイプ ⇒ Instance Launch and Terminate
 特定のインスタンスイベント ⇒ EC2 Instance-launch Lifecycle Action
 特定のグループ名 ⇒ as-group-01
[ターゲット]
 Lambda関数
 ScaleoutFunction


[ルールの定義]
 名前 ⇒ rule-scaleout
 状態 ⇒ 有効


5.動作確認


AutoScaling でスケールアウトして、ライフサイクルフックの動きを見てみます。

S3バケットにSSH秘密鍵を置いておきます。
[root@centos702 lambda4]# aws s3 cp /root/ssh_key.pem s3://blue21.dev.local/
upload: ../ssh_key.pem to s3://blue21.dev.local/ssh_key.pem

ターゲットグループの[希望][最小]に1を設定して、スケールアウトします。


しばらくするとEC2インスタンスが起動します。


ライフサイクルフックでEC2インスタンスが待機状態になっています。
このとき Lambda関数が実行されます。


Lambda関数が実行され、ライフサイクルの続行を指示すると、"保留:待機" から"実行中"になります。


[アクティビティ履歴]を見ると、"成功" になっています。


Lambda関数のログは以下のとおり。
"ps -ef" コマンドの実行結果が記録されています。

[root@centos702 lambda4]# aws logs get-log-events --log-group-name /aws/lambda/ScaleoutFunction --log-stream-name '2018/02/12/[$LATEST]45c4bec38f264a46bad65ccfebe8f2ce' --output=text --query "events[*].message"
START RequestId: 637efafc-0f95-11e8-a1ec-fd427653aa45 Version: $LATEST
        get ip
        ip=10.0.20.20
        get key from s3
        connecting ssh
        connected
        Executing ps -ef
        UID        PID  PPID  C STIME TTY          TIME CMD
        root         1     0  9 01:38 ?        00:00:02 /usr/lib/systemd/systemd --switched-root --system --deserialize 20
        root         2     0  0 01:38 ?        00:00:00 [kthreadd]
        root         3     2  0 01:38 ?        00:00:00 [ksoftirqd/0]
        root         4     2  0 01:38 ?        00:00:00 [kworker/0:0]
        root         5     2  0 01:38 ?        00:00:00 [kworker/0:0H]
        root         6     2  0 01:38 ?        00:00:00 [kworker/u30:0]
        root         7     2  0 01:38 ?        00:00:00 [migration/0]
        root         8     2  0 01:38 ?        00:00:00 [rcu_bh]
        root         9     2  0 01:38 ?        00:00:00 [rcu_sched]
        root        10     2  0 01:38 ?        00:00:00 [watchdog/0]
        root        12     2  0 01:38 ?        00:00:00 [kdevtmpfs]
        root        13     2  0 01:38 ?        00:00:00 [netns]
        root        14     2  0 01:38 ?        00:00:00 [xenwatch]
        root        15     2  0 01:38 ?        00:00:00 [xenbus]
        root        16     2  0 01:38 ?        00:00:00 [kworker/0:1]
        root        17     2  0 01:38 ?        00:00:00 [khungtaskd]
        root        18     2  0 01:38 ?        00:00:00 [writeback]
        root        19     2  0 01:38 ?        00:00:00 [kintegrityd]
        root        20     2  0 01:38 ?        00:00:00 [bioset]
        root        21     2  0 01:38 ?        00:00:00 [kblockd]
        root        22     2  0 01:38 ?        00:00:00 [md]
        root        27     2  0 01:38 ?        00:00:00 [kswapd0]
        root        28     2  0 01:38 ?        00:00:00 [ksmd]
        root        29     2  0 01:38 ?        00:00:00 [crypto]
        root        37     2  0 01:38 ?        00:00:00 [kthrotld]
        root        38     2  0 01:38 ?        00:00:00 [kworker/u30:1]
        root        39     2  0 01:38 ?        00:00:00 [kmpath_rdacd]
        root        40     2  0 01:38 ?        00:00:00 [kpsmoused]
        root        41     2  0 01:38 ?        00:00:00 [kworker/0:2]
        root        42     2  0 01:38 ?        00:00:00 [ipv6_addrconf]
        root        61     2  0 01:38 ?        00:00:00 [deferwq]
        root       116     2  0 01:38 ?        00:00:00 [kauditd]
        root       124     2  0 01:38 ?        00:00:00 [kworker/0:3]
        root       179     2  0 01:38 ?        00:00:00 [rpciod]
        root       180     2  0 01:38 ?        00:00:00 [xprtiod]
        root       246     2  0 01:38 ?        00:00:00 [ata_sff]
        root       248     2  0 01:38 ?        00:00:00 [scsi_eh_0]
        root       250     2  0 01:38 ?        00:00:00 [scsi_tmf_0]
        root       251     2  0 01:38 ?        00:00:00 [scsi_eh_1]
        root       253     2  0 01:38 ?        00:00:00 [scsi_tmf_1]
        root       255     2  0 01:38 ?        00:00:00 [kworker/u30:2]
        root       256     2  0 01:38 ?        00:00:00 [kworker/u30:3]
        root       268     2  0 01:38 ?        00:00:00 [bioset]
        root       269     2  0 01:38 ?        00:00:00 [xfsalloc]
        root       270     2  0 01:38 ?        00:00:00 [xfs_mru_cache]
        root       271     2  0 01:38 ?        00:00:00 [xfs-buf/xvda1]
        root       272     2  0 01:38 ?        00:00:00 [xfs-data/xvda1]
        root       273     2  0 01:38 ?        00:00:00 [xfs-conv/xvda1]
        root       274     2  0 01:38 ?        00:00:00 [xfs-cil/xvda1]
        root       275     2  0 01:38 ?        00:00:00 [xfs-reclaim/xvd]
        root       276     2  0 01:38 ?        00:00:00 [xfs-log/xvda1]
        root       277     2  0 01:38 ?        00:00:00 [xfs-eofblocks/x]
        root       278     2  0 01:38 ?        00:00:00 [xfsaild/xvda1]
        root       355     1  0 01:38 ?        00:00:00 /usr/lib/systemd/systemd-journald
        root       389     1  0 01:38 ?        00:00:00 /usr/lib/systemd/systemd-udevd
        root       436     1  0 01:38 ?        00:00:00 /sbin/auditd
        root       494     2  0 01:38 ?        00:00:00 [ttm_swap]
        root       516     2  0 01:38 ?        00:00:00 [edac-poller]
        root       551     1  0 01:38 ?        00:00:00 /usr/sbin/rsyslogd -n
        polkitd    553     1  0 01:38 ?        00:00:00 /usr/lib/polkit-1/polkitd --no-debug
        root       555     1  0 01:38 ?        00:00:00 /usr/lib/systemd/systemd-logind
        dbus       559     1  0 01:38 ?        00:00:00 /bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
        chrony     563     1  0 01:38 ?        00:00:00 /usr/sbin/chronyd
        root       585     1  0 01:38 ?        00:00:00 /usr/sbin/gssproxy -D
        root       775     1  0 01:38 ?        00:00:00 /sbin/dhclient -1 -q -lf /var/lib/dhclient/dhclient--eth0.lease -pf /var/run/dhclient-eth0.pid eth0
        root       840     1  0 01:38 ?        00:00:00 /usr/bin/python -Es /usr/sbin/tuned -l -P
        root       854     2  0 01:38 ?        00:00:00 [kworker/0:1H]
        root       963     1  0 01:38 ?        00:00:00 /usr/libexec/postfix/master -w
        postfix    964   963  0 01:38 ?        00:00:00 pickup -l -t unix -u
        postfix    965   963  0 01:38 ?        00:00:00 qmgr -l -t unix -u
        root       996     1  0 01:38 ?        00:00:00 /usr/lib/systemd/systemd-hostnamed
        root      1017     1  2 01:38 ?        00:00:00 /usr/sbin/crond -n
        root      1018     1  0 01:38 tty1     00:00:00 /sbin/agetty --noclear tty1 linux
        root      1021     1  0 01:38 ttyS0    00:00:00 /sbin/agetty --keep-baud 115200 38400 9600 ttyS0 vt220
        root      1063     1  0 01:38 ?        00:00:00 /usr/sbin/sshd -D
        root      1084  1063 17 01:38 ?        00:00:00 sshd: centos [priv]
        centos    1087  1084  0 01:38 ?        00:00:00 sshd: centos@notty
        centos    1088  1087  0 01:38 ?        00:00:00 ps -ef

lifecycle continue
        END RequestId: 637efafc-0f95-11e8-a1ec-fd427653aa45
        REPORT RequestId: 637efafc-0f95-11e8-a1ec-fd427653aa45  Duration: 6824.96 ms    Billed Duration: 6900 ms        Memory Size: 128 MB       Max Memory Used: 90 MB

[root@centos702 lambda4]#

テスト中、ライフサイクルフックで、"保留:待機" 状態になったEC2インスタンスを Lambda関数で続行できなかった場合は、 aws-cli で続行できます。
以下のように、AutoScalingグループ名、ライフサイクルフック名、インスタンスIDを指定してコマンドを実行します。
[root@centos702 ~]# aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id i-0d11d4ff1f236cc75 --lifecycle-hook-name hook-scaleout --auto-scaling-group-name as-group-01