AWS環境でAPCUを利用しDataTransfer料金を下げる

# 前提
EC2 + RDS + ElastiCacheを利用した一般的なサービス環境
webサーバはLaravel on Dockerで、オートスケールを利用し動的に台数が変化する

# 目的
マルチAZ構成にしている場合、EC2(AZ-A)とRDS/ElastiCache(AZ-C)の別AZ間の通信には同一VPCであっても通信料金がかかる
そのためAPCUを利用し、アクセス頻度が高いデータはローカルキャッシュから読み込むことで通信費用を抑える

# 問題点
・APCUはプロセス間でキャッシュデータを共有しないので、php-fpm(nginxからのリクエスト処理)経由で作られたデータとartisanコマンドで操作可能なデータは異なる
・あくまでローカルキャッシュなのでキャッシュクリアを行いたい場合は全webサーバに対してクリア処理を行う必要がある

# 解決法
・nginxコンテナからlocalhost向けでリクエストを送り、php-fpmプロセスに対して処理を行う
・AWSSystemManagerのRunCommand機能を利用し、タグ指定でオートスケールインスタンス全台に対してクリア処理を行う

# 構成図
![APCU操作フロー図-ページ1のコピー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298813/aad90dee-f640-de93-95ee-02ac6868013e.jpeg)

# SSM実行に関連する情報
https://qiita.com/chanP_yamazaki/items/d602c30044a6991c31e1

# 実行元サーバ情報
#### php処理
SDKを利用
https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-ssm-2014-11-06.html#sendcommand
```
AWS::createClient('ssm')->sendCommand([
    'Targets' => [
        [
            'Key' => 'tag:aws:autoscaling:groupName',
            'Values' => ['hogehoge']
        ]
    ],
    'Parameters' => [
        'commands' => [
            "docker exec -t nginxコンテナ curl 'localhost/apcu.php'",
        ],
    ],
    'DocumentName' => 'AWS-RunShellScript',
    'ServiceRoleArn' => 'arn:aws:iam::123456789:role/ssm-runcommand-service-role',
    'NotificationConfig' => [
        'NotificationArn'    => 'arn:aws:sns:ap-northeast-1:123456789:ssm-runcommand-topic',
        'NotificationEvents' => ['All'],
        'NotificationType'   => 'Command',
    ],
]);
```

#### インスタンスロール
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ssm:SendCommand",
                "iam:PassRole",
                "sns:Publish"
            ],
            "Resource": "*"
        }
    ]
}
```

# webサーバ
#### インスタンスロール
・AmazonEC2RoleforSSM

# 進捗通知Lambda
SSMエージェントが起動されてなかったり、権限の問題でタグ検索がうまくいかなかったりで、ターゲット数が0の場合でもRunCommandは成功扱いになってしまうので、一度Lambdaを通して成否チェックを行いslackに結果を通知する
```
import boto3
import json

def post_slack(message):
    send_data = {
        "text": message,
    }
    send_text = json.dumps(send_data)
    request = urllib.request.Request(
        'https://hooks.slack.com/HOGEHOGE',
        data = send_text.encode('utf-8'), 
        method = "POST"
    )
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')

def lambda_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message']);

    status = message['status'];
    if status == 'InProgress':
        # 実行中は何もしない
        return;
    
    # 実行台数0台でも成功になってしまうので細かく精査
    result = boto3.client('ssm').list_commands(
        CommandId=message['commandId']
    );
    targetCount = result['Commands'][0]['TargetCount'];
    targetParam = result['Commands'][0]['Targets'][0];
    sendMessage = '<!here> \nkey: ' + targetParam['Key'] + '\nvalue: ' + ', '.join(map(str, targetParam['Values']));
    if status == 'Success':
        if targetParam['Key'] == 'tag:aws:autoscaling:groupName':
            asData = boto3.client('autoscaling').describe_auto_scaling_groups(
                AutoScalingGroupNames=[
                    targetParam['Values'][0]
                ]
            );
            if targetCount == asData['AutoScalingGroups'][0]['DesiredCapacity']:
                sendMessage += '\nSSMのRunCommand実行に成功しました';
            else:
                sendMessage += '\nSSMのRunCommand実行台数とAS希望台数が合致しません、指定が正しいか確認してください';
        else:
            if targetCount == 0:
                sendMessage += '\nSSMのRunCommand実行の対象インスタンスが0台です、指定が正しいか確認してください';
            else:
                sendMessage += '\nSSMのRunCommand実行に成功しました';
    else:
        sendMessage += '\nSSMのRunCommand実行に失敗しました';

    post_slack(sendMessage);
```

#### 実行ロール
・AutoScalingReadOnlyAccess
・AmazonSSMReadOnlyAccess