OpsWorks with Rails 環境でDB Migration問題をなんとかする

はじめに

AWS OpsWorksではChef11を選択すると、Railsスタックをデフォルトで提供しており、エンジニアが自前でChefなど用意しなくてもHeroku感覚でAWSをPaaSとして利用することが出来ます。

  • Herokuほどフルマネージドは要件上出来ないが、インフラはある程度管理したい
  • Chef実行準備が面倒くさい
  • コードのDeploy機能とデプロイ記録とりたい
  • CloudWatch Logs統合 (ログPath設定を画面から出来る)
  • AWSビジネスサポートに入っていると、困っている時に技術的なことを直接チケットを投げて聞ける

こんなメリットたちがあります。正直最後のメリットが一番のような気もしますが・・・

DB Migration問題?

このサービスには致命的な問題があります。なんと複数インスタンスへのデプロイ時に、RailsのDB Migrationと、Unicornの再起動の同期が全く取れていません。 そのため、複数インスタンスを選択した状態で、DB Migrationを実行すると、Migration以外のノードはソース反映後すぐにUnicorn再起動がかかり、高確率で不完全な状態でデプロイされてしまいます。

OpsWorks上でのMigration実行の仕組み

OpsWorks経由で「Migrate YES」を選択してデプロイを行うと rake の db:migrate が実行されます。
https://github.com/aws/opsworks-cookbooks/blob/2209f35fc0e3fd7a75af2e3d0715d425c6a55783/deploy/attributes/deploy.rb#L79

複数ノードがある状態で、「Migrate YES」を選択してデプロイを行う場合、グループ内で任意のノード1個が選択され(基本的にはグループで一番最初のもの)、そのノードに対して内部的にchefの変数に default[:deploy][application][:migrate] = true が設定入り、その後、全ノードに同時にcookbookが実行されます。

デプロイ中は、基本的に全てのインスタンスはそれぞれ勝手にChefが実行されるだけです。そのため、これが原因でマイグレーション同期されない問題がおきています。小さなDBマイグレーションであれば問題なかったりしますが、ちょっとでも時間がかかるようになるとアウトです。規模によりますが、Railsのデプロイを1回実行するのに少なくとも3〜4分くらいかかりますので、ダウンタイム発生となります

不完全な状態でのUnicorn再起動を回避する方法 by AWS

AWSサポート等にもヒアリングした所、以下2点の方針が一般的との回答。

  1. Migration用のレイヤーを作っていただき、先にそちらでMigration込みで一旦ソースを反映して頂く
  2. Chefのデプロイフックがあるので、それを利用して何かしら処理を入れてもらう

うーむ、自前でやらねば・・・・ということで素直に上記を対応します。
(1)に関してはただの運用フロー寄りの問題なので、一旦今回は詳細を割愛します。

デプロイフック実装

OpsWorksはDocument Root配下に deploy ディレクトリ配下に before_symlink.rb (シンボリックリンク張替え直前) で以下のチェック入れます。

Chef::Log.info("Running deploy/before_symlink.rb")
current_release = release_path
env = node[:deploy][:app_rails][:rails_env]

execute "rake aws:verify_migration" do
  cwd current_release
  command "bundle exec rake aws:verify_migration"
  environment new_resource.environment.merge({ "RAILS_ENV" => env })
end

Rakeタスクを適当に作ります。

namespace :aws do
  task verify_migration: :environment do
    raise StandardError if 
  ActiveRecord::Migrator.needs_migration?
  end
end

これでとりあえず、不完全な状態でUnicornが再起動される問題だけ回避しました。before_symlinkの段階で、verify_migrationを実行し、マイグレーションが残っている状態の時は対象インスタンスの反映を途中中断させるようにします。「カラムの削除の時はどうするのか」みたいな懸念はありますが、一旦これだけでも入れておけば大きい所はカバーできそうです。

最後に

そもそもChefは諦めて、コンテナ運用(GKE + Spinnaker) に早く移行したほうがいいかもしれない

続きを読む

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ドキュメントが返される)の判定を厳密にしたりなどが必要となるでしょう。

続きを読む

新たにEC2インスタンスを立ち上げてデプロイするまでの順序

AWSのインスタンスを立ち上げるなんて毎日やるわけでもないので、やるたびに忘れてしまいますよね。ちなみに今のいままでほぼ完全に忘れていました。以前一回やったことがあったのですが、数ヶ月前だし、一回しかやったことないし。

そこで、備忘録的にメモしておこうかと。

登場人物

  • ruby on Rails
  • capistrano
  • AWS
  • itamae

手順をば

大まかには以下の通り。
1. インスタンスをつくる
2. itamaeを新たに作成したインスタンスに向けて実行する
3. capデプロイコマンドを実行する
4. EC2インスタンスのドメインを変更する(Route 53)

1. インスタンスをつくる

詳細なインスタンスの各種設定は他のQiita記事やドキュメントに譲ります。
が、新規でインスタンスをつくる際にいくつか注意すべき点があるのでそれらを特記しておきます。

pemファイルをダウンロードする

コレはあとでEC2インスタンスにssh接続するときに必要なので、消さないように作業ディレクトリにでも一時的に保管しておきます。が、itamaeなどで管理している場合は必要ないかもしれません。下記に記載の通り、GitHubで公開鍵を設定しておけば、それ経由でssh接続できるためです。

公開鍵を用意しておく

詳細は以下のリンクなどを参照してください。EC2インスタンスにパスなしでコマンド入力して入れるようにするために必要です。プロジェクトメンバー全員で共有するなら、シートか何かにまとめておくと良いかも。
4.3 Git サーバー – SSH 公開鍵の作成

2. 新たに作成したインスタンスに向けてitamaeを実行する

itamaeの作り方は他の記事に譲ります。
node/以下に環境ごとの設定ファイル(production.ymlstaging.ymlなど)があると思うので、下記コマンドを実行する時に-yオプションで指定します。

// --dry-runで問題なく通るかをまず確認する
$ bundle exec itamae ssh -u ec2-user -h your_ip_address -y node/staging.yml --dry-run sabaku.rb

// 問題ないことを確認したら実行する
$ bundle exec itamae ssh -u ec2-user -h your_ip_address -y node/staging.yml sabaku.rb

3. capのデプロイコマンドを実行する

インスタンスにもろもろ環境が出来たら、今度はGitHubの作業ブランチを指定してデプロイします。
作業ディレクトリ/deploy/以下に環境ごとの設定ファイル(下記コマンドを叩く場合はfugafuga.rbが必要)があるので、下記コマンド実行時に指定するのを忘れずに。

// この場合はfugafuga.rbを元にデプロイ
$ BRANCH=hogehoge bundle exec cap fugafuga deploy

4. EC2インスタンスのドメインを変更する(Route 53)

デプロイが終わったら、パブリックIPを直打ちしてサイトに行くことはできますが、なんだがいちいちIP書くのめんどくさい。
そこでAWSコンソールに行ってRoute 53というサービスを使います。
ここで、取得したドメインを登録しておけば、そのサブドメインなんかを気軽に登録することができるようになります。
下記に簡単な手順(といっても2点しかないですが)を記載しておきます。

Create Record Setボタンで新しいレコードセットを作成する

スクリーンショット 2017-08-18 14.36.42.png

レコードセットの設定を行う

Nameの部分の任意のサブドメイン名を入力します。TypeにはDNSレコードの種類を選択します。
Aliasの部分なんですが、Aliasを張らない場合はValueに直にインスタンスのパブリックIPを記入してしまうのが、一番手っ取り早くて簡単かと思います。
スクリーンショット 2017 08 18 14.36.55.png

よし、これで一件落着。
自分で設定したhoge.mydomain.comみたいなURLを叩けば、IP直打ちしたときみたいにサイトにアクセスできるようになります。
これで、次回インスタンスを立てることになっても、大体大丈夫でしょう。

続きを読む

aws-sam-localだって!?これは試さざるを得ない!

2017-08-11にaws-sam-localのベータ版がリリースされました。
単に「早速試したで!」と言う記事を書いても良かったのですが、少し趣向を変えてサーバレス界隈の開発環境のこれまでの推移を語った上で、aws-sam-localの使ってみた感想もお話しようかと思います。

サーバレス開発環境の今昔

サーバレス自体がかなり最近になって生まれた風潮なので昔とか言うのも問題はあるかと思いますが、とにかくサーバレスなるものの開発環境について私にわかる範囲でお話しようと思います。誤りや不正確な点については編集リクエストやコメントを頂けると幸いです。

なお、サーバレスという言葉は一般的な名詞ですが、私がAWS上でしかサーバレスに触れていないため、AzureやGCPなどには触れず、もっぱらLambdaの話になってしまうことをあらかじめご了承ください。

Lambdaのデプロイは辛かった

Lambdaの基本的なデプロイ方法はZIPで固めてアップロードです。
直接ZIPをLambdaに送るか、あらかじめS3に置いておいてLambdaにはそのURLを教えるかといった選択肢はありましたが、手動でZIPに固めてAWSに送るという手順は不可避でした。なんかもうすでに辛い。

さらに言うとLambdaに送りつけられるのはLambdaで実行するコードだけ。性質上Lambdaは単体で使われることはほとんどなく、他のサービスと連携することがほとんどなのにその辺は自分で管理するしかありませんでした。辛い。

CloudFormationで管理することは可能でしたが、CloudFormationテンプレートを書くのがかなりダルいことと、CloudFormationの更新とZIPのアップロードを別途行う必要があって手順が煩雑化しやすいため、「もうええわ」と手動管理してることが多かったと思われます。

また、ローカル環境で実行するには一工夫必要でした。

颯爽登場!Serverlessフレームワーク

そんな時に颯爽と現れたのがServerlessフレームワークでした。
ServerlessフレームワークにおいてはLambdaファンクション及び関連するリソースを独自のyamlファイルで管理します。結局は一度CloudFormationテンプレートに変換されるのですが、CloudFormationテンプレートよりも単純な形式で記述できたのが流行った一因かと思います。また、sls deployコマンドでLambdaのコードのアップロードおCloudFormationスタックの更新を一括で行ってくれたため、デプロイの手順は従来よりもはるかに簡略化されたかと思われます。

Lambdaテストしづらい問題

デプロイに関する問題はServerlessフレームワークや、ほぼ同時期に現れたSAMによって改善されましたが、開発プロセスにおいて大きな課題がありました。

テストし辛ぇ…

上記の通りLambdaは性質上他のサービスと連携することが多いため、その辺をローカル環境でどうテストするかに多くの開発者が頭を抱えました。対策として

  1. モッククラスを作って、実際のサービスのような振る舞いをさせる
  2. プロダクションとは別のリージョンに環境を再現して、そこで実行する

といった方法がありましたが、それぞれ

  1. モッククラスの実装がすこぶるダルい 下手したらロジック本体より時間かかる
  2. クラウドにデプロイしないとテストできないため、時間がかかる

といったデメリットがありました。

LocalStackとaws-sam-local

サーバレス開発者の嘆きを聞いたAtlassianがローカル環境でAWSのサービスのエンドポイントを再現するなんとも素敵なツールを作り上げました。それがLocalStackです。
再現されているサービスの数が物足りなく感じられたり、サードパーティ製であることに一抹の不安を覚えたりする人もいるかと思いますが、これ以上を求めるなら自分で作るぐらいしかないのが現状かと思います。

そしてaws-sam-local。こちらはLocalStackと少し趣が異なります。LocalStackが連携するサービスのエンドポイントを再現して提供するのに対して、aws-sam-localは実行環境の提供という意味合いが強いです。そして重要なことはAWSの公式がサポートしているということです。公式がサポートしているということです。大事なことなので(ry
「実行するのはローカルでNode.jsなりPythonなりで動かせばええやん」と思いがちですが、ランタイムのバージョンなどを本番環境と確実に揃えられるのは大きな利点です。
まだベータ版が出たばっかなので今後に期待といったところでしょう

aws-sam-local触ってみた

それでは実際に触ってみましょう。
ちなみに当方環境は

  • OS: macOS Sierra 10.12.6
  • Docker for Mac: 17.06.0-ce-mac19

です。

事前準備とインストール

公式のInstallationの項目を読み進めますが、事前にDockerを使えるようにしておく必要があります。

Macだったら普通にDocker For Macをインストールしておけば問題ありません。
一方Windowsだと

スクリーンショット 2017-08-16 14.48.18.png

まさかのDocker Toolbox推奨。 Docker For Windowsェ…
そしてaws-sam-localのインストールですが、私は-gオプション排斥論者なのでローカルインストールします

npm install aws-sam-local

実装

今回はこちらを参考にAPIゲートウェイから呼び出すLambdaを実装します。
ほぼ丸パクリですが一部アレンジしてますのでソースものっけます。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM Local test
Resources:
  HelloWorld:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: nodejs6.10
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: put

ランタイムをnodejs6.10に変更してます。
新しく作る場合にわざわざ古いバージョンを使う必要もありませんので。

余談ですが、WebStormのCloudFormation用のプラグインは今の所SAMには対応してないのか、Type: AWS::Serverless::Functionのところにめっちゃ赤線を引かれます。

index.js
/**
 * Created by yuhomiki on 2017/08/16.
 */

"use strict";

const os = require("os");
console.log("Loading function");


const handler = (event, context, callback) => {
  return callback(null, {
    statusCode: 200,
    headers: { "x-custom-header" : "my custom header value" },
    body: "Hello " + event.body + os.EOL
  });
};

exports.handler = handler;

完全に書き方の趣味の問題です。
内容は参考ページのものと全く同じです。

package.json
{
  "name": "sam_test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "invoke-local": "sam local invoke HelloWorld -e event.json",
    "validate": "sam validate",
    "api-local": "sam local start-api"
  },
  "author": "Mic-U",
  "license": "MIT",
  "dependencies": {
    "aws-sam-local": "^0.1.0"
  }
}

aws-sam-localをローカルインストールしているので、package.jsonのscriptsに追記しています。

実行

それでは実行してみましょう

上記のpackage.jsonに記載した

  • invoke-local
  • validate
  • api-local

を実行していきます。

invoke-local

Lambdaファンクションをローカル環境で実行します。
Lambdaファンクションに渡すevent変数はワンライナーで定義することも可能ですが、あらかじめJSONファイルを作っといた方が取り回しがいいです。

json.event.json
{
  "body": "MIC"
}

実行結果はこんな感じ

スクリーンショット 2017-08-16 15.25.42.png

まず最初にdocker pullしてランタイムに応じたDockerイメージをダウンロードします。
その後はコンテナ内でLambdaファンクションを実行し、最後にcallbackに与えた引数を出力といった流れです。
ログの形式がすごくLambdaですね。あとタイムゾーンもUTCになっていますね。
メモリの使用量をローカルで確認できるのは嬉しいですね。

-dオプションをつけることでデバッグもできるようです。
公式のgithubにはご丁寧にVSCodeでデバッグしてる様子がgifで上げられてます。

validate

テンプレートファイルのチェックをします。
デフォルトではカレントディレクトリのtemplate.yamlファイルをチェックしますが、-tオプションで変更することが可能です。

失敗するとこんな感じに怒られます。

スクリーンショット 2017-08-16 15.33.47.png

成功した時は「Valid!」とだけ言ってきます。きっと必要以上に他人に関わりたくないタイプなのでしょう。

api-local

sam local start-apiコマンドはローカル環境にAPIサーバを立ち上げます。
ホットリロード機能がついてるので、立ち上げっぱなしでもソースを修正したら自動で反映されます。いい感じですね。

スクリーンショット 2017-08-16 15.40.58.png

立ち上がるとこんなメッセージが出るので、あとはCURLなりPostManなりで煮るなり焼くなり好きにしましょう。

CURLの結果はこんな感じ
スクリーンショット 2017-08-16 15.51.39.png

所感

Lambdaのローカル実行環境を公式が用意したことに大きな意義があるかと思います。
Dockerさえあればすぐに使えることと、SAMテンプレートを書かざるをえないのでInfrastructure as Codeが自然と根付いていくのも個人的には好感を持てます。

ただし、まだベータ版なこともあって機能的にもの足りない部分があるのも事実です。
具体的にはやはりDynamoDBとかもテンプレートから読み取ってDockerコンテナで用意してくれたらなーと思います。LocalStackやDynamoDB Localでできないこともないでしょうが、DynamoDB Localに関してはテンプレートからテーブル作ったりするの多分無理なのでマイグレーション用のコードを書くことになりますし、LocalStackに関しては実はあまり真面目に使ったことないのでわかりませんが環境構築に一手間かかりそう。ていうかできれば一つのツールで完結させたい。

SAMしかりaws-sam-localしかり、AWS側としてもより開発がしやすくなるような環境づくりをしていくような姿勢を見せているので、今後のアップデートに期待したいところですね。

続きを読む

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… 続きを読む

クラウドの力を借りて無限収入システムを構築する(はずだった)

目的

 ビットコイン等の仮想通貨の採掘をして不労所得チャリンチャリンというのは夢がありますよね。私もチャレンジしようと思ったのですが、手元の貧相な PC では電気代すらペイできないのは明らかです。
 ではクラウドの力を借りて、GPU でやってみたらどうなのか。スポット価格をうまく使えば、もしかしてクラウド利用料を賄えて夢の無限収入システムを構築できるのではないか、というチャレンジをしてみたので、その記録です。
 なお、実際に稼げるとは思ってなくて、AWS スポットインスタンス使ってみる、GPUインスタンスを使ってみる、ブロックチェーンに触れてみる、など一石三鳥なお勉強をすることが目的です。

概念図

 インスタンス利用料が安いときに仮想通貨をマイニングすれば黒字転嫁するのでは、というアイデアです。
image.png

結果

 すごい赤字になる。スポットインスタンスを活用しても インスタンス利用料の半分まかなえる程度の採掘量にとどまり利益が生まれることはなかった、通貨マイナーの世界はきびしい。
 FPGA 使えれば結果は変わるのかなー。

構築の流れ

採掘環境構築~試しに採掘まで

環境情報

  • 採掘対象

    • Ethereum
  • OS 1
    • Ubuntu Linux 16.04 / 64bit
  • インスタンスタイプ
    • g3.4xlarge or p2.xlarge
  • リージョン
    • us-east-1(インスタンス費用がたぶんいちばん安いので)
  • マイナークライアント
    • Claymore’s Dual GPU Miner 9.7
  • ウォレット
    • coincheck

ウォレットの準備

  • Coincheck にアカウント作成

    • その他、大手のbitFlyer、Zaif 等なんでもよさそうです、手数料や取り扱い通貨が違います
  • 身分証明書を送り取引可能な状態にする
    • 最大2営業日必要との記載ですが、今回は数時間で本人確認が完了しました
  • Ether入金用のアドレスを作成 する

    • アドレス:0x0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X

マイニング用AMIを作成する

Ubuntu インスタンスを作成、Calymore9.7 を導入する。

console
$ wget https://github.com/nanopool/Claymore-Dual-Miner/releases/download/v9.7/Claymore.s.Dual.Ethereum.Decred_Siacoin_Lbry_Pascal.AMD.NVIDIA.GPU.Miner.v9.7.-.LINUX.tar.gz
$ mkdir Calymore9.7
$ tar xvzf Claymore.s.Dual.Ethereum.Decred_Siacoin_Lbry_Pascal.AMD.NVIDIA.GPU.Miner.v9.7.-.LINUX.tar.gz -C ./Calymore9.7/
$ cd Calymore9.7/

不足しているライブラリをインストール

console
sudo apt-get install ocl-icd-opencl-dev
sudo apt-get install libcurl3

nvidiaのドライバをインストール

console
sudo apt-get install -y nvidia-367

以下のshを作成

Claymore9.7/miningStarter.sh
#!/bin/sh

workername1=`hostname`
workername2=`date +"%y%m%d%H%M%S"`

#export GPU_FORCE_64BIT_PTR=0 # must be comment out for amdgpu-pro
export GPU_MAX_HEAP_SIZE=100
export GPU_USE_SYNC_OBJECTS=1
export GPU_MAX_ALLOC_PERCENT=100
export GPU_SINGLE_ALLOC_PERCENT=100

export ETH_ADDR=0x0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X0X # Ether入金アドレス
export ETH_WORKER_NAME=$workername1$workername2 # ワーカーの名前=ホスト名+起動日時
export ETH_POOL_HOST=us1.ethermine.org:4444 #AWSリージョンと同じusを指定

cd /home/ubuntu/Claymore9.7/ #SHおいたとこ

./ethdcrminer64 
  -epool $ETH_POOL_HOST 
  -ewal $ETH_ADDR.$ETH_WORKER_NAME 
  -epsw x 
  -mode 0 
  -ftime 10 
  -etha 2 
  -allpools 1 
  -wd 0 
  -eres 4 
  -gser 2

で、sh 起動、なんかうごく。

console
sudo bash miningStarter.sh

image.png

こちらで Ethereum入金アドレス で検索すると、進捗が確認できる。
ethermine.org – The fastest way to mine Ether

ためしに採掘&オンデマンドでの結果

数時間後…
image.png

ちゃんと掘れてるみたいですが、いくらになったんでしょう…

  • AWS利用料(g3.xlarge@us-east-1)

    • $1.14 / H = $27.4 / Day
  • マイニング結果 23
    • 0.002659 ETH/Day (10 MH/s) = $0.8 / Day
  • 収支
    • -98%($-26.56/Day)の赤字

 オンデマンドインスタンス価格では赤字を垂れ流す結果となりました。自動起動設定をしたあと、AMI をとっておきます。

/etc/rc.local
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

bash /home/ubuntu/Claymore9.7/miningStarter.sh #SHのありか

exit 0
  • AMI-ID

    • ami-0X0X0X0X

スポットインスタンスで起動まで

 バージニア北部のここ一週間のスポットインスタンスの価格設定履歴より、スポットの最安値とオンデマンド比の割引率を求めました。4

インスタンスタイプ オンデマンド($/H) スポット最安($/H) 割引率
g3.4xlarge 1.14 0.23 80%
g3.4xlarge 2.28 0.4 82%
g3.4xlarge 4.56 1.04 77%
p2.xlarge 0.9 0.1 89%
p2.8xlarge 7.2 1 86%
p2.16xlarge 14.4 2.2 85%

 g3とp2はどちらがマイニングに適しているのか計測しないとわかりませんが、とりあえず「p2.xlarge」でいいかなと感覚で決め、自動入札とAutoScaleの設定をします。この割引率を見る限り、負けが見えていますが。。。

  • 起動設定:MiningLaunchConfig

    • インスタンスタイプ

      • p2.xlarge
    • スポット価格
      • $ 0.1
    • AMI ID
      • ami-0X0X0X0X
  • AutoScalingGroup設定:MiningAutoScaleGroup

    • 起動設定

      • MiningLaunchConfig
    • 希望
      • 3
    • 最小
      • 3
    • 最大
      • 3

 これで、スポットインスタンスが 0.1 以下の場合自動で入札し、その価格帯であれば常時3台 起動する設定ができました。

勝手に起動する!
image.png

勝手に採掘する!
image.png

無限にお金が、、、!!

最終結果

  • AWS利用料(p2.xlarge@us-east-1)

    • $0.098 / H = $2.35 / Day
  • マイニング結果(1台あたり)
    • 0.003244 ETH/Day (12.2 MH/s) = $ 0.97 / Day
  • 収支
    • -59%($-1.38/Day)の赤字

 無限にお金が減っていく!
 残念ながら、スポットインスタンスでも無限の収入は実現できませんでした。もっと値下げしてくれれば黒字転換するんですが、そんなうまい話ないですよね。
 無念!!

参考リンク

イーサリアムを効率良くマイニングできるClaymore’s Dual Minerの使い方・設定 | トレードステーションと株・FX自動売買で暮らす
技術者向け Ethereumの基礎知識 (イーサリアム、エセリウム) – Qiita
【2017年度版】イーサリアム(ETH)でお金を稼ごう! ~マイニング編~ | デジモノ達人
イーサリウムを GPU マイニングしてみる(2017/04/21 時点) – Qiita
GPU関連でよく使うコマンドまとめ – Qiita
ETH-USD電卓:ETH/USD Ethereum Price Calculator | CoinGecko
Mining収支電卓:Ethereum Mining Profitability Calculator
採掘量確認:Balances – ethermine.org – The fastest way to mine Ether

構築時エラー集

ocl-icd-opencl-dev ないとエラー

ethdcrminer64
./ethdcrminer64: error while loading shared libraries: libOpenCL.so.1: cannot open shared object file: No such file or directory

libcurl3 ないとエラー

ethdcrminer64
./ethdcrminer64: error while loading shared libraries: libcurl.so.4: cannot open shared object file: No such file or directory

nvidiaのドライバ ないとエラー

ethdcrminer64
ETH: 1 pool is specified
Main Ethereum pool is asia1.ethermine.org:4444
DCR: 4 pools are specified
Main Decred pool is pasc-eu2.nanopool.org:15555
AMD OpenCL platform not found 
No NVIDIA CUDA GPUs detected.
No AMD OPENCL or NVIDIA CUDA GPUs found, exit

補足


  1. GPU は Windows に最適化されているものが多く、マイナーの世界では Windows を使うのがメジャーみたいです 

  2. 1 ETH = $ 299.96 で計算しています 

  3. 数時間しか稼働させていないので、インスタンスタイプにおける MH/s は正しい平均値が取れていません 

  4. 2017/8 時点 

続きを読む

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とか

必要の応じて加筆。

続きを読む

Cognito User Poolsのサインアップ・サインイン画面をiOSで試してみた

Coginit User Poolsにフェデレーション等の新機能が追加されましたが
新機能のサインアップ・サインイン画面をiOSから試してみました。
https://aws.amazon.com/jp/about-aws/whats-new/2017/08/amazon-cognito-launches-general-availability-of-a-built-in-customizable-user-experience-for-sign-in-oauth-2-0-support-and-federation-with-facebook-login-with-amazon-google-and-saml-providers-for-user-pools/

環境

  • xcode
  • aws sdk

作業概要

iOSからCognitoでサインアップ(サインイン)後、払い出されたCredentialの確認のためPollyに喋らせてみたいと思います。

  1. UserPoolを作成する
  2. Identity Poolを作成する
  3. xcodeでコードを書く
  4. Polly実行用にIAM Roleを設定する
  5. Simulatorで実行する

1. UserPoolを作成する

  1. AWSマネジメントコンソールにログインして、Cognito User Poolsを選択する
  2. Pool名(MyPool)を指定して、User Poolを作成する(全てデフォルト設定)
  3. App Clientを追加する
    1. [General settings]-[App Clients]を選択する
    2. Client名(MyApp)を指定して、Clientを追加する(全てデフォルト設定)
  4. App Integrationを設定する
    1. App client settingsを以下のように指定
      AppClientSettings.png
    2. Domain Nameを以下のように指定
      DomainName.png

attention.png

2. Identity Poolを作成する

  1. Federated Identitiesを選択し、User Pools画面から切替
  2. Identity Pool name(MyAppIdentity)とUser Pool IDとApp client idを指定してIdentity Poolを作成
    IdentityPool.png

3. xcodeでコードを書く

参考URL
https://github.com/awslabs/aws-sdk-ios-samples/tree/master/CognitoAuth-Sample
http://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-integrating-user-pools-with-identity-pools.html

Info.plist(抜粋)
<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>amazonaws.com</key>
            <dict>
                <key>NSThirdPartyExceptionMinimumTLSVersion</key>
                <string>TLSv1.0</string>
                <key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
            </dict>
            <key>amazonaws.com.cn</key>
            <dict>
                <key>NSThirdPartyExceptionMinimumTLSVersion</key>
                <string>TLSv1.0</string>
                <key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
            </dict>
        </dict>
    </dict>
    <key>AWS</key>
    <dict>
        <key>CognitoUserPool</key>
        <dict>
            <key>Default</key>
            <dict>
                <key>CognitoUserPoolAppClientId</key>
                <string>****************************</string>
                <key>CognitoUserPoolAppClientSecret</key>
                <string>****************************</string>
                <key>CognitoAuthWebDomain</key>
                <string>https://myapp.auth.us-east-1.amazoncognito.com</string>
                <key>CognitoAuthSignInRedirectUri</key>
                <string>myapp://top</string>
                <key>CognitoAuthSignOutRedirectUri</key>
                <string>myapp://top</string>
                <key>CognitoAuthScopes</key>
                <array>
                    <string>openid</string>
                </array>
            </dict>
        </dict>
    </dict>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>myapp</string>
            </array>
            <key>CFBundleURLName</key>
            <string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
        </dict>
    </array>
AppDelegate.swift(抜粋)
    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        return AWSCognitoAuth.default().application(app, open: url, options: options)
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        let clientId = "App Client IDを指定"
        let clientSecret = "App Client Secretを指定"
        let poolId = "User Pool IDを指定"
        let identityPoolId = "Identity Pool IDを指定"

        let serviceConfiguration = AWSServiceConfiguration(
            region: .USEast1, // リージョンを指定(ここではバージニア)
            credentialsProvider: nil)

        let userPoolConfiguration = AWSCognitoIdentityUserPoolConfiguration(
            clientId: clientId,
            clientSecret: clientSecret,
            poolId: poolId)

        AWSCognitoIdentityUserPool.register(
            with: serviceConfiguration,
            userPoolConfiguration: userPoolConfiguration,
            forKey: "UserPool")

        let pool = AWSCognitoIdentityUserPool(forKey: "UserPool")

        let credentialsProvider = AWSCognitoCredentialsProvider(
            regionType: .USEast1, // リージョンを指定(ここではバージニア)
            identityPoolId: identityPoolId,
            identityProviderManager:pool)

        let configuration = AWSServiceConfiguration(
            region: .USEast1,  // リージョンを指定(ここではバージニア)
            credentialsProvider: credentialsProvider
        )
        AWSServiceManager.default().defaultServiceConfiguration = configuration

        return true
    }

ログインボタンとログアウトボタンを配置する

ViewController.swift
import UIKit
import AWSCognitoAuth
import AWSCognitoIdentityProvider
import AVFoundation
import AWSPolly

class ViewController: UIViewController, AWSCognitoAuthDelegate {
    var audioPlayer = AVPlayer()
    let cognitoAuth = AWSCognitoAuth.default()

    override func viewDidLoad() {
        super.viewDidLoad()
        cognitoAuth.delegate = self as AWSCognitoAuthDelegate
    }

    func getViewController() -> UIViewController {
        return self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func LoginButtonAction(_ sender: Any) {
        cognitoAuth.getSession(self)  { (session:AWSCognitoAuthUserSession?, error:Error?) in
            if(error != nil) {
                print((error! as NSError).userInfo["error"] as? String)
            }else {
                let message = "<speak>Hello World!</speak>"
                self.talkmessage(message: message)
                print("login")
            }
        }
    }

    @IBAction func LogoutButtonAction(_ sender: Any) {
        cognitoAuth.signOut { (error:Error?) in
            if(error != nil) {
                print((error! as NSError).userInfo["error"] as? String)
            }else {
                print("logout")
            }
        }
    }

    func talkmessage(message:String) {
        let input = AWSPollySynthesizeSpeechURLBuilderRequest()

        input.textType = AWSPollyTextType.ssml
        input.text = message
        input.outputFormat = AWSPollyOutputFormat.mp3
        input.voiceId = AWSPollyVoiceId.mizuki

        let builder = AWSPollySynthesizeSpeechURLBuilder.default().getPreSignedURL(input)

        builder.continueOnSuccessWith(block: { (awsTask: AWSTask<NSURL>) -> Any? in
            let url = awsTask.result!
            self.audioPlayer.replaceCurrentItem(with: AVPlayerItem(url: url as URL))
            self.audioPlayer.play()

            return nil
        })
    }
}

4. Polly実行用にIAM Roleを設定する

  1. AWSマネジメントコンソールにログインして、IAM – Rolesを選択する
  2. 認証されたユーザー用ロール(ここではCognito_MyAppIdentityAuth_Role)に、AmazonPollyFullAccessポリシーをアタッチする

5. Simulatorで実行する

Loginボタンをクリックする
top.png

サインアップ画面とサインイン画面

  1. サインアップ画面
    signup.png

  2. パスワード入力中の画面
    signup_pass.png

  3. Verify画面
    verify.png

  4. サインイン画面
    signin.png

  5. サインイン画面(背景色変更・ロゴ表示)
    modify.png

雑感

今回の機能拡張で、Cognitoは一段と使いやすくなったと思います。

続きを読む