RDSとS3でファイルのやり取りを行う

データベースサーバ上にファイルを置いて、PL/SQLのUTL_FILE経由で読み書きするような処理があった場合、RDSに移行しようとすると、データベースサーバにファイルが置けないなあ・・・などという場合に、S3を間に置く方法があります。

前提

EC2 <-file-> S3 <-file-> RDS上のファイル
というやり取りについて記載しています。
また、以下の情報は2017年8月時点のものです。

Oracle on Amazon RDSでの制限

前提として、Oracle on RDSでできることを整理しましょう。S3にアクセスするにはUTL_HTTPパッケージが必要です。ユーザーガイドの「utl_http、utl_tcp、utl_smtp の使用」にサポートされる旨が記載されています。
次に、UTL_FILEによるアクセスです。UTL_FILEを用いるにはディレクトリオブジェクトを扱える必要があります。こちらについてもユーザーガイドの「主要データストレージ領域で新しいディレクトリを作成する」にて、rdsadmin.rdsadmin_util.create_directoryプロシージャを使用して可能なことが記載されています。

使用するライブラリ

UTL_HTTPを使ったS3へのアクセスを全て自分で書くのは大変なので、alexandria-plsql-utilsのAMAZON_AWS_S3_PKGを用いることにします。

準備手順

EC2, S3のバケット、RDSをすべて新規で構成する手順を見ていきましょう。順序としては以下のようになります。
1. RDSを作成する。
2. S3にバケットを作成する。
3. バケットに含まれるオブジェクトへのアクセス権を持つポリシーを作成する。
4. 3で作成したポリシーを持つロールを付与したEC2を作成する。
5. 3で作成したポリシーを持つユーザを作成する。
6. RDSにディレクトリを作成し、S3へのアクセスに必要な権限と5で作成したユーザのアクセス情報を設定する。
7. EC2とS3のやり取りを行ってみる。
8. RDSとS3のやり取りを行ってみる。

1. RDSの作成

まず、いきなりRDSを作成するのではなく、先にRDSのメニューから「オプショングループ」を選択し、「apex」という名前でAPEX及びAPEX-DEVを含むオプショングループを作成して下さい。これは、AMAZON_AWS_S3_PKGが内部でデコード関連でAPEXのライブラリを使用しているためです(APEXを実際に起動する必要はありません)。以下のようになります。
スクリーンショット 2017-08-19 17.32.40.png

apexオプショングループを用いてRDSを作成して下さい。指定箇所はパラメータグループの下にあります。次のようになります。
スクリーンショット 2017-08-19 17.34.53.png
あとは通常のRDSの作成と同様です。RDSの作成については以下を参照して下さい。
RDSユーザーガイド-Oracle DB インスタンスを作成して Oracle DB インスタンス上のデータベースに接続する

2. S3バケットの作成

特に特筆すべきことはありません。グローバルで一意になる名前でS3にバケットを作成しましょう。
S3入門ガイド-バケットの作成

3. ポリシーの作成

IAMから2で作成したバケットに含まれるオブジェクトへのアクセス権限を持つポリシーを作成します。
IAM -> ポリシーで「ポリシーの作成」を押したら、「独自のポリシーの作成」を選びましょう。
スクリーンショット 2017-08-19 17.46.37.png
ポリシー名には「allow-rds-s3-policy」などとし、ポリシードキュメントには以下のJSONを記述します。

S3-rds-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::<手順2で作成したS3バケットの名前>"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::<手順2で作成したS3バケットの名前>/*"
            ]
        }
    ]
}

このポリシーは2で作成したS3バケット、及びオブジェクトに対する権限を付与します。これをEC2、及びRDSのPL/SQLアクセス時に有効にすれば、EC2 <-> S3 <-> RDS上のPL/SQLでファイルをやり取りできます。

4. EC2インスタンスの作成

3の手順で作成したポリシーを付与したEC2用のIAMロールを作成します。IAMサービスから
ロール -> 新しいロールの作成 -> EC2ロールタイプ と選択し、
スクリーンショット 2017-08-20 0.33.30.png

3の手順で作成したポリシーを付与して
スクリーンショット 2017-08-20 0.34.37.png

名前をつければ完了です。
スクリーンショット 2017-08-20 0.37.17.png

あとはこのロールを指定してEC2インスタンスを作成します。
スクリーンショット 2017-08-20 0.39.29.png
1の手順で作成したRDSに1521ポートで接続可能なサブネットに作成して下さい。
EC2インスタンスの作成については以下も参照して下さい。
インスタンスの作成

5. PL/SQL用IAMユーザの作成

PL/SQLにIAMロールは付与できないので3の手順で作成したポリシーを付与したユーザをPL/SQL用に作成します。IAMサービスから
ユーザー -> ユーザーの追加
を選択し、ユーザー名を入力して「プログラムによるアクセス」を有効にします。
スクリーンショット 2017-08-20 0.45.29.png
アクセス権限の設定画面では、「既存のポリシーを直接アタッチ」を選択し、3の手順で作成したポリシーをチェックします。
スクリーンショット 2017-08-20 0.48.06.png
作成が完了した際に得られる「アクセスキー ID」と「シークレットアクセスキー」をPL/SQL側で使用することになります。

6. RDS上での設定

RDS上ではディレクトリの作成と、アクセス権限の設定を行います。
設定を行う前に、4の手順で作成したEC2インスタンスにログインし、必要なツールを入れます。

SQL*Plus

OTNのInstant Client Downloads for Linux x86-64から、basic及びsqlplusの2つのパッケージをブラウザ経由でダウンロードし、EC2インスタンスに転送してインストールして下さい(オラクル社のSSOログインが要求されますので、ブラウザで実施する必要があります)。
以下のように12.2のrpmをインストールした場合には、OCIライブラリやSQL*Plusのバイナリは/usr/lib/oracle/12.2/client64にインストールされています。

SQL*Plusのインストール
$ sudo rpm -i oracle-instantclient12.2-basic-12.2.0.1.0-1.x86_64.rpm
$ sudo rpm -i oracle-instantclient12.2-sqlplus-12.2.0.1.0-1.x86_64.rpm
$ ls /usr/lib/oracle/12.2/client64/bin/
adrci  genezi  sqlplus
$ ls /usr/lib/oracle/12.2/client64/lib/
glogin.sql             libmql1.so       libocijdbc12.so   libsqlplusic.so
libclntsh.so.12.1      libnnz12.so      libons.so         ojdbc8.jar
libclntshcore.so.12.1  libocci.so.12.1  liboramysql12.so  xstreams.jar
libipc1.so             libociei.so      libsqlplus.so
$

以下のように.bash_profileを設定しておきましょう。これでいつでもRDSにログインできます。

~/.bash_profile
...
ORACLIENT=/usr/lib/oracle/12.2/client64
export PATH=$PATH:$HOME/.local/bin:$HOME/bin:$ORACLIENT/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLIENT/lib

alias sql="sqlplus '<DBユーザー>@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=<DB名>.<エンドポイント>.ap-northeast-1.rds.amazonaws.com)(PORT=1521))(CONNECT_DATA=(SID=<DBのSID>)))'"

alexandria-plsql-utils

Gitでクローンし、AMAZON_AWS_S3_PKGをインストールします。

alexandria-plsql-utilsのインストール
$ sudo yum install git
...
完了しました!
$ git clone https://github.com/mortenbra/alexandria-plsql-utils.git
Cloning into 'alexandria-plsql-utils'...
remote: Counting objects: 447, done.
remote: Total 447 (delta 0), reused 0 (delta 0), pack-reused 447
Receiving objects: 100% (447/447), 382.00 KiB | 0 bytes/s, done.
Resolving deltas: 100% (184/184), done.
Checking connectivity... done.
$ cd alexandria-plsql-utils/
$ ls
README.md  alexandria-logo.jpg  demos  doc  extras  ora  setup
$ ls setup/
$ sql      #前節で設定したエイリアスでRDSへ接続
...
SQL> @install_core
...
SQL> show errors
No errors
SQL> @install_inet
...
SQL> show errors
No errors
SQL> @install_amazon
...
SQL> show errors
No errors
SQL> exit
$

ACLの設定

明示的にACLを設定しない限りUTL_HTTPによるアウトバウンドのアクセスはOracleにより全て拒否されます。次のようにDBMS_NETWORK_ACL_ADMINパッケージを用いて自ユーザから手順2で作成したs3バケットに対してのみアクセスを許可します。

create_acl.sql
declare
   l_myuser varchar(32);
begin
   select user into l_myuser from dual;
   dbms_network_acl_admin.create_acl(
     acl         => 's3',
     description => 's3 acl',
     principal   => l_myuser,
     is_grant    => true,
     privilege   => 'connect'
   );
   dbms_network_acl_admin.add_privilege(
     acl         => 's3',
     principal   => l_myuser,
     is_grant    => true,
     privilege   => 'resolve'
   );
   dbms_network_acl_admin.assign_acl(
     acl         => 's3',
     host        => '<手順2で作成したバケット名>.s3.amazonaws.com'
   );
end;
/

ディレクトリの作成

RDS側の格納先であるメインデータストレージ領域上のディレクトリを作成します。以下では2つ作成しています。テーブル名などと同じく、Oracleのデータベース・オブジェクト名となるので引用符で囲まなければ大文字となります。
データベース・オブジェクト名および修飾子

create_directory.sql
begin
  rdsadmin.rdsadmin_util.create_directory('EC2');
  rdsadmin.rdsadmin_util.create_directory('S3');
end;
/

アクセス

EC2からS3へのアクセス

テスト用ファイルとしてAWSのEC2オファーファイルを使い、EC2とS3のオファーファイルをそれぞれ異なるディレクトリに配置しておきましょう。

テスト用ファイルのダウンロード
$ mkdir ~/files
$ cd ~/files
$ mkdir ec2 s3
$ wget -O ec2/ec2-price.csv https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/index.csv
...
ec2/ec2-price.csv   100%[===================>]  92.92M  29.9MB/s    in 3.1s    
...
$ wget -O s3/s3-price.csv https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonS3/current/index.csv
...
s3/s3-price.csv     100%[===================>] 870.02K  --.-KB/s    in 0.05s   
...
$

EC2からS3へのコピー

EC2ではAWS CLIが使え、手順4でS3への権限をロールで与えているので、以下のコマンドを打てば完了です。

S3へのupload
$ cd ~/files
$ aws s3 cp ec2/ec2-price.csv s3://<手順2で作成したバケット名>/ec2/ec2-price.csv
upload: ec2/ec2-price.csv to s3://<手順2で作成したバケット名>/ec2/ec2-price.csv
$ aws s3 cp s3/s3-price.csv s3://<手順2で作成したバケット名>/s3/s3-price.csv
upload: s3/s3-price.csv to s3://<手順2で作成したバケット名>/s3/s3-price.csv
$

S3からEC2へのコピー

以下のコマンドを打てば完了です。

S3からのdownload
$ cd ~/files
$ rm -R */*.csv #ファイルを消しておきます
$ aws s3 cp s3://<手順2で作成したバケット名>/ec2/ec2-price.csv ec2/ec2-price.csv
download: s3://<手順2で作成したバケット名>/ec2/ec2-price.csv to ec2/ec2-price.csv
$ aws s3 cp s3://<手順2で作成したバケット名>/s3/s3-price.csv s3/s3-price.csv
download: s3://<手順2で作成したバケット名>/s3/s3-price.csv to s3/s3-price.csv       
$ ls -R
.:
ec2  s3

./ec2:
ec2-price.csv

./s3:
s3-price.csv
$

RDSからS3へのアクセス

AMAZON_AWS_S3_PKGはBLOBとS3オブジェクトをインタフェースします。
認証とやり取りのためのBLOBとして1つテーブルを用意しておきます。

rds_s3_config.sql
create table rds_s3_config (
  key varchar2(32) primary key,
  value varchar2(128),
  tmpblob blob
);
insert into rds_s3_config (key, value) values ('aws_id', '<手順5で得たアクセスキーID>');
insert into rds_s3_config (key, value) values ('aws_key', '<手順5で得たシークレットアクセスキー>');
insert into rds_s3_config (key, value) values ('aws_s3_bucket', '<手順2で作成したS3バケット名>');
insert into rds_s3_config (key, tmpblob) values ('temporary_blob', empty_blob());
commit;
実行結果
SQL> @rds_s3_config

Table created.


1 row created.


1 row created.


1 row created.


1 row created.


Commit complete.

SQL> 

S3からRDSへのコピー

S3からオブジェクトをBLOBで取り出し、BLOBをファイルに書き込みます。次のようなプロシージャを作成しておきます。

copy_s3_to_local.sql
create or replace procedure copy_s3_to_local(
  p_s3_bucket varchar2,
  p_s3_key varchar2,
  p_local_dir varchar2,
  p_local_file varchar2
) is
  l_aws_id      varchar2(128);
  l_aws_key     varchar2(128);

  l_blob        blob;
  l_length      integer;
  l_index       integer := 1;
  l_bytecount   integer;
  l_tempraw     raw(32767);
  l_file        utl_file.file_type;
  l_dir         varchar2(128);
begin
  select value into l_aws_id from rds_s3_config where key = 'aws_id';
  select value into l_aws_key from rds_s3_config where key = 'aws_key';
  amazon_aws_auth_pkg.init(l_aws_id, l_aws_key);

  l_blob := amazon_aws_s3_pkg.get_object(p_s3_bucket, p_s3_key);
  -- エラーレスポンスかどうかを粗く判定
  if utl_raw.cast_to_varchar2(dbms_lob.substr(l_blob,256,1)) like '%<Error>%' then
    raise NO_DATA_FOUND;
  end if;

  l_length := dbms_lob.getlength(l_blob);
  l_file  := utl_file.fopen(p_local_dir, p_local_file, 'wb', 32767);

  while l_index <= l_length
  loop
      l_bytecount  := 32767;
      DBMS_LOB.read(l_blob, l_bytecount, l_index, l_tempraw);
      utl_file.put_raw(l_file, l_tempraw);
      l_index      := l_index + l_bytecount;
  end loop;
  utl_file.fflush(l_file);
  utl_file.fclose(l_file);
end;
/
show errors

実行結果
SQL> @copy_s3_to_local

Procedure created.

No errors.
SQL> 

テストしてみましょう。

copy_s3_to_local_test.sql
set serveroutput on
begin
  copy_s3_to_local('<手順2で作成したS3バケット名>', 'ec2/ec2-price.csv', 'EC2', 'ec2-price.csv');
  copy_s3_to_local('<手順2で作成したS3バケット名>', 's3/s3-price.csv', 'S3', 's3-price.csv');
end;
/
テスト:S3からRDSへのダウンロード
SQL> @copy_s3_to_local_test

PL/SQL procedure successfully completed.

SQL> 

RDSからS3へのコピー

ファイルからテーブル上のBLOBに書き込み、S3にアップロードします。次のようなプロシージャを作成しておきます。

copy_local_to_s3.sql
create or replace procedure copy_local_to_s3(
  p_local_dir varchar2,
  p_local_file varchar2,
  p_s3_bucket varchar2,
  p_s3_key varchar2
) is
  l_aws_id      varchar2(128);
  l_aws_key     varchar2(128);

  l_blob        blob;
  l_handle      bfile;
  l_dir         varchar2(128);
  l_doffset     pls_integer := 1;
  l_soffset     pls_integer := 1;
begin
  select value into l_aws_id from rds_s3_config where key = 'aws_id';
  select value into l_aws_key from rds_s3_config where key = 'aws_key';
  amazon_aws_auth_pkg.init(l_aws_id, l_aws_key);

  select tmpblob into l_blob from rds_s3_config where key = 'temporary_blob' for update;
  l_handle := bfilename(p_local_dir, p_local_file);
  dbms_lob.fileopen(l_handle, dbms_lob.file_readonly);
  dbms_lob.loadblobfromfile(l_blob, l_handle, dbms_lob.getlength(l_handle), l_doffset, l_soffset);
  -- このサンプルはContent-TypeをCSVに固定
  amazon_aws_s3_pkg.new_object(p_s3_bucket, p_s3_key, l_blob, 'text/csv');
  dbms_lob.fileclose(l_handle);
  rollback;
end;
/
show errors

テストしてみましょう。

copy_local_to_s3_test.sql
set serveroutput on
begin
  copy_local_to_s3('EC2', 'ec2-price.csv', '<手順2で作成したS3バケット名>', 'ec2/ec2-price.csv');
  copy_local_to_s3('S3', 's3-price.csv', '<手順2で作成したS3バケット名>', 's3/s3-price.csv');
end;
/
テスト
$ aws s3 rm s3://<手順2で作成したS3バケット名>/ec2/ec2-price.csv #ファイルを削除
delete: s3://<手順2で作成したS3バケット名>/ec2/ec2-price.csv
$ aws s3 rm s3://<手順2で作成したS3バケット名>/s3/s3-price.csv   #ファイルを削除
delete: s3://<手順2で作成したS3バケット名>/s3/s3-price.csv
$ aws s3 ls s3://<手順2で作成したS3バケット名>/ec2/ #空であることを確認
$ aws s3 ls s3://<手順2で作成したS3バケット名>/s3/  #空であることを確認
$ sql
... 
SQL> @copy_local_to_s3_test

PL/SQL procedure successfully completed.

SQL> exit
...
$ aws s3 ls s3://<手順2で作成したS3バケット名>/ec2/ #アップロードされたことを確認
2017-08-21 13:44:18   97438744 ec2-price.csv
$ aws s3 ls s3://<手順2で作成したS3バケット名>/s3/  #アップロードされたことを確認
2017-08-21 13:44:20     890903 s3-price.csv
$

まとめ

以上、EC2とS3のファイルのやり取り、そしてS3とRDSのファイルのやり取りについて見てきました。
より本格的に処理するには、特に紹介したPL/SQLプロシージャにおいて、S3へのアップロードのContent-Typeを適切に選択したり、エラーレスポンス(XMLドキュメントが返される)の判定を厳密にしたりなどが必要となるでしょう。

続きを読む

AnsibleでAWS操作 Simple Email Service編

AnsibleでAWS操作シリーズ

  1. aws-cliインストール編
  2. EC2インスタンス編
  3. S3バケット編
  4. CloudFrontディストリビューション編
  5. Simple Email Service編

関連記事

aws-cli コマンド一覧(随時追記)

やりたかったこと

  • SSL証明書発行時のドメイン認証メールをSESにて受信
  • 受信メールをS3バケットに保存
  • GUIを使わずに黒い画面でコマンドを「ッターーン!」してかっこつけたい

やったこと

前提

  • CIサーバー(ansible実行サーバー)構築済み
  • CLIサーバー(aws-cli実行サーバー)構築済み
  • Ansibleインストール済み
  • aws-cliインストール済み
  • 各サーバーへのSSH接続設定済み
  • 独自ドメイン取得済み
  • Route53の設定済み

${~}は各環境に合わせて値を設定してください。

作業フロー

1. ドメイン認証用のトークンを発行

command
ansible-playbook -i inventory/production create-aws-ses-token.yml

※戻り値の値を控えます

2. Route53のレコードセットの更新

command
ansible-playbook -i inventory/production update-aws-route53-record-set.yml

3. Route53の承認ステータスの確認

command
ansible-playbook -i inventory/production view-aws-ses-verification-status.yml

VerificationStatusがSuccessになることを確認します。
※多少時間がかかる場合があります

4. メール保存用のS3バケットを作成

command
ansible-playbook -i inventory/production setup-aws-s3-bucket.yml

5. SESルール周りのセットアップの作成

command
ansible-playbook -i inventory/production setup-aws-ses-rule.yml

ディレクトリ構成


├── ansible.cfg
├── create-aws-ses-token.yml
├── templates
│   └── production
│       ├── route53
│       │   └── record_set.j2
│       ├── s3api
│       │   └── s3-policy.j2
│       └── ses
│           └── rule-set.j2
├── inventory
│   └── production
│       └── inventory
├── roles
│   ├── active-aws-ses-rule-set
│   │   └── tasks
│   │       └── main.yml
│   ├── create-aws-s3-bucket
│   │   └── tasks
│   │       └── main.yml
│   ├── create-aws-ses-rule
│   │   └── tasks
│   │       └── main.yml
│   ├── create-aws-ses-rule-set
│   │   └── tasks
│   │       └── main.yml
│   ├── create-aws-ses-token
│   │   └── tasks
│   │       └── main.yml
│   ├── setup-aws-s3-bucket
│   │   └── tasks
│   │       └── main.yml
│   ├── update-aws-route53-record-set
│   │   └── tasks
│   │       └── main.yml
│   └── view-aws-ses-verification-status
│       └── tasks
│           └── main.yml
├── setup-aws-ses-rule.yml
├── setup-aws-s3-bucket.yml
├── update-aws-route53-record-set.yml
├── view-aws-ses-verification-status.yml
└── vars
    └── all.yml

Ansible構成ファイル

inventory

inventory/production/inventory
[ciservers]
${CIサーバーホスト}

[cliservers]
${CLIサーバーホスト}

[all:vars]
ENV=production

vars

vars/all.yml
SERVER_IP: ${IPアドレス}
TEMP:
  DIRECTORY: /temp
DOMAIN:
  MAIN:
    NAME: ${ドメイン名}
  SUB:
    NAME: ${サブドメイン名}
AWS:
  ROUTE53:
    HOSTED_ZONE_ID: ${ホストゾーンID}
  S3:
    BUCKET:
      NAME: ${バケット名}
  SES:
      TOKEN: ${認証トークン}
      REGION: us-west-2
    RULE:
      NAME: ${ルール名}
      SET:
        NAME: ${ルールセット名}

templates

json/production/ses/rule-set.j2
{
  "Name": "{{ AWS.SES.RULE.NAME }}",
  "Enabled": true,
  "TlsPolicy": "Optional",
  "Recipients": [
    "admin@{{ DOMAIN.MAIN.NAME }}",
    "admin@{{ DOMAIN.SUB.NAME }}"
  ],
  "Actions": [
    {
      "S3Action": {
        "BucketName": "{{ AWS.S3.BUCKET.NAME }}"
        }
    }
  ],
  "ScanEnabled": true
}
s3-policy.j2
{
  "Version":"2012-10-17",
  "Statement":[{
      "Sid":"AddPerm",
      "Effect":"Allow",
        "Principal": "*",
      "Action":["s3:PutObject"],
      "Resource":["arn:aws:s3:::{{ AWS.S3.SES.NAME }}/*"]
    }
  ]
}
record_set
{
  "Comment": "DomainRecords",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "EvaluateTargetHealth": false,
          "DNSName": "{{ DOMAIN.MAIN.NAME }}"
        },
        "Type": "A",
        "Name": "{{ DOMAIN.MAIN.NAME }}"
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "{{ DOMAIN.SUB.NAME }}",
        "Type": "A",
        "TTL": 1200,
        "ResourceRecords": [
          {
            "Value": "{{ SERVER_IP }}"
          }
        ]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "_amazonses.{{ DOMAIN.MAIN.NAME }}.",
        "Type": "TXT",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": ""{{ AWS.SES.TOKEN }}""
          }
        ]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "{{ DOMAIN.MAIN.NAME }}.",
        "Type": "MX",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "10 inbound-smtp.{{ AWS.SES.REGION }}.amazonaws.com."
          }
        ]
      }
    }
  ]
}

playbook

create-aws-ses-token.yml
- hosts: cliservers
  roles:
    - create-aws-ses-token
  vars_files:
    - vars/all.yml
setup-aws-ses-rule.yml
- hosts: cliservers
  roles:
    - create-aws-ses-rule-set
    - create-aws-ses-rule
    - active-aws-ses-rule-set
  vars_files:
    - vars/all.yml
setup-aws-s3-bucket.yml
- hosts: cliservers
  roles:
    - create-aws-s3-bucket
    - setup-aws-s3-bucket
  vars_files:
    - vars/all.yml
update-aws-route53-record-set.yml
- hosts: cliservers
  roles:
    - update-aws-route53-record-set
  vars_files:
    - vars/all.yml
view-aws-ses-verification-status.yml
- hosts: cliservers
  roles:
    - view-aws-ses-verification-status
  vars_files:
    - vars/all.yml

tasks

role/active-aws-ses-rule-set/tasks/main.yml
- name: Active Receipt Rule Set
  shell: |
    aws ses set-active-receipt-rule-set 
    --region={{ AWS.SES.REGION }} 
    --rule-set-name {{ AWS.SES.RULE.SET.NAME }}
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/create-aws-s3-bucket/tasks/main.yml
- name: Create Bucket
  shell: "aws s3 mb s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/create-aws-ses-rule-set/tasks/main.yml
- name: Create Receipt Rule Set
  shell: |
    aws ses create-receipt-rule-set 
    --region={{ AWS.SES.REGION }} 
    --rule-set-name {{ AWS.SES.RULE.SET.NAME }} 
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/create-aws-ses-rule/tasks/main.yml
- name: Create Replaced File
  template: 
    src={{ ENV }}/ses/rule-set.j2
    dest={{ TEMP.DIRECTORY }}/rule-set.json
  tags:
    - always

- name: Create Receipt Rule
  shell: |
    aws ses create-receipt-rule 
    --region={{ AWS.SES.REGION }} 
    --rule-set-name {{ AWS.SES.RULE.SET.NAME }} 
    --rule file://{{ TEMP.DIRECTORY }}/rule-set.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/create-aws-ses-token/tasks/main.yml
- name: "Create SES Token"
  shell: |
    aws ses verify-domain-identity  
    --domain "{{ DOMAIN.MAIN.NAME }}" 
    --region={{ AWS.SES.REGION }}
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/setup-aws-s3-bucket/tasks/main.yml
- name: Create Replaced File
  template: 
    src={{ ENV }}/s3api/s3-policy.j2
    dest={{ TEMP.DIRECTORY }}/s3-policy.json
  tags:
    - always

- name: Create Policy
  shell: |
    aws s3api put-bucket-policy 
    --bucket {{ AWS.S3.BUCKET.NAME }} 
    --policy file://{{ TEMP.DIRECTORY }}/s3-policy.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/update-aws-route53-record-set/tasks/main.yml
- name: Create Replaced File
  template: 
    src={{ ENV }}/route53/record_set.j2
    dest={{ TEMP.DIRECTORY }}/record_set.json
  tags:
    - always

- name: Update Record Set
  shell: |
    aws route53 change-resource-record-sets 
    --hosted-zone-id {{ AWS.ROUTE53.HOSTED_ZONE_ID }} 
    --change-batch file://{{ TEMP.DIRECTORY }}/record_set.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/view-aws-ses-verification-status/tasks/main.yml
- name: View Verification Status
  shell: |
    aws ses get-identity-verification-attributes 
    --identities ${DOMAIN.MAIN.NAME}
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

終わりに

上記作業が正常に終了すれば、SSL証明書発行時のドメイン認証メールをSESにて受信及びS3バケットへの保存が可能になりました。

ただ、S3バケット上で保存している電子メールは そのままだと解読が出来ない ので、オブジェクトをローカル等にダウンロードした上で内容を表示する必要があります。(Lambdaを使えば転送とかもできるかも?)

ここまでの設定をすればACMにて無料のSSL証明書を発行する準備が出来たので、
CloudFront+S3+ACMによる無料SSLサイトを構築することが可能 です。

AWSサービス同士はとても連携しやすく、同サービス内だからこそのメリットもたくさんあるので他にも良い組み合わせがあったら記事にまとめていこうと思います♪

じゃあの。

続きを読む

AWS IoT Buttonで"ゆれ"とツイートする

AWS IoT Buttonというものがあります。

こんなの↓

(出典:https://aws.amazon.com/jp/iotbutton/

簡単に言えばAmazon Dash Buttonの処理をカスタマイズ出来る版ってな感じのシロモノです。
これがあれば、あんなことやそんなことが出来ると夢が拡がる一方ですが、
取り敢えずはAWS IoT初体験として「押したら”ゆれ”とツイートするボタン」を試しに作ってみました。
特に意味はありません。

ちなみに2017年8月時点ではまだ日本での販売はされておらず、
今回入手したものは本家の米Amazonから購入しています。うっかり5個買っちゃった。

概観図

tweet_button1.png

Twitterのアプリ登録

Twitterへ投稿する処理を実装するためには、あらかじめアプリ登録を済ませておく必要があります。
具体的な手順は下記のとおりです(一連の手順やWebページのUIは変更される可能性有)。

  1. ボタンを押したとき呟くことになるアカウントでTwitterへログインする
  2. https://apps.twitter.com へアクセス
  3. 右上の「Create New App」を押す
  4. Name、Description、Websiteを適当に入力する
    • Websiteは必須項目なので何かしらを入力(https://twitter.com など)、Callback URLは空でOK
  5. Twitter Developer Agreementを確認してチェックを付けた後、「Create your Twitter application」を押す
  6. 登録成功した感じのページに遷移したらタブの「Keys and Access Tokens」を選択
  7. 下の方にある「Create my access token」のボタンを押す

ここまでの操作により、下に赤枠で示した4つの情報が確認できればTwitter側は準備完了です。

※Access Tokenは途中にあるハイフンも含むのでコピペの際に間違って消さないよう注意

AWS IoT Buttonのセットアップ

AWS IoT Buttonを利用するためにはAWSアカウントが必要です。
まだアカウントを作成していない場合はこちらなどから頑張ってサインアップします。

AWSコンソールへサインイン出来たら、
ここに書いてある通りにセットアップを実行します。

一つ注意点として、セットアップ手順を進めるPCが無線LANにアクセスできない端末の場合、
無線LANアクセス可能なPCへ証明書と鍵を送ってButtonの設定を完了させる必要があります。
これにもたついているとButtonへの接続が切れて入力やり直しとかになったりします。
まぁ今時そんなPCはまず無いと思いますが、念のため。
なんでこのPC無線LAN子機内蔵してないんだ……

Lambda関数の処理を実装

AWS IoT Buttonのセットアップ手順でLambda関数の作成までは行ったので、
その関数の処理をTwitterへ投稿する処理に変更します。
上でセットアップしたButtonが押されると、ここで書いた処理が実行されるという感じになるわけですね。

下記の手順は既にNode.js & npmがインストールされているという前提になります。

まず、作業ディレクトリを作成して、

mkdir iotbutton
cd iotbutton

Twitterのライブラリをインストールし、

npm install twitter

Twitterへ”ゆれ”と投稿する処理を書いてindex.jsとして保存した後、
(clientのパラメータにはTwitterでアプリ登録した際に確認したキーを指定)

const Twitter = require('twitter');

const client = new Twitter({
    consumer_key: 'xxxxxxxx',
    consumer_secret: 'xxxxxxxx',
    access_token_key: 'xxxxxxxx',
    access_token_secret: 'xxxxxxxx'
});

exports.handler = (event, context, callback) => {
    client.post('statuses/update', {status: 'ゆれ'}, function(error, tweet, response) {
        if (error) {
            console.log(error);
            callback(error);
            return;
        }
        console.log(tweet);
        callback();
    });
};

最後にnode_modulesindex.jsをzipで固めてLambda関数のコードとしてアップロードするだけです。
とっても簡単!

一度アップロードした後は、AWSコンソール上からインラインでコードの編集が出来るようになります。

動作確認

これで準備は全て整いましたので、早速試してみます。
ただし、ゆれてもいないときに一人ゆれツイートを投稿して誤爆しちゃったみたいな感じになるのは嫌なので
今だけ”This is a test tweet by Lambda.”に文言を変えています。

まずはButtonを押下。
少し間を空けてからタイムラインを更新すると……

tweet_button2.png

成功です!
これでもういつ地震が来ても大丈夫。

制限事項とか残課題とか

  • Buttonを押してから呟きが投稿されるまでに5秒~10秒くらいのタイムラグがある。自分で投稿した方が速そう
  • セットアップしたWi-Fiアクセスポイントのある場所でしか使えない。
  • 間違えて押すと悲しいことに。 → 2回押されたらキャンセル or 削除させる?

雑感

タイムラグが致命的なため、地震ツイートRTA用途には使えなさそうなのが非常に残念でした。

それはともかくとして、セットアップの容易さとAWSとの連携による汎用性の高さを併せ持つ
このButton(1個約20$)は魅力的で可能性を感じるプロダクトだと改めて実感しました。
早く日本でも販売開始してほしい。

余談ですが、国産のIoTプロダクトではMESHというのもありますね。
機会があればこっちも触ってみるかもしれません。

続きを読む

AWS EC2 Mysql レプリケーション環境構築

はじめに

本番環境はRDSのMulti A-Z でレプリケーションしているのですが、selectはslaveに、FOR UPDATEはmasterになどクエリの向き先をしっかり確認しておく必要があったので勉強がてらEC2でレプリケーション環境を構築したので自分用にメモ。

やること

・ インスタンスを2つ起動
・ レプリケーション設定
・ 接続確認

EC2インスタンス起動

セキュリティ設定
2つのサーバのMysqlが互いに通信できるように、
Inboundで3306ポートに対して、互いのPrivateIPを設定します。

レプリケーション設定

Master-Server

my.conf

/etc/my.cnf
[mysqld]
server-id=101
log_bin

server-idは、1から(2^32)-1)間の正整数で、サーバ間で重複しないように設定する(任意の値でOK)
ただし、1、2はserver-idがないときのデフォルトとして使用されるため極力使わないほうがいい

Slave-ServerのUser作成とアクセスの許可

mysql > grant replication slave on *.* to 'replication'@'Slave-Srever Private IP' identified by 'password'

ステータスの確認

mysql > show master status;
+-------------------+----------+--------------+------------------+
| File              | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+------------------+
| mysqld-bin.000001 |     1234 |              |                  |
+-------------------+----------+--------------+------------------+

Slave-Server

my.conf

/etc/my.cnf
[mysqld]
server-id=201
replicate-do-db=db名(対象DB指定)

Master-Server登録

mysql > change master to master_host = 'Master-Server Private IP',
        master_user = 'replication',
        master_password = 'pass',
        master_log_file = 'mysqld-bin.000001', //Master-Serverの情報
        master_log_pos = 1234 //Master-Serverの情報

Slave-ServerのUser作成とアクセスの許可

mysql > grant all privileges on *.* to name@'Master-Srever Private IP' identified by 'password';

ステータスの確認

mysql > show slave status \G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: Master-Server Private IP
                  Master_User: replication
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysqld-bin.000001
          Read_Master_Log_Pos: 2258
               Relay_Log_File: mysqld-relay-bin.000004
                Relay_Log_Pos: 10195
        Relay_Master_Log_File: mysqld-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes

Slave_IO_Running: Yes、Slave_SQL_Running: Yesで設定OK

接続確認

各サーバーからホスト指定でログインできれば接続はできている状態となります。
下記のエラーが出る場合はEC2インスタンスのセキュリティを見直す必要があります。

ERROR 1130 (HY000): Host 'Private IP' is not allowed to connect to this MySQL server

さいごに

その都度、各mysqlの再起動や必要であれば権限の反映FLUSH PRIVILEGESが必要になるかと思います。
自分用のメモとして記述しましたが、不備がありましたらご教授いただけますと幸いです。

続きを読む

AnsibleでAWS操作 S3バケット編

AnsibleでAWS操作シリーズ

  1. aws-cliインストール編
  2. EC2インスタンス編
  3. S3バケット編
  4. CloudFrontディストリビューション編

関連記事

aws-cli コマンド一覧(随時追記)

やりたかったこと

  • S3バケットの作成/削除
  • S3バケットへのオブジェクトの転送/削除
  • S3バケットを静的Webサイトホスティング化
  • GUIを使わずに黒い画面でコマンドを「ッターーン!」してかっこつけたい

やったこと

前提

  • CIサーバー(ansible実行サーバー)構築済み
  • CLIサーバー(aws-cli実行サーバー)構築済み
  • Ansibleインストール済み
  • aws-cliインストール済み
  • 各サーバーへのSSH接続設定済み

${~}は各環境に合わせて値を設定してください。

作業フロー

1. S3バケットを新規作成

command
ansible-playbook -i inventory/production create-aws-s3-bucket.yml

2. S3バケットをWebサイトホスティング化

command
ansible-playbook -i inventory/production setup-aws-s3-bucket.yml

3. オブジェクトを転送

command
ansible-playbook -i inventory/production upload-aws-s3-object.yml

4. オブジェクトの転送確認

command
ansible-playbook -i inventory/production view-aws-s3-object.yml

5. オブジェクトの削除

command
ansible-playbook -i inventory/production delete-aws-s3-object.yml

6. バケットの削除

command
ansible-playbook -i inventory/production delete-aws-s3-bucket.yml

ディレクトリ構成


├── ansible.cfg
├── create-aws-s3-bucket.yml
├── delete-aws-s3-bucket.yml
├── delete-aws-s3-object.yml
├── files
│   └── production
│       └── s3
│           └── sample.txt
├── inventory
│   └── production
│       └── inventory
├── roles
│   ├── create-aws-s3-bucket
│   │   └── tasks
│   │       └── main.yml
│   ├── delete-aws-s3-bucket.yml
│   │   └── tasks
│   │       └── main.yml
│   ├── delete-aws-s3-object.yml
│   │   └── tasks
│   │       └── main.yml
│   ├── setup-aws-s3-bucket.yml
│   │   └── tasks
│   │       └── main.yml
│   ├── upload-aws-s3-bucket.yml
│   │   └── tasks
│   │       └── main.yml
│   └── view-aws-s3-object.yml
│       └── tasks
│           └── main.yml
├── upload-aws-s3-object.yml
├── setup-aws-s3-bucket.yml
├── vars
│   └── all.yml
└── view-aws-s3-object.yml

Ansible構成ファイル

inventory

inventory/production/inventory
[ciservers]
${CIサーバーホスト}

[cliservers]
${CLIサーバーホスト}

[all:vars]
ENV=production

vars

vars/all.yml
AWS:
  S3:
    BUCKET:
      NAME: ${バケット名}

playbook

create-aws-s3-bucket
- hosts: cliservers
  roles:
    - create-aws-s3-bucket
  vars_files:
    - vars/all.yml
delete-aws-s3-bucket
- hosts: cliservers
  roles:
    - delete-aws-s3-bucket
  vars_files:
    - vars/all.yml
delete-aws-s3-object
- hosts: cliservers
  roles:
    - delete-aws-s3-object
  vars_files:
    - vars/all.yml
setup-aws-s3-bucket
- hosts: cliservers
  roles:
    - setup-aws-s3-bucekt
  vars_files:
    - vars/all.yml
upload-aws-s3-object
- hosts: cliservers
  roles:
    - update-aws-s3-bucket
  vars_files:
    - vars/all.yml
view-aws-s3-object
- hosts: cliservers
  roles:
    - view-aws-s3-object
  vars_files:
    - vars/all.yml

tasks

role/create-aws-s3-bucket/tasks/main.yml
- name: Create Bucket
  shell: "aws s3 mb s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/delete-aws-s3-bucket/tasks/main.yml
- name: "Remove Bucket"
  shell: "aws s3 rb s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/delete-aws-s3-bucket/tasks/main.yml
- name: "Delete Bucket"
  shell: "aws s3 rb s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/delete-aws-s3-object/tasks/main.yml
- name: "Delete Object"
  shell: "aws s3 rm s3://{{ AWS.S3.BUCKET.NAME }}/sample.txt"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/setup-aws-s3-bucket/tasks/main.yml
- name: "Setup Bucket"
  shell: |
    aws s3 website s3://{{ AWS.S3.BUCKET.NAME }} \
    --index-document index.html
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/upload-aws-s3-object/tasks/main.yml
- name: "Upload Object"
  shell: "aws s3 cp files/{{ ENV }}/s3/sample.txt s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/view-aws-s3-object/tasks/main.yml
- name: "View Objects"
  shell: "aws s3 ls s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

終わりに

S3のバケット作成やファイル操作も 簡単 に行うことが可能で、 静的サイト として利用する場合のためのindexドキュメントの指定などのオプションもあります。

また、今回は触れませんでしたが転送したファイル単位で パーミッション の設定も出来るので柔軟性もあります。

また、CloudFrontと組み合わせればキャッシュを有効利用してパフォーマンス向上を図ることも可能です。

S3バケットは 静的コンテンツのホスティング簡易ファイルサーバーWordpressの記事をhtml化 したりとかなり汎用性が高く、 料金もかなり良心的 なのでどんどん利用していきましょう♪

じゃあの。

続きを読む

Running Kubernetes Cluster on AWS

Following this guide https://kubernetes.io/docs/getting-started-guides/kops/, set up Kubernetes cluster on AWS using kops.

1. Install kops

According to the guide, kops is:

kops helps you create, destroy, upgrade and maintain production-grade, hig… 続きを読む

AWS SystemsManagerを使ってみたメモ

(WIP)まだ修正中なのでどんどん追加していく

AWS SystemsManagerを使用してみた

脆弱性検知、自動パッチ当て、構成管理などなど幅広く使用できるAWSマネージドサービスと聞いて使ってみた。
とりあえずAWSマネジメントコンソールのEC2のサイドバーの上から実施していく。

サマリ

  1. コマンドの実行
  2. ステートマネージャー
  3. 自動化
  4. Patch Compliance
  5. パッチベースライン

コマンドの実行

ここではランコマンドなるものの実行ができるらしい
ただし前準備が色々必要そうなのでまとめてみる…
1. IAMロール作成(EC2インスタンス用、ユーザー用)
まずはEC2インスタンス用から作成
IAMロール作成画面から作成する
  image.png

  image.png

image.png

次に現在サインインしているIAMユーザーに設定されているロールに対して下記ポリシーをアタッチする
– AmazonSSMFullAccess
– AmazonSSMReadOnlyAccess

  1. SSMエージェントのインストール
    インストールコマンドを実行していく(AmazonLinux201703)
mkdir /tmp/ssm
cd /tmp/ssm
sudo yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
  1. ドキュメント選択
    適当なドキュメントを選択する
    今回はansibleでyum updateとかしてみようと思ったので、ansibleのplaybookを実行できるドキュメントを使用する
    image.png

playbookを書いていざ実行してみると、、、
ansibleがインストールされていないとのこと(笑)
image.png

なのでansibleをインストールして再度実行してみる、すると、、、
image.png

無事成功!

ステートマネージャー

自動化

Patch Compliance

パッチベースライン

参考

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-setting-up.html?icmpid=docs_ec2_console

続きを読む

Rails5アプリケーションのAWSによるネットワーク構築 Nginx+Puma+Capistranoな環境とAWS構築(VPC EC2 RDS CloudFlont Route53 etc)

はじめに

数ヶ月前に書いたRails5のAWS構築をもう少し項目を増やしてから公開しようと思っていたのですが、なかなか出来ないのでもう公開します。(ELBとかCloudFlontとかもっとちゃんと書けそう、、)
項目の追加や修正は適宜入れると思います。

Railsは5.0を使っています。(記事を書いたときの最新)

4系は以前書きました↓

構築する環境

  • VPC
  • EC2
    • Nginx + Puma
    • Capistrano
  • (S3)
  • ELB
  • RDS
  • CloudFlont
  • Route53
  • (CloudWatch)

VPC、EC2のインスタンスの作成

AWSのデザインが多少変更はありますが、以下を参考に作成出来ます。

AWS VPCによるネットワーク構築とEC2によるサーバー構築

これに追加でElastic IPの設定もしておきました。

EC2内の環境構築

作成したEC2にログインして環境を構築します。
ec2-userでまずログインしましょう。

ユーザーの作成

デフォルトではec2-userなので新たなユーザーを追加し、そのユーザーでsshログイン出来るように設定します。

$ sudo adduser shizuma #ユーザーの追加 「shizuma」の部分は好きなユーザー名にします。以後「shizuma」の部分は各自のユーザー名になります。
$ sudo passwd shizuma
#ここで、新規ユーザーのパスワードを設定します。
$ sudo visudo
-----------------------------
#vimが起動するので新規ユーザーにroot権限を与える。
root    ALL=(ALL)       ALL
shizuma ALL=(ALL)       ALL #この行を追加
-----------------------------
$ sudo su - shizuma #ユーザー切り替え
#先ほど設定したパスワード

ここでローカルに一旦戻り、鍵を作成します。

$ cd .ssh
$ ssh-keygen -t rsa
-----------------------------
Enter file in which to save the key ():first_aws_rsa #ここでファイルの名前を記述して、エンター
Enter passphrase (empty for no passphrase): #何もせずそのままエンター
Enter same passphrase again: #何もせずそのままエンター
-----------------------------
$ vi config
-----------------------------
# 以下を追記
Host first_aws
  Hostname 54.64.22.197 #自分の設定に合わせて
  Port 22
  User shizuma #先ほどのユーザー名
  IdentityFile ~/.ssh/first_aws_rsa #秘密鍵の設定
-----------------------------

次に、サーバーに戻り作成した 公開鍵 をサーバーに設定します。

$ mkdir .ssh
$ chmod 700 .ssh
$ cd .ssh
$ vi authorized_keys
-----------------------------
#localの「first_aws_rsa.pub」の中身のコピペ。(localで $ cat first_aws_rsa.pubとかすると良い)
ssh-rsa sdfjerijgviodsjcIKJKJSDFJWIRJGIUVSDJFKCNZKXVNJSKDNVMJKNSFUIEJSDFNCJSKDNVJKDSNVJNVJKDSNVJKNXCMXCNMXNVMDSXCKLMKDLSMVKSDLMVKDSLMVKLCA shizuma@shizuma-no-MacBook-Air.local
-----------------------------
$ chmod 600 authorized_keys
$ exit
$ exit

これで設定が完了したので、以降作成したユーザーでアクセスするようにします。

基本ライブラリとrubyの環境構築

$ sudo yum install 
git make gcc-c++ patch 
openssl-devel 
libyaml-devel libffi-devel libicu-devel 
libxml2 libxslt libxml2-devel libxslt-devel 
zlib-devel readline-devel 
mysql mysql-server mysql-devel 
ImageMagick ImageMagick-devel 
epel-release
$ sudo yum install nodejs npm --enablerepo=epel
$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source .bash_profile
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv rehash
$ rbenv install -v 2.3.1
$ rbenv global 2.3.1
$ rbenv rehash
$ ruby -v

gitの設定

最低限の設定だけしておきます。

$ vi .gitconfig
.gitignore
[user]
  name = your_name #自分の名前
  email = hoge@hoge.com #自分のメアド

[alias] #これはお好きに
  a = add
  b = branch
  ch = checkout
  st = status

[color] #色付け
  ui = true

[url "github:"] #pull、pushのための設定
    InsteadOf = https://github.com/
    InsteadOf = git@github.com:

アプリケーションフォルダの設置

/var/www/rails にアプリケーションフォルダを設置します。

$ cd /
$ sudo mkdir -p /var/www/rails
$ sudo chown -R shizuma var/www

GitHubの接続とアプリケーションのclone

$ cd ~/.ssh
$ ssh-keygen -t rsa
-----------------------------
Enter file in which to save the key ():aws_git_rsa #ここでファイルの名前を記述して、エンター
Enter passphrase (empty for no passphrase): #何もせずそのままエンター
Enter same passphrase again: #何もせずそのままエンター
-----------------------------
$ chmod 744 config
$ vi config
-----------------------------
# 以下を追記
Host github github.com
  Hostname github.com
  User git
  Port 22
  IdentityFile ~/.ssh/aws_git_rsa #秘密鍵の設定
-----------------------------
$ cat aws_git_rsa.pub
-----------------------------
ssh-rsa sdfjerijgviodsjcIKJKJSDFJWIRJGIUVSDJFKCNZKXVNJSKDNVMJKNSFUIEJSDFNCJSKDNVJKDSNVJNVJKDSNVJKNXCMXCNMXNVMDSXCKLMKDLSMVKSDLMVKDSLMVKLCA shizuma@ip-10-0-1-10
-----------------------------

ここで、これをコピペしてgithubに公開鍵を登録する。
githubへの鍵の登録がよくわからない方は以下の記事を参考に。
gitHubでssh接続する手順~公開鍵・秘密鍵の生成から~

そして、git clone する。

$ cd /var/www/rails
$ git clone https://github.com/kuboshizuma/cheerfull # 自分のアプリケーション

RDSの設定

EC2の環境構築に一区切りついたのでRDSの設定を行います。

DBサブネットの登録

RDSで使うサブネットを登録します。
この設定には異なるアベイラビリティゾーンにあるサブネットが最低1つずつ計2つ必要になります。
また、Railsアプリケーションをおいたサブネットはゲートウェイに繋がったpublicなサブネットなので、privateなサブネットを異なるアベイラビリティゾーンに一つずつ作成します。

スクリーンショット_2017-03-04_11_56_01.png

パラメータグループの設定

パラメータグループを設定します。
mysqlを使用していると設定なので、ここではmysqlを選択します。バージョンは適宜選択。
「chara…」で検索すると出て来るcharasetをutf-8に変更。

スクリーンショット_2017-03-04_12_00_57.png

設定が完了した後に、パラメータの編集を押してパラメーター変更。

スクリーンショット_2017-03-04_12_04_29.png

セキュリティグループの作成

VPCでセキュリティグループを作成します。

スクリーンショット_2017-03-04_12_08_19.png

インバウンドルールにMySQLを設定し、MySQLのアクセスのみ許可します。
「送信元」は0.0.0.0/0で設定。特定のRailsアプリケーションサーバーがあるサブネットに限定するほうがよさそう。

スクリーンショット 2017-03-04 12.55.42.png

RDSインスタンスの作成

エンジンの選択

スクリーンショット_2017-03-04_12_12_05.png

本番稼働用?

スクリーンショット_2017-03-04_12_12_45.png

DB詳細設定の指定

無料枠はt2.micro

スクリーンショット_2017-03-04_12_17_35.png

詳細設定の設定

各種作成したもので設定。

スクリーンショット_2017-03-04_12_19_44.png

スクリーンショット_2017-03-04_12_19_53.png

接続確認

hostは各自の作成したRDSインスタンスのエンドポイントをみる。

$ mysql -h hogepoge.ap-northeast-1.rds.amazonaws.com -u shizuma -P 3306 -p

接続出来たら完了!

絵文字の扱い

絵文字も登録出来るようにする場合。
以下を参考に utfmb4を採用する。

ActiveRecordをutf8mb4で動かす

MAMPでは /Applications/MAMP/conf/my.cnfmy.cnf を設置した。

Railsアプリケーションの起動のための準備

puma setting

以下を追記する。

cofig/puma.rb
# add setting for production
_proj_path = "#{File.expand_path("../..", __FILE__)}"
_proj_name = File.basename(_proj_path)
_home = ENV.fetch("HOME") { "/home/#{ENV['PUMA_USER']}" }
pidfile "#{_home}/run/#{_proj_name}.pid"
bind "unix://#{_home}/run/#{_proj_name}.sock"
directory _proj_path
# add end

ENV['PUMA_USER'] にサーバーのユーザー名が入るように環境変数を設定。

database setting

以下のように編集。

config/database.yml
production:
  <<: *default
  database: cheerfull
  username: <%= ENV['DATABASE_USER_NAME_PRODUCTION'] %>
  password: <%= ENV['DATABASE_PASSWORD_PRODUCTION'] %>
  host: <%= ENV['DATABASE_HOST_PRODUCTION'] %>

それぞれ該当の環境変数を設定。

rake secret

以下のsecret keyの箇所に値が入るように環境変数を設定。
rake secret とコマンドを打つと出て来る文字列を設定する。

config/secrets.yml
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
$ gem install bundler
$ bundle install
$ rake db:create RAILS_ENV=production
$ rake db:migrate RAILS_ENV=production

PumaとNginxの起動

Pumaの起動

アプリケーションディレクトリにて以下を実行。

$ RAILS_SERVE_STATIC_FILES=true RAILS_ENV=production puma -w 4

「Ctr+c」でプロセス消さなければ生き残る。
プロセス消す必要があれば以下のようにする。

$ ps aux | grep puma # プロセスIDを探す
$ kill -9 (ID)

Nginx の起動

「cheerfull」の部分は自分のアプリケーションディレクトリ名に変更する。

/etc/nginx/conf.d/cheerfull.conf
  # log directory
  error_log  /var/www/rails/cheerfull/log/nginx.error.log;
  access_log /var/www/rails/cheerfull/log/nginx.access.log;
  upstream app_server {
    # for UNIX domain socket setups
    server unix:/home/shizuma/run/cheerfull.sock fail_timeout=0;
  }
  server {
    listen 80;
    server_name 12.134.156.178; # 自分のIP
    # nginx so increasing this is generally safe...
    # path for static files
    root /var/www/rails/cheerfull/public;
    # page cache loading
    try_files $uri/index.html $uri @app_server;
    location / {
      # HTTP headers
      proxy_pass http://app_server;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
    }
    # Rails error pages
    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /var/www/rails/cheerfull/public;
    }
    client_max_body_size 4G;
    keepalive_timeout 5;
  }

ユーザーをnginxから自分のユーザー名に変更しておく。

/etc/nginx/nginx.conf
#user nginx;
user shizuma;

nginxを起動します。

$ sudo service nginx restart

これで、IPアドレスでアクセスするとアプリケーションが表示されるようになりました。

Capistranoの設定

諸々動くことが確認出来たのでデプロイが出来るように設定します。
デプロイのためにCapistranoの設定をします。
慣れてきたら、いきなりCapistranoの設定をしていけばいいと思います。

socketの場所もアプリケーションディレクトリに変更するのでnginxの設定もそれに合わせて変更します。

「cheerfull」の部分は自分のアプリケーション名に変更する。

deploy.rb
require 'dotenv'
Dotenv.load

lock "3.7.1"

set :application, "cheerfull"
set :repo_url, "git@github.com:your_gitname/cheerfull.git"
set :branch, 'master'
set :deploy_to, '/var/www/rails/protospacce'
set :linked_files, fetch(:linked_files, []).push('.env')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')
set :keep_releases, 5
set :rbenv_ruby, '2.3.1'

# puma setting
set :puma_threads,    [4, 16]
set :puma_workers,    0
set :pty,             true
set :use_sudo,        false
set :stage,           :production
set :deploy_via,      :remote_cache
set :deploy_to,       "/var/www/rails/#{fetch(:application)}"
set :puma_bind,       "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{shared_path}/log/puma.error.log"
set :puma_error_log,  "#{shared_path}/log/puma.access.log"
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true

namespace :deploy do
  desc 'Create database'
  task :db_create do
    on roles(:db) do |host|
      with rails_env: fetch(:rails_env) do
        within current_path do
          execute :bundle, :exec, :rake, 'db:create'
        end
      end
    end
  end
end

実際にdeployする前に、共有ファイルは用意しておきましょう。この場合、.env

ここでpumaのセッティングをしたので、config/puma.rb はもう不要になります。
また、dotenvを使用する場合は変更を読み込むために config/deploy/templates/puma.rb.erb を用意します。以下を用意。

config/deploy/templates/puma.rb.erb
#!/usr/bin/env puma

directory '<%= current_path %>'
rackup "<%=fetch(:puma_rackup)%>"
environment '<%= fetch(:puma_env) %>'
<% if fetch(:puma_tag) %>
  tag '<%= fetch(:puma_tag)%>'
<% end %>
pidfile "<%=fetch(:puma_pid)%>"
state_path "<%=fetch(:puma_state)%>"
stdout_redirect '<%=fetch(:puma_access_log)%>', '<%=fetch(:puma_error_log)%>', true


threads <%=fetch(:puma_threads).join(',')%>

<%= puma_bind %>
<% if fetch(:puma_control_app) %>
activate_control_app "<%= fetch(:puma_default_control_app) %>"
<% end %>
workers <%= puma_workers %>
<% if fetch(:puma_worker_timeout) %>
worker_timeout <%= fetch(:puma_worker_timeout).to_i %>
<% end %>

<% if puma_preload_app? %>
preload_app!
<% else %>
prune_bundler
<% end %>

on_restart do
  puts 'Refreshing Gemfile'
  ENV["BUNDLE_GEMFILE"] = "<%= fetch(:bundle_gemfile, "#{current_path}/Gemfile") %>"
  ENV.update Dotenv::Environment.new('.env')
end

<% if puma_preload_app? and fetch(:puma_init_active_record) %>
before_fork do
  ActiveRecord::Base.connection_pool.disconnect!
end

on_worker_boot do
  ActiveSupport.on_load(:active_record) do
    ActiveRecord::Base.establish_connection
  end
end
<% end %>
$ bundle exec production cap puma:config 

これでこの設定を本番に反映。

あとはデプロイコマンドを打つ。

$ bundle exec cap production deploy

デプロイdone!

nginxのlogの位置やsocketの位置を変更します。

/etc/nginx/conf.d/cheerfull.conf
  # log directory
  error_log  /var/www/rails/cheerfull/shared/log/nginx.error.log;
  access_log /var/www/rails/cheerfull/shared/nginx.access.log;
  upstream app_server {
    # for UNIX domain socket setups
    server unix:/var/www/rails/cheerfull/shared/tmp/sockets/cheerfull-puma.sock fail_timeout=0;
  }
  server {
    listen 80;
    server_name 12.134.156.178; # 自分のIP
    # nginx so increasing this is generally safe...
    # path for static files
    root /var/www/rails/cheerfull/current/public;
    # page cache loading
    try_files $uri/index.html $uri @app_server;
    location / {
      # HTTP headers
      proxy_pass http://app_server;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
    }
    # Rails error pages
    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /var/www/rails/cheerfull/current/public;
    }
    client_max_body_size 4G;
    keepalive_timeout 5;
  }
$ sudo service nginx restart

これで、デプロイ環境が整いました。
以前起動したpumaのプロセスはきっておきましょう。

複数サーバーへのデプロイ

multiconfig を使用します。

Gemfile
gem 'capistrano-multiconfig', '~> 3.1', require: false

Capfileのcapistrano/setupから変更。

Capfile
# require "capistrano/setup"
require 'capistrano/multiconfig'

config/deploy/production.rb から config/deploy/app1/production.rb に変更する。
以下のようにdeploy出来るようになる。

$ bundle exec cap app1:production deploy

違うサーバーにデプロイするときは同じように config/deploy/app2/production.rb を準備すればオーケー。

$ bundle exec cap app2:production deploy

これで違うサーバーにデプロイ出来る。

これまでと同様にec2サーバーをもう一度用意して2箇所にデプロイ出来るようにする。

ロードバランサー ELB

ロードバランサーをhttpsで運用したいときadmin@hoge.comのようなアドレスへのメールを受信可能のしないといけない。

以下でとりあえずzoneファイルのインポートまでする。
Amazon Route53 ネームサーバへの移行手順(お名前.comからの)

その後にこの記事にあるようにお名前.com側の設定をする。
お名前.com + Route53で独自ドメインのメールをGmailに転送する

メールの転送が出来ていることを確認する。
そして、ELBの指示に従って設定。
下記参考。
【初心者向け】ELBにSSL証明書をインストールする

そこまでしたら
Amazon Route53 ネームサーバへの移行手順(お名前.comからの)
の続きでELBとつなげる。

注意点は、ターゲットグループにトラフィックをルーティングするときのプロトコルをHTTPにすること。これでELBへはHTTPSでアクセス出来、それをアプリケーションではHTTPで扱うことが出来る。

また、以下のように nginxのサーバーネームにドメイン名を追加する必要がある。

server_name hogepoge.com 00.000.00.00 sakamichi-app-elb-000000000.ap-northeast-1.elb.amazonaws.com

CloudFront

Amazon CloudFront + ACM 独自ドメインで HTTPS (SSL) 配信設定メモ

CloudWatchとか

必要の応じて加筆。

続きを読む

Amazon Inspectorを使ってみたメモ

Amazon Inspectorを使ってみた

業務で脆弱性診断ツールの選定を任されたので、実際に使ってみた
(使ってみないと何がいいのかわからんよね)

サマリ

  1. IAMロール作成
  2. EC2インスタンスへのタグつけ
  3. AWSエージェントをインストール
  4. 評価ターゲットの定義
  5. 評価テンプレートの定義
  6. 評価の実行

IAMロールを作成

Amazon InspectorがAWS EC2インスタンスのタグ情報を取得できる必要があるため、
下記権限で作成する

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

EC2インスタンスへのタグつけ

EC2インスタンスに対して適当なタグをつける

AWSエージェントをインストール

脆弱性診断対象のEC2インスタンスに対してSSHし、エージェントをインストールする

[ec2-user@ip-10-0-1-41 ~]$ sudo su -
Last login: Wed Aug  2 22:31:53 JST 2017 on pts/0
[root@ip-10-0-1-41 ~]# cd /usr/src/
[root@ip-10-0-1-41 src]# wget https://d1wk0tztpsntt1.cloudfront.net/linux/latest/install
:
:
install                                            100%[================================================================================================================>]  26.60K  --.-KB/s    in 0s      

2017-08-12 15:40:42 (269 MB/s) - ‘install’ saved [27238/27238]

[root@ip-10-0-1-41 src]# 
[root@ip-10-0-1-41 src]# bash install -u false
Forced update specified as argument is : false
Distribution of the machine is Amazon Linux AMI.
Distribution type of the machine is amzn.
:
:
Current running agent reports version as: 1.0.1041.1
This install script was created to install agent version:1.0.1041.1
In most cases, these version numbers should be the same.
[root@ip-10-0-1-41 src]# 

評価ターゲットの定義

ここで、
1. どのインスタンスに対して診断をするのか(複数可)
2. 対象インスタンスのまとまりに対して名前つけ
を実施する
今回はこんな感じにしてみた
image.png

評価テンプレートの定義

何をもってして評価するのかをここで定義する。具体的には、
1. 評価基準(ルールテンプレートなるもの)
2. 診断時間(15分〜24時間)
3. 上記設定をした評価基準に対しての名前つけ
を実施する。今回はこんな感じにしてみた
image.png

評価の実行

1〜5までの設定が全て完了したら最後は実行するのみ!
image.png
※すでに実行後なため実行ボタンが選択できなくなっています。。。

結果などがでてきたらまた続きをかきたいと思います!

続きを読む

サーバーレスの入門に!自宅サーバーレス+自宅S3環境で作るサーバーレス・サムネイルサーバー!

サーバーレスのHelloWorld = サムネイルサーバー?

日本語で読めるサーバーレスの有名記事といえば,伊藤直也さんの一休の事例だと思います.
下記の記事では,Amazon Lambdaを使って,一休のサムネイルサーバーを構築した事例の紹介をしています.

伊藤直也氏が語る、サーバーレスアーキテクチャの性質を解剖する(前編)。QCon Tokyo 2016
伊藤直也氏が語る、サーバーレスアーキテクチャの性質を解剖する(中編)。QCon Tokyo 2016
伊藤直也氏が語る、サーバーレスアーキテクチャの性質を解剖する(後編)。QCon Tokyo 2016

スケールしやすい.安価にサービスできる.ということでサーバーレス・アーキテクチャは注目が集まっているようです.

そこで,今回,一休での事例をまねて,

1時間で作れる!かんたん自宅Serverless環境! ~はじめてのServerless Application入門~

で作った,Serverless環境のiron-functionsと,

コマンド1つで作れる!かんたん自宅Amazon S3互換環境!

で作った,S3互換環境を合わせて,サーバーレス・サムネイルサーバーを構築したいと思います.

サーバーレス・サムネイルサーバーのシークエンス

image.png

ざっくりとした概要の図です.
1. リクエストから「ファイル名」と「サイズ」を受け取る
2. minioのthumbsのバケットに該当のサイズのサムネイルがあるか?ない場合は3.へ.ある場合は7.へ.
3. minioのimagesのバケットからファイルをダウンロード
4. ダウンロードした画像ファイルをリサイズ
5. リサイズした画像をminioのthumbsのバケットへアップロード
6. リサイズした画像をレスポンスし,終了
7. minioのthumbsのバケットからサムネイルをダウンロード
8. ダウンロードした画像をレスポンスし,終了

API仕様書

(仕様書というほどではないですが・・・)

概要

png画像のサムネイルを提供する.

  • 指定できるサイズは画像の横のサイズのみ.
  • 縦横比は保持する.
  • png以外の画像フォーマットには非対応.
  • テストのため,エラーハンドリングは考慮しない

エントリポイント

/iron_thumbs/thumbs

URLパラメーター

変数 説明
filename imagesにアップロードされたファイル名(key)
size サムネイルの画像の横のサイズ

レスポンス

png画像データ

http://localhost:8000/iron_thumbs/thumbs?filename=test.jpg&size=30

minioのimagesバケットに入っているtest.pngを,画像の横のサイズが30pxになるように縮小し,バイナリデータとして出力する.

事前準備

今回,

  • iron-functions
  • minio

という2つのソフトウェアを使います.
手前みそですが,Seveless環境のiron-functionsは

1時間で作れる!かんたん自宅Serverless環境! ~はじめてのServerless Application入門~

の記事で紹介しています.
また,S3互換環境であるminioに関しては,

コマンド1つで作れる!かんたん自宅Amazon S3互換環境!

の記事で紹介しています.
今回,サムネイルサーバーを構築するにあたって,最低限必要なものとして,

  • docker
  • awscli
  • glide

の3種類です.glideは上記記事では,取り扱っていませんが,go言語でライブラリをインストールするためのパッケージとして今回必要です.
あとgolangの開発環境があるとよいですが,今回,動かすだけであれば必要ないと思います.(おそらく,glideのインストールと同時にgo言語の開発環境も入ると思います)

ソースコード

今回,コードが300行程度あったため,githubのほうに置きました.
https://github.com/kotauchisunsun/iron_thumbs.git

デプロイ

最初にMinio(S3互換サーバー)を前回と同じ設定で8080ポートでサービスします.

設定する変数
Access Key AKIAIOSFODNN7EXAMPLE
Secret Key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# minioを起動
$ sudo docker run --rm -p 8080:9000 -it 
    -e MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
    -e MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    minio/minio server /export

その次に,minioにバケットを作成します.

# minioへimagesバケットの作成
$ aws --endpoint-url http://192.168.0.6:8080 s3 mb s3://images
# minioへthumbsバケットの作成
$ aws --endpoint-url http://192.168.0.6:8080 s3 mb s3://thumbs

iron-functionsを8000ポートでサービスします.

# iron-functionsのサービス
$ sudo docker run --rm -it --name functions -v ${PWD}/data:/app/data 
                   -v /var/run/docker.sock:/var/run/docker.sock 
                   -p 8000:8080 iron/functions

サムネイルサーバーをサービスします.一応Dockerコンテナの流儀のようなものを意識して,各種サービスに必要なパラメーターは環境変数として渡しています.

# ソースコードをクローン
$ git clone https://github.com/kotauchisunsun/iron_thumbs.git
$ cd iron_thumbs
# パッケージの依存関係を解消
$ glide update
$ sudo fn build
$ fn apps create iron_thumbs 
    --config IMAGE_BUCKET=images
    --config THUMB_BUCKET=thumbs
    --config S3_ENDPOINT=http://192.168.0.6:8080
    --config S3_REGION=us-east-1
    --config S3_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
    --config S3_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
$ fn routes create iron_thumbs /thumbs

今回,テストデータとして,いらすとやの画像を使います.それっぽい画像としてクラウドコンピューティングの画像を使います.
image.png

# 素材をダウンロード
$ wget "http://4.bp.blogspot.com/-dVbfTZcofUU/VGX8crT5GiI/AAAAAAAApG4/CB7GF5UmMqE/s400/computer_cloud_system.png"
# minioにファイルをアップロード
$ aws --endpoint-url http://192.168.0.6:8080 s3 cp computer_cloud_system.png s3://images/

実際にiron_thumbsに対してリクエストしてみます.

$ curl "http://localhost:8000/r/iron_thumbs/thumbs?filename=computer_cloud_system.png&size=180" > thumbs.png

これでサムネイルが出来ています!

ベンチマーク

「いらすとや」ベンチ

今回もベンチマークを取ってみました.

どのようなものを想定したかというと,「いらすとや」を自分で運用すると考えました.
image.png

「いらすとや」で画像を検索すると,検索にマッチした画像のサムネイル(赤枠の部分)が表示されます.この画像の横のサイズが約180pxになっています.(実際は少し違います)
このサムネイルをいかに速く作れるのか?をベンチマークしてみたいと思います.

使ったベンチマークソフトは以前も使ったApacheBench.設定は,

$ ab -n 1000 -c 10 "http://localhost:8000/r/iron_thumbs/thumbs?filename=computer_cloud_system.png&size=180"

こんな感じ.
今回,2種類のベンチマークをしました.

always_make_thumbs

これはリクエストごとに常にサムネイルを生成し,レスポンスします(ソースコードを改変してます.)

cached_thumbs

これはthumbsのバケットからサムネイルを取ってきて,レスポンスします.

サムネイルサーバーのシステム設計をする

色々なサムネイルサーバーの話がネットには転がっています.

pixivのサムネイル事情(pixiv)
料理を楽しくする画像配信システム(cookpad)

このように各社各社がしのぎを削ってるようです.
今回,このようなサムネイルサーバーの構成を考えてみました.

image.png

実運用されているサムネイルサーバーは,サムネイルの動的生成だけでなく,CDNが前に立つことがよくあるようです.
理由は主に2つあって,

  1. サムネイルサーバーの負荷軽減
  2. 高速なサムネイルの配布

です.サムネイル作成はCPUへの負荷が高く,サーバーが高負荷になりやすいです.では,サーバーを追加すればよいか?という話になるのですが,お金のないベンチャーはそういうことが出来ません.そのため,出来るだけCDNでサムネイルをキャッシュし,なおかつアクセスのある時は,サーバーレスで短時間動かすことで,安値でスケールしやすいシステムを作ることが出来ます.
実はこの辺の,サムネサーバーの要求要件が,かっちりサーバーレスのアーキテクチャにはまっているため,事例として,よく取りあげられてるのだろうなぁ.と思っています.

なのですが,

ぶっちゃけCDNとサムネサーバーとS3の面倒を見るのはめんどくさいです

キャッシュというのは,高速化には常套手段ではありますが,個人的には,バグを引き起こす原因の上位に来てます.(サーバーレスアーキテクチャだけど,すごくステートフルだし)
実際に,最近,メルカリではCDNの周りで個人流出事件を起こしてます(CDN切り替え作業における、Web版メルカリの個人情報流出の原因につきまして)
そのような理由で,個人的にはキャッシュは出来るだけ,使いたくない気持ちではあります.
しかし,今回考えたサムネサーバーの構成は,先ほどの図にあるように2段階にキャッシュを持っています.

  1. CDN
  2. サムネイル

CDNにサムネイルがなければ,サムネサーバーに問い合わせ,サムネサーバーがサムネイルを今までに作ったことがなかった場合,画像ファイルをダウンロードしてきて,サムネイルを作ります.

そのため,今回は,出来るだけキャッシュを少なくするために,「どこかのサーバーを省略できないかな?」というのが,このベンチの目的です.
そのため,

  • CDNを使わないバージョン=cached_thumbs
  • サムネを動的に作り続けるバージョン=always_make_thumbs

と考えて,ベンチを行ってみました.

結果・考察

項目 平均レスポンス時間[ms]
always_make_thumbs 8417.722
cached_thumbs 7542.938

一度サムネをS3(minio)にキャッシュする方が,1,000[ms]程度速く,7,500[ms]でサムネイルをサービスすることが出来ました

ただ50歩100歩で,普通にWebページを見ていて,7.5秒もサムネの表示に時間がかかってたら使い物にならない感じです.前に検証した,

Serverless環境は600倍以上遅い? ~GoとNode.jsとPythonでベンチマークとってみた!~

の通り,そもそもiron-functionsは遅いです.簡単なGoのサーバーレスのサービスでも6,500[ms]程度かかってました.そこから考えると,サムネ生成からレスポンスまで,8,500[ms]-6,500[ms]なので,2,000[ms],約2秒と考えられます.

あなたのサイトは2秒以内?表示速度がフォームコンバージョンに与える影響

によると,

47%のEコマースユーザーは2秒以内にページが読み込まれることを期待している

だそうです.そのため,サムネの表示に2秒程度かかっていては,やはり問題があります.そのため,前段のCDNのようなキャッシュ機構はまだまだ必要なようです.

ただ一方で

今の世の中だと2秒ぐらいでサムネイルを作れる

という見方もあります.
確かに,2秒というレスポンス時間は遅いです.これに実際にサーバーレスの起動の時間が入ると,もっと時間がかかる.Amazon Lambdaが早いといっても,レスポンスに3秒ぐらいかかるかもしれない.ただ

1人サムネイル生成を待てば,あとはCDNがキャッシュしてくれる.

という世界観だとどうでしょう.割と個人的にはありなんじゃないかなーと.1人が3秒ぐらいサムネイルの生成を待つけども,それ以外の人はCDNが高速にレスポンスしてくれる.
 今回,サムネイルサーバーを作ってみて,「結構めんどくさいなぁ.」とか「S3のバケット2つに依存するなぁ」とか,「これサムネイル側のキーの衝突とかちゃんとハンドリングするの面倒だなぁ」とか,「効率化するためにサムネをS3にキャッシュするのだるいなぁ」とか思いました.そういうことを考えると,コードをクリーンに保ちつつ,運用しやすいインフラを考えると,サーバーレス+CDNの構成で十分で,サムネイル自身をバケットに保持してキャッシュする必要はそうそうないんだなーと思いました.
確かに,

サーバレスにサムネイル画像を配信する試み

という記事の中で,

ユーザに投稿してもらった画像は、オリジナル画像としてS3に保存
オリジナル画像は配信しない (private)
サムネイル画像が要求されたらオリジナル画像から動的変換2 し、CDNで配信
サムネイル画像は保存しない
CDN にヒットしなかったらもう一度動的変換

サムネイル画像は保存しない.という方向性があって,へー.と思ってたんですが,これなら割と落としどころとしてはありなラインなんだなぁ.と検証してみて思いました.

感想

最近,後輩を指導してて,

私「この問題は,Aの可能性があります.ただ一方でBの問題も考えられ,Cの問題も考えられます.別の視点だとDの問題も考えられ・・・」
後輩「その原因のうち,どれを選んで,どれから調べればいいんですか?」
私「

という話をしました.
実際のところ,「問題のある箇所を特定するためにテストを行い」「問題を切り分け」「解決する」というのが一般的だと思います.しかし,全てが全てそういう風に出来るわけではないです.
そんな中で「えいや」と問題を割り切って,テストをしたときに,問題の原因が一発で分かる.みたいなところが「勘」と言ってる所以です.

今回,サムネイルサーバーを作ってみて,この「勘」と呼ばれる部分が働かずに苦労しました.私はGo言語に慣れていないのですが,慣れていると「普通は,はまらないところには,はまらない.」ということが出来ます.しかし,慣れていないと,「普通はこういう実装しないよな・・・」というやり方で実装してしまい,「普通は,はまらないところに,はまる」をしてしまい,問題解決にすごく時間を取られて,頭を悩ませたりしました.

あとiron-functions周りもそうでした.これは純粋にナレッジが少ない面もありますが,公式のドキュメントもなくて,厳しかったです.私はあまりDockerやDocker周りのお作法をあまり知らない面もあるので,その面で「勘」が働かず,毎度毎度厳しい感じになってました.

特に,glideによるライブラリのインストールとiron-functionsの連携部分が厳しく,GoとDockerとiron-functionsの3つの領域の重なった領域だったので,かなり時間がかかってしまいました.

そんなわけで日々組んでおかないといざ作るときになると厳しいなぁ.と思うことはよくあります.やはり日々精進が必要なのだなぁと感じた次第でした.

続きを読む