Help us understand the problem. What is going on with this article?

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

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

Introduction

前回は、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直下だと、通常ありそうな場所では無いため、分かりにくかったからです。

/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ユーザのアクセスキーとシークレットは別ファイルで定義することにする)。

/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)。

/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接続用のパラメータファイルです。

/credentials/ssm
[default]
aws_access_key_id = AXXXXXXXXXXXXXXXXXXX
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

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

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

/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関連処理を追加したものです。

/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ディレクトリ処理を追加。

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

ここまでのソース

こちらでどうぞ。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした