Systems ManagerやRun Commandを使ってオンプレのマシンをAWSから操る

#今回やったこと
オンプレのマシン上で、AWSの各種のサービスと連携させて各種のスクリプトが動くようにした。
今回は[EC2 Systems Manager](https://aws.amazon.com/jp/ec2/systems-manager/)と[EC2 Run Command](https://aws.amazon.com/jp/ec2/systems-manager/run-command/)を（どうしても）触りたかったのでそのあたりの使い方をまとめてみる。
それぞれがどのようなものかは、リンク先の公式ページをご覧になった方がよく理解できるかと思うので割愛させていただく。

#マネージドインスタンスに登録する
 オンプレ上のマシンに対してRun Commandを走らせたりするにはEC2 Systems Managerのマネージドインスタンスに登録する必要がある。登録の手順は[公式のセットアップ](http://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-setting-up.html)配下。今回動かしたかったのはWindows環境（Win10）のオンプレマシン（というかローカルPC）なので、[ハイブリッド環境での Systems Manager のセットアップ](http://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-managedinstances.html)を参考に実施した。大体の手順は次の通り。

1. IAM サービスおよびユーザーロールを作成
2. Systems Manager の前提条件を確認
3. マネージドインスタンスのアクティベーションを作成する
4. SSM エージェントのデプロイ

1．は個人の環境によって色々手順があり、アカウント毎に一回作っておけばいいので今回は割愛。2．はそもそもの条件なのでこれも割愛。なので、実質やるのは3．と4．になる。

### マネージドインスタンスのアクティベーションを作成する
今回はWindowsなので、AWS Tools for Windows PowerShellを使ってみる。
```
New-SSMActivation -DefaultInstanceName name -IamRole IAM service role -RegistrationLimit number of managed instances –Region region
```
Enter キーを押して、アクティベーションが正常に完了したら、システムからアクティベーションコードと ID が返される。これを使って次のステップで登録する。ちなみにこれらはAWSコンソールのEC2、**SYSTEMS MANAGER 共有リソース**にある**アクティベーション**からも発行可能だ。

### SSMエージェントのデプロイ
公式ドキュメントの[ハイブリッド環境での Systems Manager のセットアップ](http://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-managedinstances.html) にある **Windows ハイブリッド環境のサーバーおよび VM に SSM エージェントをインストールする**の項目う。これが終われば登録され、AWS コンソールのマネージドインスタンスの一覧にマシンが現れるようになる。
以下をpowershellで実行。

```
$dir = $env:TEMP + "\ssm"
New-Item -ItemType directory -Path $dir
cd $dir
(New-Object System.Net.WebClient).DownloadFile("https://amazon-ssm-region.s3.amazonaws.com/latest/windows_amd64/AmazonSSMAgentSetup.exe", $dir + "\AmazonSSMAgentSetup.exe")
Start-Process .\AmazonSSMAgentSetup.exe -ArgumentList @("/q", "/log", "install.log", "CODE=code", "ID=id", "REGION=region") -Wait
Get-Content ($env:ProgramData + "\Amazon\SSM\InstanceData\registration")
Get-Service -Name "AmazonSSMAgent"
```

自分の環境だとなぜかDownloadのところで失敗した。PowerShell詳しくないマンなので、[Windowsでwgetする色々な方法](http://qiita.com/nightyknite/items/b4db8766c0b94764cd3c)を参考にwget for windowsを利用して該当部分のファイルを無理やり突っ込んだら動いた。セーフ。
ここまでで、マネージドインスタンスの一覧にマシンが表示され、操る準備が整った。手軽だ。

#LambdaでRun Commandを実行する

これまでの操作で、AWS コンソールから直接マシンにPowerShellスクリプトを走らせることができるようになった。しかし、CloudWatch EventsやらSNSやらと連動させようとするとLambdaから実行できると都合がよい。
この辺りは[カスタムメトリクスをLambdaとCloudWatch Eventsを使って取得する](http://qiita.com/d-yamanaka/items/9ecac06901c739a40872)が非常に参考になる。node.jsを使いたかったのでそこもマッチした。

## SNSと連携させてみる

SNSからメッセージが届いたら[このあいだ触ってみたPolly](http://qiita.com/OMOIKANESAN/items/51dcfe3672aa08f298ab)で、件名を音声にさせるスクリプトをRun Commandで実行するLambdaを作ってみた。何やらややこしい上、RunCommandでやる必要はないのだが……


### Lambda側のソース
```js


'use strict';

console.log('Loading function');

const AWS = require('aws-sdk');
const ssm = new AWS.SSM();

exports.handler = (event, context, callback) => {
    /* SNSから送られた送られたメッセージと件名を取得 */
    const message = event.Records[0].Sns.Message; /* 今回こっちは使わない */
    const subject = event.Records[0].Sns.Subject;
    /* 確認用 */
    callback(null, message);
    
    /* 全体の大まかな構成は引用元参考 */
    const generator  = (function *() {

        try {
            /* ここでコマンドをここでコマンドを仕込む。下記のように複数指定可能 */
            const commands = [
                'node C:\\RUN_COMMAND\\get_announce_and_play.js ' + subject,
                'dir C:\\RUN_COMMAND'
            ];

            yield sendCommand(['mi-ここにインスタンスID'], commands, generator);

            callback(null, 'success');

        } catch (e) {
            callback(e.message);
        }
    })();

    /* 処理開始 */
    generator.next();
};

/* 処理の中身 */
function sendCommand(instanceIds, commands, generator) {

    const params = {
        DocumentName: 'AWS-RunPowerShellScript', /* 必須。今回はWindowsなのでPowerShellScriptを指定 */
        InstanceIds: instanceIds,
        Parameters: {
            commands: commands,
            executionTimeout: [
                '300'
            ]
        },
        TimeoutSeconds: 600
    };

    ssm.sendCommand(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            generator.throw(new Error('send command error : ' + params.InstanceIds));
            return;
       }
       console.log('successful send command : ' + params.InstanceIds);
       generator.next();
    });
}

```

sendCommandで使うparameterについてもっと知りたい場合は[Class: AWS.SSM sendCommand](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SSM.html#sendCommand-property)に書いてある。

### オンプレマシン側のソース

```js

'use strict';

var aws = require('aws-sdk');
const fs = require('fs');

var credentials = new aws.SharedIniFileCredentials({profile: 'オンプレマシン上のCredentialが入ったプロフィールを指定'});
aws.config.credentials = credentials;
let polly = new aws.Polly({region:'us-west-2'}); /* 書いた時点で使えるリージョンがここだった */
let voiceString = process.argv[2]; /* 送られてきた文字列を変換する */

// describeVoices
let descParams = {
    LanguageCode: 'ja-JP'
};

polly.describeVoices(descParams, (err, data)=> {
    if (err) {
        console.log(err);
    } else {
        let voiceId = data.Voices[0].Id;

        let textMsg = voiceString;
        let speechParams = {
            OutputFormat: 'mp3',
            VoiceId: voiceId,
            Text: textMsg,
            SampleRate: '22050',
            TextType: 'text'
        };

        // synthesizeSpeech
        polly.synthesizeSpeech(speechParams, (err, data) => {
            if (err) {
                console.log(err);
            } else {
                console.log(data);
                /*出来上がったらとりあえず保存しておく*/
                fs.writeFile('C:/RUN_COMMAND/polly.mp3', data.AudioStream, (err) => {
                    if (err) {
                        console.log(err);
                    } else {
                        console.log('Success');
                    }
                });
            }
        });
    }
});
```

とりあえず、上記のLambdaをSNSに適当に作ったTopicsと連携させて、テストメッセージを送ったところ ~~無事に音声が変換された~~ 。と思いきや、日本語だと文字コードの問題で、SSM⇒マシン間で日本語が文字化けする。何語か分からない。Alarmとかのメッセージは大凡英語で問題ないので今回はこれまでにすることに。

#感想
Lambdaで実行するPowerShellコマンドを変更すれば、いろんなことに使える気がする。Alarm飛んだらサービス再起動するとか、メッセージ送ったらオンプレのインスタンスでバッチを走らせるとか。