RddshiftからS3へUNLOADする

S3へ書き出したDynamoDBデータをRedshiftへコピーする [Copyコマンドについて]の逆をする。

UNLOADコマンドを使用する際の個人的な確認用まとめ

構文(抜粋)

UNLOAD ('select-statement')
TO 's3://object-path/name-prefix'
authorization
[ option [ ... ] ]

where option is

{ MANIFEST
| DELIMITER [ AS ] 'delimiter-char' 
| FIXEDWIDTH [ AS ] 'fixedwidth-spec' }  
| ENCRYPTED
| BZIP2  
| GZIP     
| ADDQUOTES 
| NULL [ AS ] 'null-string'
| ESCAPE
| ALLOWOVERWRITE
| PARALLEL [ { ON | TRUE } | { OFF | FALSE } ]
[ MAXFILESIZE [AS] max-size [ MB | GB ] ]

相変わらずわかりにくい構文っすよね。。。

簡単なサンプル

sample
UNLOAD ('SELECT * FROM test;')
TO 's3://mybucket_name/target_dir/'
CREDENTIALS 'aws_access_key_id=<キー>;aws_secret_access_key=<シークレットキー>'
MANIFEST
DELIMITER AS 't'
GZIP
ALLOWOVERWRITE

UNLOAD (‘select-statement’)

SQL文を書く。途中で '' を使用したい場合はちゃんとエスケープすること ''

TO ‘s3://object-path/name-prefix’

出力先のS3を指定する。
object-path 配下に name-prefixXXXXXXXXXXXX で作成される。

CREDENTIALS

COPYにも書いたけどIAMなりクレデンシャルキーなり指定する

オプション指定

一覧
MANIFEST
DELIMITER [ AS ] 'delimiter-char' 
FIXEDWIDTH [ AS ] 'fixedwidth-spec' }  
ENCRYPTED
BZIP2  
GZIP     
ADDQUOTES 
NULL [ AS ] 'null-string'
ESCAPE
ALLOWOVERWRITE
PARALLEL [ { ON | TRUE } | { OFF | FALSE } ]
MAXFILESIZE [AS] max-size [ MB | GB ] ]

幾つかかいつまんで

MANIFEST

UNLOAD プロセスによって作成されたデータファイルを明示的にリストするマニフェストファイルを作成します。

DELIMITER

パイプ文字 (|)、カンマ (,)、タブ (t) など、出力ファイル内のフィールドを分離する単一の ASCII 文字。デフォルトの区切り文字はパイプ文字です。

BZIP2/GZIP

出力時に圧縮して出力する

ADDQUOTES

アンロードされた各データフィールドは引用符で囲まれるため、Amazon Redshift は区切り文字自体を含んでいるデータ値をアンロードすることができます。

出力データに区切り文字が含まれている場合、カラム毎に ”” で囲んで出力してくれる機能
読み込み時の使い方に寄る。

NULL AS ‘null-string’

NULL文字の置き換え

ALLOWOVERWRITE

デフォルトでは、UNLOAD によってファイルの上書きが発生する可能性がある場合、その UNLOAD 操作は失敗します。ALLOWOVERWRITE が指定された場合、UNLOAD によって、マニフェストファイルを含めた既存のファイルが上書きされます。

ファイルが存在すると上書き禁止でエラー終了する。
これを指定すると上書きする。

MAXFILESIZE AS 最大サイズ [ MB | GB ]

Amazon S3 で作成された UNLOAD ファイルの最大サイズ。5 MB ~ 6.2 GB の十進値を指定します。AS キーワードはオプションです。デフォルト単位は MB です。MAXFILESIZE を指定しない場合、デフォルトの最大ファイルサイズは 6.2 GB です。

マニフェストファイルが使用されている場合、このサイズは MAXFILESIZE に影響されません。

LIMIT

SELECT クエリは、外部の SELECT で LIMIT 句を使用することはできません。その代わり、ネスティングされた LIMIT 句を使用してください。

sample
SELECT 
 * 
FROM 
 test 
WHERE id IN (
  SELECT 
   id
  FROM 
   test
  LIMIT 100
);

こんな感じにしろとのこと。

参考

UNLOAD

続きを読む

AWS linux PHP apache mysql wordpress 環境構築

Step1
EC2インスタンス作成

1.png

Step2
TaraTerm使って、linuxサーバ接続する

2.jpg

Step3
yum 更新
$ sudo yum -y update

4.jpg
5.jpg

Step4
Apache2.4インストール

//バージョン確認
$ sudo yum list available | grep httpd

8.jpg

//httpd24インストールする
$ sudo yum -y install httpd24

6.jpg
7.jpg

//結果確認
$ sudo yum list installed | grep httpd24

9.jpg

Step5
PHP7.0インストールする

//バージョン確認
$ sudo yum list available | grep php70
//インストールする
$ sudo yum -y install php70 php70-mbstring php70-pdo
//結果確認
$ sudo yum list installed | grep php70

10.jpg
11.jpg
12.jpg

Step6
mysqlインストールする

//mysql バージョン確認
//mysql インストールする
//結果確認
$ sudo yum list available | grep mysql57
$ sudo yum install mysql
$ sudo yum list installed | grep mysql

20.jpg21.jpg
22.jpg

Step7
apache配置

etc/httpd/con/httpd.confを編集し、以下の2行を追加
(編集する前はhttpd.confバックアップする)
AddType application/x-httpd-php .html .htm .php .phtml
AddType application/x-httpd-php-source .html .htm .phps

・httpd.confバックアップする
$ sudo cp httpd.conf httpd.conf.bak

23.jpg

・httpd.conf編集する
$ vim httpd.conf

24.jpg

・AddTypeのところで下記二行追加
AddType application/x-httpd-php .html .htm .php .phtml
AddType application/x-httpd-php-source .html .htm .phps

25.jpg

・httpd restart
$ sudo service httpd restart

26.jpg

・確認する
var/www/htmlの下でindex.php を作成、下記のコードを追加
<?php
phpinfo();
?>
ブローザにIP入力、下記の画面出ます

28.jpg

Step8
mysql配置

・mysql起動する時、エラー発生しました。
改正:$ sudo yum install mysql57-server

29.jpg

・mysql 起動する
$ service mysqld start

30.jpg

・mysql 登録する

31.jpg

・ユーザ追加
ユーザ名:mysql
パスワード:mysql
mysql> CREATE USER ‘mysql’@’localhost’ IDENTIFIED BY ‘mysql’;

32.jpg
33.jpg

Step9
wordpess配置

・下記のurlから日本語wordpessダウンロードする
https://ja.wordpress.org/install/

var/www/html下に
$ wget https://ja.wordpress.org/wordpress-4.8.3-ja.zip

36.jpg

・解凍する

37.jpg

38.jpg

・エラー発生した
39.jpg

・原因:PHPにmysqlサポート追加されない。

・改正:
$ sudo yum -y install php70-mysqlnd
/etc/php.ini に extension=msql.so 追加

40.jpg
41.jpg

・設定画面が出ます
43.jpg

Step10
wordpess設定

・rootユーザにdatabaseを追加
mysql> create database wordpress;

・rootユーザパスワード修正
mysql>update mysql.user set authentication_string=password(‘root’) where user=’root’;

45.jpg

・wordpessに情報記入
46.jpg

47.jpg

wp-config.php作成
$ cd var/www/html/wordpress

48.jpg

・情報記入
49.jpg
50.jpg
51.jpg

ここまで、以上になります。

追記:
FTP使いたくない場合下記参照

・wp-config.php に書きます
define(‘FS_METHOD’, ‘direct’);
・wordpressフォルダすべてのファイル書き可能に変更
sudo chmod 777 * -R

続きを読む

boto3が~/.aws/credentialsのaws_session_tokenを見ないで毎回`Enter MFA code:`と聞いてくるのをなんとかする方法

はじめに

AWSのroleを使っていてMFAが設定してあり、boto3を使いつつLocal環境開発時の鬱陶しさ軽減の話です。

※ よく見ると ISSUE: https://github.com/boto/botocore/issues/1126 になっていますね。いずれ修正されるのでしょう。

Version

  • boto3==1.4.4
  • botocore==1.5.7

ちょっと古いです。

内容

Pythonのboto3ライブラリは、AWS操作の鉄板ライブラリです。
認証の時にCredentialを色々な箇所から自動的に取得してくれますが、
~/.aws/configrole_arnmfa_serial があると、~/.aws/credentials にキーを設定してあってもその前にAssumeRoleProviderが発動し、Enter MFA code: というプロンプトを表示してMFAの入力を求められてしまいます。

ソースコード を見ると、

  • EnvProvider
  • AssumeRoleProvider
  • SharedCredentialProvider

の順にCredentialをチェックして、AssumeRoleProvider の時にこのプロンプトを表示します。
別途事前にassume-roleを実行していてファイル~/.aws/credentialsに書いてある場合はこの手順はスキップしたいです。

以下のようにするととりあえずできます。

sample.py
from botocore.session import get_session
from boto3.session import Session

bc_session = get_session()
session = Session(botocore_session=bc_session, profile_name="<your profile name>")
cred_resolver = bc_session.get_component('credential_provider')  # type: CredentialResolver

# set assume-role after shared-credentials-file
assume_role = cred_resolver.get_provider("assume-role")
cred_resolver.remove("assume-role")
cred_resolver.insert_after('shared-credentials-file', assume_role)

#
s3 = session.resource("s3")

# for testing
for bucket in s3.buckets.all():
    print(bucket.name)

ちなみにprofile_name の部分は、環境変数AWS_PROFILE で指定してあれば省略可能です。

さいごに

小技でした。

続きを読む

Amazon ECSの ネットワークモードに追加された awsvpcを試す

Amazon ECSのネットワークモードに awsvpc が追加されました。

これ選択すると、タスクごとにENIが割り当てられ、VPCのIPアドレスでアクセスできるようになります。
またENIに対してセキュリティグループを設定できるので、これまでのようにホストのセキュリティグループを
共有しなくてよくなるという意味でよりセキュアになります。
日本語訳の技術Blogも公開されていますので、その他のメリットなど
詳細についてはこちらを参照いただければと思います。
https://aws.amazon.com/jp/blogs/news/introducing-cloud-native-networking-for-ecs-containers/

試してみる

ECSのサンプルアプリケーションを起動してみます
https://github.com/awslabs/ecs-demo-php-simple-app

タスク定義の作成

すでにサンプルアプリの定義が存在したため、新しいリビジョンを作成しました。
ネットワークモードをbridgeからawsvpcへ変更します。

image.png

タスク内のコンテナはENIを共有するため、ポートマッピングはコンテナ側のポートのみが指定可能です。

image.png

サービスの作成

既存のecs-test-clusterというECSクラスター上にecs-test-serviceというサービスを追加してみます。
タスク定義では先ほどネットワークモードを awsvpc に設定したリビジョンを選択します。

image.png

ネットワーク構成の画面ではawsvpcを選択しているとVPC、サブネット、セキュリティグループを
それぞれ指定することができます。
例えばport 80を開ける場合、これまではECSインスタンスのセキュリティグループに許可設定を入れる必要が
ありましたが、こちらで個別に設定ができるようになります。

image.png

タスクの実行結果

タスクの詳細画面を確認すると、NetworkでVPCのPrivate IPを確認することができます。

image.png

EC2のダッシュボード上でもECSインスタンスに対し、ENIが追加されていることを確認できました。
image.png

コンテナインスタンス上で docker ps コマンドを実行すると、これまでのamazon-ecs-agentの他に
amazon-ecs-pause というコンテナが実行されていることが確認できました。

[ec2-user@ip-172-16-0-159 ~]$ docker ps
CONTAINER ID        IMAGE                                                       COMMAND                  CREATED             STATUS              NAMES
XXXXXXXXXXXX        XXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/XXXX:latest      "/usr/sbin/apache2..."   5 minutes ago       Up 5 minutes        ecs-console-sample-app-static-2-sample-app-
XXXXXXXXXXXX        amazon/amazon-ecs-pause:0.1.0                               "./pause"                5 minutes ago       Up 5 minutes        ecs-console-sample-app-static-2-internalecspause-
XXXXXXXXXXXX        amazon/amazon-ecs-agent:latest                              "/agent"                 2 hours ago         Up 2 hours          ecs-agent

VPCのIPアドレスに対してリクエストを送ること、サンプルアプリケーションが正常に
実行されていることを確認できました。

[ec2-user@ip-172-16-0-159 ~]$ curl http://172.16.0.207
<!DOCTYPE html>
<html lang="en">

   <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <title>Simple PHP App</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link href="assets/css/bootstrap.min.css" rel="stylesheet">
        <style>body {margin-top: 40px; background-color: #333;}</style>
        <link href="assets/css/bootstrap-responsive.min.css" rel="stylesheet">
        <!--[if lt IE 9]><script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
    </head>

   <body>
        <div class="container">
            <div class="hero-unit">
                <h1>Simple PHP App</h1>
                <h2>Congratulations</h2>
                <p>Your PHP application is now running on a container in Amazon ECS.</p>
                <p>The container is running PHP version 5.3.10-1ubuntu3.26.</p>

もちろんサービス作成時にELBを設定していれば、ELB経由でアクセスできます。
image.png

注意点

インスタンス毎のENI制限

EC2のインスタンスタイプ毎にENI数の制限がありますが、ECSはEC2上で実行されるため、
この制限が適用されます。
http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI

今回のテストで使用したクラスターはEC2 インスタンスタイプを t2.microに設定していたため、
ネットワークインターフェースの上限は 2 です。
試しにサービスを追加してみると以下のようなエラーで正常にタスクを起動できません。

service ecs-test-service2 was unable to place a task
because no container instance met all of its requirements. 
The closest matching container-instance xxxxxxxxxxxx encountered error "RESOURCE:ENI". 
For more information, see the Troubleshooting section.

コンテナの起動時間

正確な実測はできていませんが、
VPC Lambdaと同じような理屈でコンテナの起動が遅くなる可能性があるのではないかと

参考になれば幸いです。
以上です。

続きを読む

S3へ書き出したDynamoDBデータをRedshiftへコピーする [Copyコマンドについて]

S3へ書き出したDynamoDBのデータをRedshifへ取り込む際、指定方法がいろいろあったりして自分でも忘れやすいのでメモ

ここで打つコマンドはRedshfitコマンドであり、psqlコマンドとは異なる。

構文(抜粋)

COPY table-name 
[ column-list ]
FROM data_source
authorization
[ [ FORMAT ] [ AS ] data_format ] 
[ parameter [ argument ] [, ... ] ]

table-name

テーブルはすでにデータベースに存在する必要があります。テーブルは一時テーブルまたは永続的テーブルです。COPY コマンドは、新しい入力データをテーブルの既存の行に追加します。

FROM data_source

ターゲットテーブルにロードするソースデータの場所

  • S3

    • FROM ‘s3://mybucket_name/target_dir/’
  • DynamoDB

    • FROM ‘dynamodb://table-name’
  • EMR

    • FROM ‘emr://emr_cluster_id/hdfs_filepath’

CREDENTIALの指定

S3やDynamoDBから引っ張ってくるときはクレデンシャルの指定が必要

  • AWS_CLIのキーを使用: 'aws_access_key_id=<キー>;aws_secret_access_key=<シークレットキー>'
  • IAM_Roleを使用: 'arn:aws:iam::<aws-account-id>:role/<role-name>'

オプション

列のマッピングオプション

  • 列リスト

    • COPY tablenem の後に、列をリストで列挙する方法

ソースデータフィールドを特定のターゲット列にロードするには、列名のカンマ区切りリストを指定します。COPY ステートメントで列は任意の順序に指定できますが、Amazon S3 バケットなどにあるフラットファイルからロードする場合、ソースデータの順序に一致する必要があります。Amazon DynamoDB テーブルからロードする場合、順序は関係ありません

COPY sample (id, name, age)
  • JSONPaths ファイル

    • ソースデータの列情報をJSONファイルで記述し、S3に置いておく方法

データファイルを JSON または Avro 形式でロードする場合、COPY は Avro スキーマのフィールド名をターゲットテーブルまたは列リストの列名と一致させることで、JSON または Avro ソースデータのデータ要素をターゲットテーブルに自動的にマッピングします。

基本的にこっちを使っている。

JSONPath式

jsonpaths.json
{
    "jsonpaths": [ // 個々の記述は固定
        "$['venuename']", // 各列情報を"$['列名']で記載
        "$['venuecity']",
        "$['venuestate']",
        "$['venueseats']"
    ]
}

S3に書き出したDynamoDBの場合、 "$['カラム名']['型']", の形式で記載する。
項目の個数はCOPY先のテーブルに合わせなければならない

余談:
jsonpaths.jsonで指定したカラム数が7でCOPY先のテーブルのカラム数が8といったカラム数に差がある場合、下記のようなエラーがでる。

err
 error:  Number of jsonpaths and the number of columns should match. JSONPath size: 7, Number of columns in table or column list: 8

DynamoDBには確実に存在しないカラムであっても、Redshift側のテーブルにカラムが存在する場合、jsonpaths.jsonにそのカラムを指定し、COPY先のテーブルとカラム設定をあわせておけば
COPYを実行することが出来る。その際、DynamoDBに存在しないカラムのデータについては NULL で取り込む。指定した値で取り込むといった事がオプション指定で可能である。

取り込むテーブルとJSONのカラム数合わせろ、そこだけ確認しろ、自分。

データ形式パラメータ

  • FORMAT AS json

    • jsonpathsファイルを指定する方法

      • FORMAT AS json ‘s3://mybucket_name/target_dir/jsonpaths.json’
  • DELIMITER [AS] [‘delimiter_char’]

    • csv,tsv形式等を任意の区切り文字を指定して取り込む

      • DELIMITER AS ‘\t’
  • FIXEDWIDTH ‘fixedwidth_spec’

    • 固定長の場合に使用(ワタシは使用したこと無い。)

      • FIXEDWIDTH ‘colLabel1:colWidth1,colLabel:colWidth2, …’
  • BZIP2/GZIP/LZOP

    • 取り込むデータがどんな圧縮をされているかを指定する。gzip圧縮されていてもそのまま取り込むことが出来る。

データ変換パラメータ

テーブルをロードする際に、COPY は暗黙的にソースデータの文字列をターゲット列のデータ型に変換しようとします。デフォルトの動作とは異なる変換を指定する必要がある場合、またはデフォルトの変換がエラーになった場合、次のパラメータを指定してデータ変換を管理できます。

ここは使ったことがあるのと無いのがある。

一覧
ACCEPTANYDATE
ACCEPTINVCHARS
BLANKSASNULL
DATEFORMAT
EMPTYASNULL
ENCODING
ESCAPE
EXPLICIT_IDS
FILLRECORD
IGNOREBLANKLINES
IGNOREHEADER
NULL AS
REMOVEQUOTES
ROUNDEC
TIMEFORMAT
TRIMBLANKS
TRUNCATECOLUMNS

幾つか使うやつの抜粋

  • BLANKSASNUL

NULL など、空白文字のみから構成される空のフィールドをロードします。このオプションは CHAR と VARCHAR の列にのみ適用されます。INT など、他のデータ型の空のフィールドは常に NULL でロードされます。

  • EMPTYASNULL

Amazon Redshift で CHAR と VARCHAR の空のフィールドを NULL としてロードすることを指定します。INT など、他のデータ型の空のフィールドは常に NULL でロードされます。

  • IGNOREHEADER [ AS ] number_rows

指定された number_rows をファイルヘッダーとして扱い、ロードされせん。

  • NULL AS ‘null_string’

null_string に一致するフィールドを NULL としてロードします。

マニュフェストファイル

Datapipeline等を使用する場合、マニュフェストファイルが生成されます。
それを使用して取り込む場合に指定する。

  • manifest

manifest を指定する場合は、FORMAT でマニュフェストファイルを指定する。

サンプル

S3からCOPY

sample
COPY table
FROM 's3://mybucket_name/target_dir/' 
CREDENTIALS 'aws_access_key_id=<キー>;aws_secret_access_key=<シークレットキー>'
FORMAT AS json 's3://mybucket_name/target_dir/jsonpaths.json'
GZIP;

DynamoDBからCOPY

sample
COPY table 
FROM 'dynamodb://table_name' 
CREDENTIALS 'aws_access_key_id=<キー>;aws_secret_access_key=<シークレットキー>'
readratio 0.7;

readratio は確保されたキャパシティに対して使用する割合

マニュフェストファイルを使用した取り込み

COPY table
FROM 's3://mybucket_name/target_dir/manifest'
CREDENTIALS 'aws_access_key_id=<キー>;aws_secret_access_key=<シークレットキー>'
FORMAT AS json 's3://mybucket_name/target_dir/jsonpaths.json'
manifest;

資料

COPY

続きを読む

サーバーレス(HTML on S3)にGoogle認証付きS3アップローダーを作る

やりたいこと

  • S3上にHTMLとJSファイルをおいて静的webページとして公開
  • webページに入力した内容をcsvにまとめてS3に出力
  • 認証はgoogleのOpenID Connectを使用(roleをつけておく)
  • バケットポリシーでIP制限をかけておく

S3でwebページ公開に関して

以下のサイトを参考にして公開
S3で静的ウェブサイトをホスティングしてみる
バケットポリシーも作っておく

googleのOpen Connect周り

以下のサイトを参考にクライアントID+認証周り作成
Googleの「OpenID Connect」を利用する為の「クライアントID」の取得方法

スクリーンショット 2017-11-13 18.23.27.png
*承認済みのJavaScript生成元を設定せずにしばらくはまっていました…

Googleと連携するroleの作成

AWSのコンソール画面で
IAM > role > roleの作成
と進み、ウェブIDのタブを選択してプロバイダーにGoogleを選択
AudienceにはクライアントIDを入力してpolicyのattachなどを行う
スクリーンショット 2017-11-15 16.49.52.png

例で用いたpolicyはS3の特定のフォルダにのみアクセス可能なものにしました

policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME/output/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME"
            ],
            "Effect": "Allow",
            "Condition": {
                "StringEquals": {
                    "s3:prefix": "output"
                }
            }
        }
    ]
}

いよいよJavascript作成

フォルダ構成

  • BUCKET

    • output
    • HTML
      • index.html
      • favicon.ico
      • logic.js

ログイン周り

以下のURLを参考にJavascriptを作成(ログイン箇所に関しては例をそのまま使用)
ブラウザの JavaScript
Web Federated Identity Examples

index.html
<!DOCTYPE html>
<html>
<head>
    <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
    <title>AWS SDK for JavaScript - Sample Application</title>
    <meta charset="utf-8"/>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</head>

<body>
<div id="results" role="alert"></div>
<nav class="navbar navbar-default">
    <div class="container">
        <!-- 2.ヘッダ情報 -->
        <div class="navbar-header">
            <a class="navbar-brand">S3アップローダー</a>
        </div>
    </div>
</nav>
<div class="container">
    <form>
        <div class="form-group" id="name">
            <label>名前</label>
            <input type="text" name="name" class="form-control" placeholder="申請 太郎" required>
        </div>
        <div class="form-group" id="account_flg">
            <label>アカウント</label>
            <select name="account_flg" class="form-control">
                <option value="不要">不要</option>
                <option value="必要">必要</option>
            </select>
        </div>
        ...
        <button type="submit" onclick="upload_csv();" class="btn btn-primary">申請登録</button>
    </form>
</div>
<span
        id="login"
        class="g-signin"
        data-height="short"
        data-callback="loginToGoogle"
        data-cookiepolicy="single_host_origin"
        data-requestvisibleactions="http://schemas.google.com/AddActivity"
        data-scope="https://www.googleapis.com/auth/plus.login">
</span>
<script>
    //以下ほぼサンプルのコピペ
    //s3 = null;  s3をアップローダーでも使いたいので後々グローバル変数で指定しています
    var clientID = '************.apps.googleusercontent.com'; // Google client ID
    var roleArn = 'arn:aws:iam::************:role/ROLE_NAME';
    document.getElementById('login').setAttribute('data-clientid', clientID);
    function loginToGoogle(response) {
        if (!response.error) {
            AWS.config.credentials = new AWS.WebIdentityCredentials({
                RoleArn: roleArn,
                WebIdentityToken: response.id_token
            });
            s3 = new AWS.S3();
            console.log('You are now logged in.');
        } else {
            console.log('There was a problem logging you in.');
        }
    }
    (function () {
        var po = document.createElement('script');
        po.type = 'text/javascript';
        po.async = true;
        po.src = 'https://apis.google.com/js/client:plusone.js';
        var s = document.getElementsByTagName('script')[0];
        s.parentNode.insertBefore(po, s);
    })();
</script>
<script src="logic.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>

アップローダー周り

logic.js
//ここに必要な項目のIDを書いておく
csv_array = [
    'name',
    'account_flg'
]

//メインのところ(どのcsvを作成するか)
function upload_csv() {
    upload('account', make_csv(csv_array));
}

//配列情報から変数(csv)にヘッダー情報と内容を入れる関数
function make_csv(array) {
    var csv = '';
    for (i in array) {
        var block = document.getElementById(array[i]);
        csv += block.getElementsByTagName("label")[0].innerHTML + ',';
    }
    csv = csv.slice(0,-1);
    csv += '\n';
    for (i in array) {
        var block = document.getElementById(array[i]);
        if (block.getElementsByTagName("select")[0]){
            var num = block.getElementsByTagName("select")[0].selectedIndex;
            csv += block.getElementsByTagName("select")[0].options[num].value + ',';
        } else {
            csv += block.getElementsByTagName("input")[0].value + ',';
        }
    }
    csv = csv.slice(0,-1);
    return csv;
}

//出来上がったcsvファイルをアップロードする関数
function upload(service_name, body) {
    var results = document.getElementById('results');
    var params = {
        Bucket: 'BUCKET_NAME',
        Key: 'output/' + service_name + '_' + timestamp() + '.csv', //名前が被らないようにtimestam付
        ContentType: 'csv',
        Body: body
    };
    s3.putObject(params, function (err, data) {
        results.setAttribute("class", err ? 'alert alert-danger' : 'alert alert-success');
        results.innerHTML = err ? "<strong>ERROR!</strong>" : "<strong>UPLOADED</strong>";
    });
}

//タイムスタンプ作成用関数
function timestamp() {
    var d = new Date();
    var year = d.getFullYear();
    var month = d.getMonth() + 1;
    var day = d.getDate();
    var hour = ( d.getHours() < 10 ) ? '0' + d.getHours() : d.getHours();
    var min = ( d.getMinutes() < 10 ) ? '0' + d.getMinutes() : d.getMinutes();
    return '' + year + month + day + hour + min;
}

終わりに

IP制限をかけておけば簡単に社内サービスを作成できます
単にローカルファイルを上げるだけであれば、htnl上でファイル取得をしてuploadの関数を使えばできます
サーバーレス素晴らしい

続きを読む

oci runtime errorでdockerが起動しなくなった件

AWS EC2上で起動させていたコンテナがリソース不足で応答不能になったので、
EC2停止 -> スケールアップ -> EC2起動
したら、コンテナが起動しなくなってしまった。

$ docker-compose ps
        Name                      Command                State     Ports
------------------------------------------------------------------------
ec2user_gitlab_1       /sbin/entrypoint.sh app:start    Exit 255
ec2user_postgresql_1   /sbin/entrypoint.sh              Exit 255
ec2user_redis_1        /sbin/entrypoint.sh --logl ...   Exit 255


$ docker-compose up -d
Starting ec2user_postgresql_1 ...
Starting ec2user_redis_1 ...
Starting ec2user_postgresql_1
Starting ec2user_postgresql_1 ... error

ERROR: for ec2user_postgresql_1  Cannot start service postgresql: oci runtime error: container with id exists: 6b69a34b1f5b140bc5b8Starting ec2user_redis_1 ... error

ERROR: for ec2user_redis_1  Cannot start service redis: oci runtime error: container with id exists: fdef8901e8a94418b4cca0fb3eaa7851128cff85eda0e622e7ed9fd013707516

ERROR: for redis  Cannot start service redis: oci runtime error: container with id exists: fdef8901e8a94418b4cca0fb3eaa7851128cff85eda0e622e7ed9fd013707516

ERROR: for postgresql  Cannot start service postgresql: oci runtime error: container with id exists: 6b69a34b1f5b140bc5b889644d117d53fd1840d96879d80ffbdc6284f7ec0305
ERROR: Encountered errors while bringing up the project.

$ docker logs <container id>
   :
   :
container with id exists: <container id>

dockerやEC2を再起動しても解決しなかったが、以下の手順で復活できました。

$ sudo su -
# cd /run/runc/
# ls 
<container id1> <container id2> <container id3>
# rm -fr *

$ docker-compose up -d
Starting ec2user_postgresql_1 ...
Starting ec2user_redis_1 ...
Starting ec2user_postgresql_1
Starting ec2user_redis_1 ... done
Starting ec2user_gitlab_1 ...
Starting ec2user_gitlab_1 ... done

$ docker-compose ps
        Name                      Command               State                           Ports
---------------------------------------------------------------------------------------------------------------------
ec2user_gitlab_1       /sbin/entrypoint.sh app:start    Up      0.0.0.0:10022->22/tcp, 443/tcp, 0.0.0.0:10080->80/tcp
ec2user_postgresql_1   /sbin/entrypoint.sh              Up      5432/tcp
ec2user_redis_1        /sbin/entrypoint.sh --logl ...   Up      6379/tcp

めでたしめでたし。

今まで何度も同じEC2インスタンスを停止->再開していても問題はありませんでした。
今回のような問題に遭遇したのは初めてでしたが、EC2の停止にだいぶ時間がかかっており、少し怪しい気配は感じていました。

削除した /run/runc/<container id>/ には state.json というファイルが存在していて、サイズは25Kほどありcontainerの状態を保持しているようでした。
コンテナを停止するとディレクトリごと消える為、サーバが異常終了し、このファイルが残ってしまった事が原因のようでした。

続きを読む

EC2(Amazon Linux)とRDS(MySQL)でLAMP環境を構築するまで

備忘録程度に簡単にメモ

EC2でLAMP環境を構築するまでにやったこと

AWSでやったこと

  • AWSのアカウントを作った
  • 無料枠の範囲内でとりあえずEC2とRDSを立てる
    理由はなくなんとなくAmazon LinuxとMySQLの構成で作成。
    RDSを作成する時は無料枠のオプションを表示することが出来るので、そのチェックを入れておくと料金が発生する設定にしてしまうことを防ぐことができる。
  • 請求アラームの作成
    0ドルを超えるとメールで発報するよう設定(通常料金でも1,000円に収まる範囲だと思うが無料枠でいろいろしたいので)
  • Elastic IPの取得
    IPアドレスを取得し、EC2に関連付ける(無料枠内で起動しているEC2に関連付けされている間は無料らしい)
  • EC2のセキュリティグループにアクセスする際のIPからのSSH接続を許可
    ここで一緒に任意のIPからのHTTP/HTTPSも許可しとくといい

たぶんざっくりこんなもん
リージョンはオレゴン(リージョン次第で無料かどうか変わるのかはわからない)

SSH接続の設定

EC2を作成したときにpemをDLしているはずなので、それを使ってSSH接続してみる

pemの権限設定を変更する

$ chmod 400 [pemのパス]

SSH接続してみる

$ ssh -i [pemのパス] ec2-user@[パブリックドメイン]

パブリックドメインはec2から始まるインスタンスごとに勝手に設定されているもの
こんなの(ec2-11-111-111-1.us-east-2.compute.amazonaws.com)

これでつながるはず。繋がらなかったらインバウンドで許可したIPアドレスに間違いがないか、
ユーザ名(デフォはec2-user)を別に設定していないかなどを確認

ここまでできればあとはサーバ側の設定とDNSさえどうにかすればWebサーバとして利用できる

SSHConfigをいじって接続しやすくする(おまけ)

$ vi ~/.ssh/config 

以下のような感じで追記

#AWSサーバ
Host [sshコマンドで使う名前]
        HostName [パブリックドメイン]
        User ec2-user
        IdentityFile [pemのパス]

これで以下みたいに簡単に接続できる

$ ssh [sshコマンドで使う名前]

サーバ側の設定(とりあえずPHPとMySQLが使える状態に持っていく)

とりあえずソフトウェアパッケージを更新

$ sudo yum update -y

Apahe、PHP、MySQLをインストール

$ sudo yum install -y httpd24 php70 mysql56-server php70-mysqlnd

Apache起動とサーバ起動時に自動的に立ち上がるよう設定

$ sudo service httpd start
$ sudo chkconfig httpd on
chkconfig --list httpd
httpd           0:off   1:off   2:on    3:on    4:on    5:on    6:off

EC2のインバウンド設定でHTTPを任意のIPに対して許可していれば、ブラウザにIPアドレスを入力すると
Apacheのテストページが見れるはず

Apacheのドキュメントルートを編集できるようにする

デフォルトではApacheは/var/www/htmlをドキュメントルートにしている。
/var/wwwディレクトリはrootが所有者なので、apacheユーザに変更する
SSH時のユーザはec2-userなので、apacheユーザグループにec2-userを追加する。

$ sudo usermod -a -G apache ec2-user

再接続する

$ exit
$ groups
ec2-user wheel apache
$ sudo chown -R ec2-user:apache /var/www

グループ編集許可を与える

$ sudo chmod 2775 /var/www
$ find /var/www -type d -exec sudo chmod 2775 {} ;
$ find /var/www -type f -exec sudo chmod 0664 {} ;

PHPファイルの動作を確認する

さっきの設定がうまく行っていれば、ec2-userはルートディレクトリにファイルを作成する事ができるはず

$ echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php

[IPアドレス]/phpinfo.php にアクセスしてPHPの情報が表示されるはず
公開すべき情報ではないので、忘れずに削除しとく

$ rm /var/www/html/phpinfo.php

EC2とRDSを疎通させる

とりあえずMySQLポートを開放

EC2のインバウンドにMYSQLの接続を任意の場所から許可するよう設定

mysqlコマンドでRDSのMySQLへ接続してみる

RDSのインスタンスのセキュリティグループで、EC2インスタンスのセキュリティグループからのMYSQL接続を許可するよう設定

mysql –h [RDSインスタンスのエンドポイント] -P 3306 –u [ユーザ名] –p
Enter password: [パスワード]

基本的にユーザ名とパスワードはAWSのユーザ名パスワードになっているはず(何も設定していなければ)

ここまでできればApache、PHP、MySQLのセットアップはざっくりと完了。
セキュリティ的な話をすると弱い部分はたくさんあるので、実際の公開時には補強の必要があるが、
最低限は上記で完了。
後は静的HTML置くなり、Codeigniterでも入れて本格的なWebアプリ公開するなりする。

続きを読む

AWSのインスタンスをバックアップする

AWSのインスタンスのバックアップを取ろうと思い、
下記参考サイトのスクリプトを実行したところ正常に動かなかったので一部修正しました。

jqをインストールしています。

#!/bin/sh

# インスタンスIDを取得
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)

# バックアップ対象のインスタンス名
INSTANCE_NAME="インスタンス名"

# 本日の日付
DATE_CURRENT=`date +%Y-%m-%d`

# 本日の日時
TIME_CURRENT=`date +%Y%m%d%H%M`

# 何日後に削除するか
PURGE_AFTER_DAYS=3

# ↑のタイムスタンプ
PURGE_AFTER=`date -d +${PURGE_AFTER_DAYS}days -u +%Y-%m-%d`

#バックアップを作成する
AMI_ID=`aws ec2 create-image --instance-id ${INSTANCE_ID} --name "${INSTANCE_NAME}_${TIME_CURRENT}" --no-reboot  | jq '.ImageId' | sed 's/"//g'`

# AMIを検索するためのタグを作成する
aws ec2 create-tags --resources ${AMI_ID} --tags Key=Name,Value="${INSTANCE_NAME}" Key=PurgeAllow,Value=true Key=PurgeAfter,Value=$PURGE_AFTER

# タグを条件にして削除可能なバックアップを検索する
AMI_PURGE_ALLOWED=`aws ec2 describe-tags --filters "Name=resource-type,Values=image" "Name=key,Values=PurgeAllow" | jq '.Tags[].ResourceId' | sed 's/"//g'`

for AMI_ID in ${AMI_PURGE_ALLOWED}; do
  PURGE_AFTER_DATE=`aws ec2 describe-tags --filters "Name=resource-type,Values=image" "Name=resource-id,Values=${AMI_ID}" "Name=key,Values=PurgeAfter" | jq '.Tags[].Value' | sed 's/"//g'`

  if [ -n ${PURGE_AFTER_DATE} ]; then
    DATE_CURRENT_EPOCH=`date -d ${DATE_CURRENT} +%s`
    PURGE_AFTER_DATE_EPOCH=`date -d ${PURGE_AFTER_DATE} +%s`

    if [[ ${PURGE_AFTER_DATE_EPOCH} < ${DATE_CURRENT_EPOCH} ]]; then
      # タグを見て削除対象か判定を行い、対象であればバックアップを削除する
      aws ec2 deregister-image --image-id ${AMI_ID}

      SNAPSHOT_ID=`aws ec2 describe-images --image-ids ${AMI_ID} | jq '.Images[].BlockDeviceMappings[].Ebs.SnapshotId' | sed 's/"//g'`
      aws ec2 delete-snapshot --snapshot-id ${SNAPSHOT_ID}
    fi
  fi
done

参考サイト

https://dev.classmethod.jp/cloud/aws/aws-shellscript-summary/#toc-2
AMIとEBSのバックアップを作成する
AMIのバックアップの取得

続きを読む

Amazon CloudSearch の domain を AWS SDK for PHP で作成する

ゴール

Cloud formation は CloudSearchをサポートしていなさそうなので、AWSのコンソールから行える CloudSearch のドメイン作成と同等のことを AWS SDK for PHP で実現します。

環境

実行環境

  • AWS SDK for php version 2.8.31
  • php 7.0.21
  • PHPはEC2で実行しました。EC2には CloudSearchFullAccess を付与したIAMロールが設定してあります。

composer.json

composer.json
    "require": {
        "aws/aws-sdk-php": "2.*"
    }

CloudSeachのドメインを作成する PHPスクリプト

繰り返して実行すると、設定が更新されます。「既に存在するのでダメ」なエラーは発生しませんでした。

<?php

require_once 'vendor/autoload.php';

use AwsCloudSearchCloudSearchClient;
use AwsCommonEnumRegion;


const DOMAIN_NAME = '[作成するドメイン名]';

$client = CloudSearchClient::factory([
    'profile' => 'cloudsearch',
    'region'  => Region::US_WEST_2  // Oregon
]);

// ドメイン作成
$doamin = $client->createDomain(['DomainName'=>DOMAIN_NAME]);
// var_dump($doamin);

// Scaling Options でインスタンスサイズを設定
$client->UpdateScalingParameters([
    'DomainName'=>DOMAIN_NAME,
    'ScalingParameters' => [
        'DesiredInstanceType' => 'search.m1.small',
        'DesiredReplicationCount' => 1,
        // 'DesiredPartitionCount' => 1, // only search.m3.2xlarge
    ]
]);

// Availability Options でマルチAZ を無効 に設定
$client->UpdateAvailabilityOptions([
    'DomainName'=>DOMAIN_NAME,
    'MultiAZ' => false
]);

// Access Policies で指定したIPアドレスからのアクセスを許可
$client->UpdateServiceAccessPolicies([
    'DomainName'=>DOMAIN_NAME,
    'AccessPolicies' => accessPolicy(),
]);

// フィールドの設定
foreach( defineFields() as $name => $f )
{
    $f['IndexFieldName'] = $name;
    $client->defineIndexField([
        'DomainName' => DOMAIN_NAME,
        'IndexField' => $f,
    ]);
}

$indexFiled = $client->indexDocuments(['DomainName' => DOMAIN_NAME]);
// var_dump(indexFiled);

/**
 * アクセス ポリシー
 */ 
function accessPolicy()
{
    $policy = <<<EOM
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "cloudsearch:*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "[接続を許可するIPアドレス]"
        }
      }
    }
  ]
}
EOM;
    return $policy;
}

/**
 * フィールドの定義
 */
function defineFields()
{
    // AnalysisScheme
    // http://docs.aws.amazon.com/ja_jp/cloudsearch/latest/developerguide/text-processing.html
    return [
        'tweet_id' => [
            'IndexFieldType'=> 'literal',
            'LiteralOptions' => [
                'SearchEnabled' => true,
                'FacetEnabled' => false,
                'ReturnEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'tweet_url' => [
            'IndexFieldType'=> 'text',
            'TextOptions' => [
                'AnalysisScheme' => '_en_default_',
                'ReturnEnabled' => true,
                'SortEnabled' => false,
                'HighlightEnabled' => false,
            ],
         ],
         'user_id' => [
            'IndexFieldType'=> 'text',
            'TextOptions' => [
                'SearchEnabled' => true,
                'FacetEnabled' => false,
                'ReturnEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'user_screen_name' => [
            'IndexFieldType'=> 'text',
            'TextOptions' => [
                'AnalysisScheme' => '_en_default_',
                'ReturnEnabled' => true,
                'SortEnabled' => true,
                'HighlightEnabled' => false,
            ],
         ],
         'created_at' => [
            'IndexFieldType'=> 'date',
            'DateOptions' => [
                'FacetEnabled' => true,
                'ReturnEnabled' => true,
                'SearchEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'text' =>[
            'IndexFieldType'=> 'text',
            'TextOptions' => [
                'AnalysisScheme' => '_ja_default_',
                'ReturnEnabled' => true,
                'SortEnabled' => true,
                'HighlightEnabled' => true,
            ],
         ],
         'retweet_count' => [
            'IndexFieldType'=> 'int',
            'IntOptions' => [
                'SearchEnabled' => true,
                'FacetEnabled' => true ,
                'ReturnEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'retweeted' => [
            'IndexFieldType'=> 'literal',
            'LiteralOptions' => [
                'SearchEnabled' => true,
                'FacetEnabled' => false,
                'ReturnEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'hash_tags' => [
            'IndexFieldType'=> 'text-array',
            'TextArrayOptions' => [
                'SearchEnabled' => true,
                'AnalysisScheme' => '_ja_default_',
                'HighlightEnabled' => true,
                'ReturnEnabled' => true,
            ],
         ],
         "hash_tags_facet" => [
            'IndexFieldType'=> 'literal-array',
            'LiteralArrayOptions' => [
                'FacetEnabled' => true,
                'ReturnEnabled' => false,
                'SourceFields' => 'hash_tags',
            ],
         ],
         'urls' => [
            'IndexFieldType'=> 'text-array',
            'TextArrayOptions' => [
                'SearchEnabled' => true,
                'AnalysisScheme' => '_ja_default_',
                'HighlightEnabled' => true,
                'ReturnEnabled' => true,
            ],
         ],
    ];
}

実行後の Indexing Options の画面キャプチャ
スクリーンショット 2017-11-12 11.53.27.png

TwitterのAPIでツイートを取得して、主に ツイートのメッセージとハッシュタグで検索します。

メモ

インスタンス・タイプ

今回はデータ量が少ないので、明示的に search.m1.small にしています。

インスタンス・タイプを決めるにあたり、参考になる情報がドキュメントにあります。

一括アップロードを実行するには、以下に従います。

・バッチのサイズを制限の 5 MB にできるだけ近付けてください。小さなバッチを大量にアップロードすると、アップロードとインデックス作成の処理速度が低下します。
・必要なインスタンスタイプを、デフォルトの search.m1.small より大きなインスタンスタイプに設定します。使用できるアップロードスレッドの数は、ドメインで使っている検索インスタンスのタイプ、データの性質、インデックス作成オプションによって異なります。インスタンスタイプが大きいほど、アップロード容量が大きくなります。search.m1.small インスタンスにバッチを並列アップロードしようとすると、通常は、高い確率で 504 または 507 エラーが発生します。必要なインスタンスタイプの設定の詳細については、「スケーリングオプションの設定」を参照してください。

〜 省略 〜

データが 1 GB 未満のデータセットまたは 1,000,001 KB 未満のドキュメントの場合は、スモール検索インスタンスで十分です。1~8 GB のデータセットをアップロードするには、アップロードする前に、必要なインスタンスタイプを search.m3.large に設定することをお勧めします。8~16 GB のデータセットは、search.m3.xlarge で始まります。16~32 GB のデータセットは、search.m3.2xlarge で始まります。アップロードのサイズが 32 GB を超える場合は、インスタンスタイプに search.m3.2xlarge を選択し、データセットに対応できるように、望ましいパーティション数を増やします。各パーティションに最大 32 GB のデータを格納できます。さらに多くのアップロード容量が必要な場合、または 500 GB を超えるインデックスを作成する場合は、Service Increase Limit Request を送信します。

http://docs.aws.amazon.com/ja_jp/cloudsearch/latest/developerguide/uploading-data.html#bulk-uploads

TextField の AnalysisScheme

管理コンソールから操作する時にプルダウンで表示される言語別のテキスト処理の一覧です。
http://docs.aws.amazon.com/ja_jp/cloudsearch/latest/developerguide/text-processing.html

目的の言語の デフォルトの分析スキーム を AnalysisScheme に指定します。日本語は _ja_default_ 、英語は_en_default_ です。

日本語(ja)
アルゴリズム語幹解釈オプション: full
アルゴリズム複混合が有効
オプションのトークン分割ディクショナリ
デフォルトの分析スキーム: __ja_default__
・アルゴリズム語幹解釈: full
・デフォルトのストップワードディクショナリ

続きを読む