PHP Slim 環境構築(13) AWSサービスの利用

# PHP Slim 環境構築(13) AWSサービスの利用


## Introduction

[前回](https://qiita.com/dynamitecoolguy/items/7e6f273913c8c11195ef)は、DockerコンテナにNginxとPHP-FPMの両方を格納した構成をEC2に構築し、同一サーバ上で動く別のDockerコンテナ(RDB, DynamoDB, S3)とアクセスを行いました。
今回は、RDB、DynamoDB、S3などをAWS上のサービスを利用するように変更します。

この一連のシリーズは、自分への備忘録が第一目的のため、だいぶ不親切です。
すみません・・・


## AWS


### SystemManager

DB接続情報などを格納するのにSystemManagerのパラメータストアを使用するようにします。
SecretsManagerによるシークレットローテーションなども試したいのですが、有償になってしまうため今回は断念しています(無償期間は過ぎた)。

今回使用したパラメータは以下の通りです。

|キー名|意味|
|:--|:--|
|DYNAMODB_ACCESS_KEY|DynamoDBにアクセスするIAMユーザーのAccessKey|
|DYNAMODB_SECRET|DynamoDBにアクセスするIAMユーザーのSecret|
|LOG_DB_PASSWORD|ログ用DBに想定しているRedshiftへの接続パスワード|
|LOG_DB_USER|ログ用DBに想定しているRedshiftへの接続ユーザ|
|STORAGE_ACCESS_KEY|S3にアクセスするIAMユーザのAccessKey|
|STORAGE_BUCKET|アクセスするS3のバケット名|
|STORAGE_SECRET|S3にアクセスするIAMユーザのSecret|
|USER_DB_PASSWORD|RDBへの接続パスワード|
|USER_DB_USER|RDBへの接続ユーザ|

### RDS(MariaDB)

今回は、テスト用に使用するRDBとして、RDS for MariaDBを使用しました。
Auroraを使用しないのは、もちろん無償だからです。
Dockerコンテナを起動しているEC2からアクセスできるように、セキュリティグループを設定する必要があります。
また、このDBへのアクセスユーザとパスワードをSSMに登録します(USER_DB_USER, USER_DB_PASSWORD)。

### RDS(PostgreSQL)

今回は、ログ用DBとしてRedshiftを想定していますが、テスト用にはRDS for PostgreSQLを使用しました。
理由は、もちろん無償・・・。
セキュリティグループに注意してください。
くた、このDBへのアクセスユーザとパスワードをSSMに登録します (LOG_DB_USER, LOG_DB_PASSWORD)。

### DynamoDB

DynamoDBとDynamoDBにアクセスすることができる(AmazonDynamoDBFullAccess)IAMユーザを作成しました。
このユーザのアクセスキーとシークレットを、SSMに登録します (DYNAMODB_ACCESS_KEY, DYNAMODB_SECRET)。

### ElastiCache

ElastiCache(Redis)を作成しました。


### S3

S3にテストに用いるバケットを作成し、そのバケットにアクセスできるIAMユーザを作成しました。
このユーザのアクセスキーとシークレット、および、作成したバケット名をSSMに登録します (STORAGE_ACCESS_KEY, STORAGE_SECRET, STORAGE_BUCKET)。


## 変更点

前回まではローカルPCで稼働するlocal環境、単一のEC2上にall-in-oneで動作するdevelopment環境を作成しました。
今回は、一部awsを利用するdevaws環境を新設しました。次回は、EC2をECSに移行する予定ですので、今回の環境は過渡期的なものです。


### ソースツリー

今回の変更箇所です。

```
$(PROJECTROOT)
  /compose
    /web_front
      devaws.conf                (新規)
    /web_hoge
      Dockerfile
      settings-devaws.yml        (新規)
    docker-compose-devaws.yml    (新規)
  /credentials
    .empty                       (新規)
    ssm
  /src
    /hoge
      /lib
        /Controller
          DynamodbController.php
          StorageController.php
        ApplicationSetting.php   (新規)
        MyContainerBuilder.php
    composer.json
    composer.lock
  .gitigore
```

### /compose/web_front/devaws.conf　(新規)

/compose/web_front/development.confと同じです。

### /compose/web_hoge/Dockerfile

設定ファイルのコピー先を変更しました。
これは/var/www直下だと、通常ありそうな場所では無いため、分かりにくかったからです。

```dockerfile:/compose/web_hoge/Dockerfile
...(略)...
#COPY settings-${environment}.yml /var/www/settings.yml
COPY settings-${environment}.yml /usr/local/etc/myapp/settings.yml
...(略)...
```


### /compose/web_hoge/settings-devaws.yml 　(新規)

前回までの設定ファイルsettings-development.ymlでは、接続情報を直接書いていたのですが、セキュリティ上、AWSサービスのアクセスキーなどを直接書くわけにはいかないので、SSMのパラメータストアの情報を見に行く仕組みを作成し、それに書き換えています。
また、SSMへの接続情報が追加されています ($$で囲まれた部分。ただし、SSMへアクセスできるIAMユーザのアクセスキーとシークレットは別ファイルで定義することにする)。

```yml:/compose/web_hoge/settings-devaws.yml
userdb:
  host: mydbinstance.csvkmjl9r9hs.ap-northeast-1.rds.amazonaws.com
  dbname: userdb
  user: $$USER_DB_USER$$
  password: $$USER_DB_PASSWORD$$

logdb:
  host: logdbinstance.csvkmjl9r9hs.ap-northeast-1.rds.amazonaws.com
  dbname: logdb
  user: $$LOG_DB_USER$$
  password: $$LOG_DB_PASSWORD$$

redis:
  host: myredis.g3vevh.ng.0001.apne1.cache.amazonaws.com

dynamodb:
  endpoint: https://dynamodb.ap-northeast-1.amazonaws.com
  region: ap-northeast-1
  key: $$DYNAMODB_ACCESS_KEY$$
  secret: $$DYNAMODB_SECRET$$
  table: mydynamo

storage:
  endpoint: https://s3.ap-northeast-1.amazonaws.com
  region: ap-northeast-1
  key: $$STORAGE_ACCESS_KEY$$
  secret: $$STORAGE_SECRET$$
  bucket: $$STORAGE_BUCKET$$

ssm:
  endpoint: https://ssm.ap-northeast-1.amazonaws.com
  region: ap-northeast-1
```


### /compose/docker-compose-devaws.yml 　(新規)

devaws環境用、docker-composeファイル。local, development環境は、docker-compose-common + 環境用と二つに分けていましたが、devaws環境は一つです。ややこしいので、docker-compose-commonは不要だったかもしれないですね。

web_hogeのvolumesに、ssm接続設定ファイルを含むディレクトリの定義が追加されています(../credentials:/usr/local/etc/myapp/credentials)。


```yml:/compose/docker-compose/devaws.yml
version: '3'

services:
  web_front:
    build:
      context: ./web_front
      args:
        - environment=devaws
    ports:
      - "80:3128"
      - "443:8443"
    volumes:
      - ../certs:/etc/certs:ro
    container_name: web_front
    networks:
      - private_net

  web_hoge:
    build:
      context: ./web_hoge
      args:
        - environment=devaws
    volumes:
      - ../src/hoge:/var/www/hoge
      - ../src/vendor:/var/www/vendor
      - ../credentials:/usr/local/etc/myapp/credentials
    container_name: web_hoge
    networks:
      - private_net

networks:
  private_net:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.28.0.0/24
```

### /credentials/.empty

gitで空ディレクトリを管理するためのファイル。.gitkeepと同じです。

### /credentials/ssm 　(新規、GitHubリポジトリには含まれていません)

SSM接続用のパラメータファイルです。

```text:/credentials/ssm
[default]
aws_access_key_id = AXXXXXXXXXXXXXXXXXXX
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

### /src/hoge/lib/Controller/DynamodbController.php

DynamoDBの接続テーブルの設定の取得を、直接設定ファイルを参照するような形から、ApplicationSettingクラスに移譲するように変更しました。

```php:/src/hoge/lib/Controller/DynamodbController.php

class DynamodbController
{
    /**
     * @var string
     */
    private $targetTable = null;

..(略)..

    private function getTable(): string
    {
        if (is_null($this->targetTable)) {
            $setting = ApplicationSetting::getInstance();
            $this->targetTable = $setting->getSettingValue('dynamodb', 'table');
        }
        return $this->targetTable;
    }
```


### /src/hoge/lib/Controller/StorageController.php

DynamoDBControllerと同様に、設定ファイルを参照するのではなく、ApplicationSettingクラスに移譲するように変更しました。


### /src/hoge/lib/ApplicationSettings.php (新規)

設定ファイルの内容を取得し、$$で囲まれた文字列があれば、実際の値をSSMに問い合わせて値を返します。
なお、値はローカル、apcuでキャッシュされます。

ソースは、前回にはMyContainerBuilderに含まれていた部分を抽出し、ssm関連処理を追加したものです。

```php:/src/hoge/lib/ApplicationSettings.php
<?php

namespace Hoge;

use Aws\Credentials\CredentialProvider;
use Aws\Exception\AwsException;
use Aws\Sdk;

/**
 * Class ApplicationSetting
 * @package Hoge
 */
class ApplicationSetting
{
    /**
     * 設定ファイルのパス
     */
    const SETTINGS_YAML = '/usr/local/etc/myapp/settings.yml';

    /**
     * 設定ファイルのパス
     */
    const SSM_CREDENTIALS_PROFILE = 'default';
    const SSM_CREDENTIALS_PATH = '/usr/local/etc/myapp/credentials/ssm';

    /**
     * localにcacheするparameterのTTL
     */
    const PARAMETER_TTL = 60;

    /**
     * @var ApplicationSetting
     */
    private static $self = null;

    /**
     * @var array
     * 取得済みの値
     */
    private $replacedText = [];

    /**
     * @var array
     * settings.yamlの内容
     */
    private $settings = [];

    /**
     * @return ApplicationSetting インスタンスを取得
     */
    public static function getInstance(): ApplicationSetting
    {
        if (is_null(static::$self)) {
            static::$self = new static();
        }
        return static::$self;
    }

    /**
     * ApplicationSetting constructor.
     */
    protected function __construct()
    {
        $this->settings = yaml_parse_file(static::SETTINGS_YAML);
    }

    /**
     * 指定されたカテゴリ内のキーの値を取得する
     * @param string $category
     * @param string $key
     * @return mixed
     */
    public function getSettingValue(string $category, string $key)
    {
        if (!isset($this->settings[$category]) || !isset($this->settings[$category][$key])) {
            return false; // TODO: 本当はException
        }
        $value = $this->settings[$category][$key];

        if (strpos($value, '$$') === false) {
            return $value; // 置換の必要が無ければその値のまま返す
        }

        if (!preg_match('/^(.*)\$\$(.*)\$\$(.*)$/', $value, $matches)) { // {{KEY_NAME}}か?
            return $value; // 置換の必要が無ければその値のまま返す
        }

        return $matches[1] . $this->replaceValue($matches[2]) . $matches[3];
    }

    /**
     * @param string $original
     * @return mixed
     */
    private function replaceValue(string $original)
    {
        // このリクエストで置換済みの値か?
        if (isset($this->replacedText[$original])) {
            return $this->replacedText[$original];
        }

        // apcuに保存されている値か?
        $fetchedValue = apcu_fetch($original);
        if ($fetchedValue) {
            $this->replacedText[$original] = $fetchedValue;
            return $fetchedValue;
        }

        // SSMサービスへの認証
        $provider = CredentialProvider::ini(
            static::SSM_CREDENTIALS_PROFILE,
            static::SSM_CREDENTIALS_PATH
        );
        $memoizedProvider = CredentialProvider::memoize($provider);

        // SSM Clientの取得
        $ssmSetting = $this->settings['ssm'];
        $sdk = new Sdk([
            'endpoint' => $ssmSetting['endpoint'],
            'region' => $ssmSetting['region'],
            'version' => '2014-11-06',
            'credentials' => $memoizedProvider
        ]);
        $ssm = $sdk->createSsm();

        // settingsファイル内のすべての$$キーをリストアップ
        $keys = [];
        foreach ($this->settings as $settingArray) {
            foreach ($settingArray as $key => $value) {
                if (preg_match('/\$\$(.*)\$\$/', $value, $matches)) { // {{KEY_NAME}}か?
                    $keys[] = $matches[1];
                }
            }
        }

        // SystemManagerのパラメータストアから値を一斉に取得
        try {
            $result = $ssm->getParameters([
                'Names' => $keys
            ]);
        } catch (AwsException $e) {
            return false;  // TODO: 本当はException
        }

        foreach ($result['Parameters'] as $parameter) {
            $name = $parameter['Name'];
            $value = $parameter['Value'];
            if (!apcu_exists($name)) {
                apcu_store($name, $value, static::PARAMETER_TTL);
            }
            $this->replacedText[$name] = $value;
        }

        if (!isset($this->replacedText[$original])) {
            return false; // TODO: 本当はException
        }
        return $this->replacedText[$original];
    }
}
```

### /src/hoge/lib/MyContainerBuilder.php

前回のソースから、ApplicationSettingで処理しているYamlファイル関連処理などを削除したものです。

### /src/composer.json, composer.lock

composer require ext-apcuにより、APCuモジュールを有効にしました。

```composer.json
{

    .. (略)..

    "require": {
        "ext-apcu": "^5.1"

    .. (略)..

}
```

### /.gitignore

credentialsディレクトリ処理を追加。

```txt:.gitignore
# Created by .ignore support plugin (hsz.mobi)
/.idea/
*.iml
/credentials/*
!/credentials/.empty
```


## ここまでのソース

[こちら](https://github.com/dynamitecoolguy/slimtemplate/tree/chapter13)でどうぞ。