現在のインスタンス料金を取得する script: RDS 編

http://qiita.com/bells17/items/5326d11edc6acc4feea2
の RDS 版です

注意点として取得するデータのエンジンを Aurora に絞ってます

rds.rb
require 'json'
require 'bigdecimal'

results = {}
json_data = open(ARGV[0]) {|io| JSON.load(io) }

# product 情報を取得
json_data['products'].keys.each do |skuNo|
    product = json_data['products'][skuNo]

    if (product['productFamily'] == 'Database Instance' and
          product['attributes']['locationType'] == 'AWS Region' and
          product['attributes']['location'] == 'Asia Pacific (Tokyo)' and
          product['attributes']['databaseEngine'] == 'Amazon Aurora') # Aurora だけに絞ってます (エンジンが mysql か postgresql かは無いっぽい??)

        results[product['sku']] = {
            sku: product['sku'],
            location: product['attributes']['location'],
            instanceType: product['attributes']['instanceType'],
            instanceFamily: product['attributes']['instanceFamily'],
            vcpu: product['attributes']['vcpu'],
            physicalProcessor: product['attributes']['physicalProcessor'],
            clockSpeed: product['attributes']['clockSpeed'],
            memory: product['attributes']['memory'],
            networkPerformance: product['attributes']['networkPerformance'],
            currentGeneration: product['attributes']['currentGeneration'],
            price_unit: 'USD'
        }

    end
end


# price

# on demand
json_data['terms']['OnDemand'].keys.each do |skuNo|
    if (results[skuNo])
        results[skuNo][:price_per_hour] = Proc.new {
            skuTerm = json_data['terms']['OnDemand'][skuNo][json_data['terms']['OnDemand'][skuNo].keys[0]]
            priceInfo = skuTerm['priceDimensions'][skuTerm['priceDimensions'].keys[0]]
            BigDecimal(priceInfo['pricePerUnit']['USD']).floor(2).to_f.to_s
        }.call
        results[skuNo][:price_per_day] = (BigDecimal(results[skuNo][:price_per_hour]) * BigDecimal("24")).floor(2).to_f.to_s
        results[skuNo][:price_per_month] = (BigDecimal(results[skuNo][:price_per_day]) * BigDecimal("30")).floor(2).to_f.to_s
    end
end

## reserved 
json_data['terms']['Reserved'].keys.each do |skuNo|
    if (results[skuNo])

        plans = json_data['terms']['Reserved'][skuNo].values.select do |plan|
            plan['termAttributes']['PurchaseOption'] == "All Upfront" # "All Upfront" のものだけ取得したい
        end

        results[skuNo][:price_reserved_1year_purchased_all_upfront] = plans.find { |plan|
            plan['termAttributes']['LeaseContractLength'] == '1yr'
        }['priceDimensions'].values.find {|priceDimension|
            priceDimension['description'] == "Upfront Fee"
        }['pricePerUnit']['USD']

        results[skuNo][:price_reserved_3year_purchased_all_upfront] = plans.find { |plan|
            plan['termAttributes']['LeaseContractLength'] == '3yr'
        }['priceDimensions'].values.find {|priceDimension|
            priceDimension['description'] == "Upfront Fee"
        }['pricePerUnit']['USD']

    end
end

# sort
sorted_result = {}
results.values.each do |row|
    sorted_result[row[:currentGeneration]] ||= {}
    sorted_result[row[:currentGeneration]][row[:instanceFamily]] ||= []
    sorted_result[row[:currentGeneration]][row[:instanceFamily]].push row
end

results = []
['Yes', 'No'].each do |currentGeneration| # 現行世代のものから並べる
    next unless sorted_result[currentGeneration]
    sorted_result[currentGeneration].keys.sort.each do |instanceFamily| # インスタンスファミリー毎に並べる
        results.concat sorted_result[currentGeneration][instanceFamily].sort_by { |row| row[:price_per_hour] }
    end
end

p results.to_json

上記を保存して以下のように実行する

curl https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonRDS/current/index.json > price-AmazonRDS.json
ruby rds.rb price-AmazonRDS.json | sed -e s/^"// | sed -e s/"$// | sed -e 's/\"/"/g' | jq .

以下のような結果が取れる

[
  {
    "sku": "H7JQN46Z6VDZ3K5V",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.t2.small",
    "instanceFamily": "General purpose",
    "vcpu": "1",
    "physicalProcessor": "Intel Xeon Family",
    "clockSpeed": "Up to 3.3 GHz",
    "memory": "2 GiB",
    "networkPerformance": "Low to Moderate",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.06",
    "price_per_day": "1.44",
    "price_per_month": "43.2",
    "price_reserved_1year_purchased_all_upfront": "403",
    "price_reserved_3year_purchased_all_upfront": "776"
  },
  {
    "sku": "MK8ETWDCPSK52PEV",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.t2.medium",
    "instanceFamily": "General purpose",
    "vcpu": "2",
    "physicalProcessor": "Intel Xeon Family",
    "clockSpeed": "Up to 3.3 GHz",
    "memory": "4 GiB",
    "networkPerformance": "Low to Moderate",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.12",
    "price_per_day": "2.88",
    "price_per_month": "86.4",
    "price_reserved_1year_purchased_all_upfront": "792",
    "price_reserved_3year_purchased_all_upfront": "1530"
  },
  {
    "sku": "8Z6GS5F6NKX37Q5E",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.large",
    "instanceFamily": "Memory optimized",
    "vcpu": "2",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "15.25 GiB",
    "networkPerformance": "Moderate",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.35",
    "price_per_day": "8.4",
    "price_per_month": "252.0",
    "price_reserved_1year_purchased_all_upfront": "1704",
    "price_reserved_3year_purchased_all_upfront": "3433"
  },
  {
    "sku": "PQP78BGE4C2HXDQF",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.xlarge",
    "instanceFamily": "Memory optimized",
    "vcpu": "4",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "30.5 GiB",
    "networkPerformance": "Moderate",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "0.7",
    "price_per_day": "16.8",
    "price_per_month": "504.0",
    "price_reserved_1year_purchased_all_upfront": "3408",
    "price_reserved_3year_purchased_all_upfront": "6867"
  },
  {
    "sku": "2WTMTR9HDDT7AA73",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.2xlarge",
    "instanceFamily": "Memory optimized",
    "vcpu": "8",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "61 GiB",
    "networkPerformance": "High",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "1.4",
    "price_per_day": "33.6",
    "price_per_month": "1008.0",
    "price_reserved_1year_purchased_all_upfront": "6815",
    "price_reserved_3year_purchased_all_upfront": "13733"
  },
  {
    "sku": "VRNJP9SPPRH2KM8M",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.4xlarge",
    "instanceFamily": "Memory optimized",
    "vcpu": "16",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "122 GiB",
    "networkPerformance": "High",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "2.8",
    "price_per_day": "67.2",
    "price_per_month": "2016.0",
    "price_reserved_1year_purchased_all_upfront": "13631",
    "price_reserved_3year_purchased_all_upfront": "27466"
  },
  {
    "sku": "NC3BZ293ZJFBVUT5",
    "location": "Asia Pacific (Tokyo)",
    "instanceType": "db.r3.8xlarge",
    "instanceFamily": "Memory optimized",
    "vcpu": "32",
    "physicalProcessor": "Intel Xeon E5-2670 v2 (Ivy Bridge)",
    "clockSpeed": "2.5 GHz",
    "memory": "244 GiB",
    "networkPerformance": "10 Gigabit",
    "currentGeneration": "Yes",
    "price_unit": "USD",
    "price_per_hour": "5.6",
    "price_per_day": "134.4",
    "price_per_month": "4032.0",
    "price_reserved_1year_purchased_all_upfront": "27261",
    "price_reserved_3year_purchased_all_upfront": "54932"
  }
]

続きを読む

EC2にNginx + Gunicorn + SupervisorでDjangoアプリケーションをデプロイする

Nginx + Gunicorn + Supervisorの組み合わせでDjangoアプリケーションを立ち上げたので手順のメモ
今回はOSに何も入っていない状態から始めていきます

環境

OS: Amazon Linux AMI
Python: 3.6.1
Django: 1.11.4
Nginx: 1.10.3
Gunicorn: 19.7.1
Supervisor: 3.3.3

Nginxのインストール

nginxのインストール

$ sudo yum install nginx

nginx起動する

$ sudo nginx

nginx自動起動設定

$ sudo chkconfig --add nginx
$ sudo chkconfig nginx o

自動起動設定確認
以下のようになっていればok

$ chkconfig | grep nginx
nginx           0:off   1:off   2:on    3:on    4:on    5:on    6:off

http://ipアドレスにアクセスしちゃんと起動しているか確認する
以下の通りになっていればOK
スクリーンショット 2017-08-03 13.40.56.png

Python環境の構築

今回はAnacondaで構築した
こちらからPython 3.6 versionをダウンロードする
ダウンロードしたパッケージをCyberduckなどのFTPツールで/home/ec2-userにアップロードする

アップロード完了したら下記コマンドでAnacondaインストールする

$ bash Anaconda3-4.4.0-Linux-x86_64.sh

インストール完了後、Anacondaのコマンドが使えるようにPATHを通す

$ export PATH="$PATH:/home/ec2-user/anaconda3/bin"

condaのコマンドを打って確認

$ conda info -e
# conda environments:
#
root                  *  /home/ec2-user/anaconda3

良さげです

pythonも3.6になっている

$ python --version
Python 3.6.1 :: Anaconda 4.4.0 (64-bit)

Djangoプロジェクトの作成

今回は直接EC2上でプロジェクトを作成します
本来はローカルで開発したDjangoアプリケーションをgit cloneすべき
また、DBもデフォルトのSQliteを使用しますが、実際のサービスを公開するにはPostgresqlやMariaDBを使う

まずはDjangoのインストール
root環境で動かすかどうかは少し議論の分かれるところで、別に環境を作ってDjangoを動かした方がいいんじゃないか、と思ったりもしますが、とりあえず今回はroot環境でインストールしてしまいます

$ pip install django

問題なければプロジェクトを作成

$ django-admin startproject test_project

プロジェクトが作られていることを確認

$ ls -ltr
total 511032
-rw-rw-r--  1 ec2-user ec2-user 523283080 Aug  3 04:50 Anaconda3-4.4.0-Linux-x86_64.sh
drwxrwxr-x 20 ec2-user ec2-user      4096 Aug  3 04:53 anaconda3
drwxrwxr-x  3 ec2-user ec2-user      4096 Aug  3 05:05 test_project

/test_project/test_project/settings.pyのALLOW HOSTを下記の通り編集しておく

settings.py
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["サーバのIPアドレス"]

下記のコマンドでDjangoを起動
デフォルトだと127.0.0.1:8000がbindアドレスとして使用されているため、オプションで0.0.0.0:8000を追加する必要があります
また、事前にAWSのセキュリティグループで8000番ポートを解放しておく必要があります

$ cd test_project
$ python manage.py runserver 0.0.0.0:8000

そして、http://IPアドレス:8000にアクセスすると以下の通りDjangoアプリケーションにアクセスできる
スクリーンショット 2017-08-03 15.23.25.png

Gunicornのインストール

GunicornはPython製のWSGIサーバ
WSGIサーバというのはWebサーバとWebアプリケーションをつなぐサーバのこと
なので、Nginx <-> Gunicorn <-> Djangoというような構成をイメージしていただければと

まずはGunicornのインストールをやっていく

$ pip install gunicorn

インストールされたらGunicornでDjangoを起動させる

$ gunicorn test_project.wsgi --bind=0.0.0.0:8000

先程と同様、http://IPアドレス:8000にアクセスするとDjangoアプリケーションに接続できる

Nginxの設定の変更

/etc/nginx.confを以下の通り編集する

/etc/nginx.conf

〜中略〜

http {
    〜中略〜

    upstream app_server {
        server 127.0.0.1:8000 fail_timeout=0;
    }

    server {
        #以下4行はコメントアウト
        #listen       80 default_server;
        #listen       [::]:80 default_server;
        #server_name  localhost;
        #root         /usr/share/nginx/html;

        # 以下3行を追加
        listen    80;
        server_name     IPアドレス or ドメイン;
        client_max_body_size    4G;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
            # 以下4行を追加
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_pass   http://app_server;
        }

    〜以下略〜

編集したら下記コマンドでnginxを再起動する

$ sudo service nginx restart
Stopping nginx:                                            [  OK  ]
Starting nginx:                                            [  OK  ]

これでNginxでのリバースプロキシの設定は完了
今回はnginx.confを直接編集したけれど、設定ファイルをどこか別のところに書いてそれを読み込ませる、という方法でもOK

その後、DjangoをGunicornで立ち上げる

$ gunicorn test_project.wsgi --bind=0.0.0.0:8000

次はhttp://IPアドレスにアクセスするとDjangoの画面が表示されるはず

Supervisorでプロセスをデーモン化する

今の状態だとGunicornのコマンドを中止したり、サーバからログアウトするとアプリケーションが停止してしまう
これを解消するためにSupervisorでGunicornのプロセスをデーモン化する

早速、Supervisorをインストール、としたいところだけれど、SupervisorはPython2系でしか動作しない
そのためAnacondaでPython2系の仮想環境を構築し、その環境にSupervisorをインストールしていく

まずは下記コマンドでSupervisor用のPython2系の仮想環境を作る

$ conda create -n supervisor python=2.7

python2系の環境に切り替えてpipでsupervisorをインストール

$ source activate supervisor
$ pip install supervisor

問題なくインストールできたら、supervisorの設定ファイルを作成し、それを/etc配下に配置する

$ echo_supervisord_conf > supervisord.conf
$ sudo mv supervisord.conf /etc

次にsupervisorの設定を行うため、supervisord.confを下記の通り編集

supervisord.conf
〜中略〜
[supervisord]
logfile=/var/log/supervisord.log ; ログの場所を変更
;logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log #コメントアウト
logfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10           ; # of main logfile backups; 0 means none, default 10
loglevel=info                ; log level; default info; others: debug,warn,trace
pidfile=/var/run/supervisord.pid ; 追記
;pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid #コメントアウト

〜中略〜
# includeはコメントアウトされているのでコメント外す
[include]
files = supervisord.d/*.conf ; 起動するプロセスのconfファイルの配置場所
;files = relative/directory/*.ini

ログファイルは作っておき、パーミッションも設定しておく

$ sudo touch /var/log/supervisord.log
$ sudo chown ec2-user /var/log/supervisord.log
$ sudo chgrp ec2-user /var/log/supervisord.log
$ sudo chmod 774 /var/log/supervisord.log

あと、ログローテションも設定しておく

$ sudo sh -c "echo '/var/log/supervisord.log {
       missingok
       weekly
       notifempty
       nocompress
}' > /etc/logrotate.d/supervisor"

次にデーモン化するプロセスのコマンドを記載したファイルを作っていく
まずはそれらのファイルを配置するディレクトリを作成

$ sudo mkdir /etc/supervisord.d

/etc/supervisord.d配下にdjango_app.confを作成
ここに、Gunicornのプロセスをデーモン化するための設定を以下のように書く

django_app.conf
[program:django_app]
directory=/home/ec2-user/test_project
command=gunicorn test_project.wsgi --bind=0.0.0.0:8000
numprocs=1
autostart=true
autorestart=true
user=ec2-user
redirect_stderr=true

directoryに実行するディレクトリを指定、commandのところにプロセスを起動するためのコマンドを指定する

ここまでできたら下記のコマンドでsupervisorを立ち上げる

$ supervisord

次に、confファイルを読み込ませる
こちらは仮にconfを修正などする場合は必ず実行する

$ supervisorctl reread

なお、下記のコマンドでデーモンを再起動し、そのタイミングでもconfが読み込まれたりする

$ supervisorctl reload

下記のコマンドでGunicornのプロセスをデーモン化する

$ supervisorctl start django_app

仮にdjango_app: ERROR (already started)というメッセージが出た場合は、以下のコマンドでプロセスの再起動をしたり、停止をしてからstartしたりする

$ supervisorctl stop django_app # 停止
$ supervisorctl restart django_app # 再起動

さて、この状態でサーバからログアウトしてみる
そして、http://IPアドレスにアクセスすると、Djangoの画面が表示される
GunicornのプロセスがSupervisorによりデーモン化されていることになる

よかったですね

続きを読む

Kinesis Firehose & s3 & Athenaでビッグデータ処理!

背景

  • ユーザーから送られてきた大量の位置情報ログをpostgresqlに保存するのがつらくなってきたのでs3に投げて処分するようにしたい。
  • けれどもfluentdを入れたaws-ec2をログアグリゲータにしてスケールさせるのは面倒。
  • けれども、実証実験や営業資料の作成のニーズが多いためs3に投げたログは引き続きRDBMSのような何かで気軽に検索&集計できるようにしたい。

構成

以下のように、サービスから投げたログをKinesisFirehoseが受け取ってs3に保存し、Athenaで検索するようにしました。

cloudcraft - KinesisFirehose & S3 & Athena (2).png

実装とハマりどころ

KinesisFirehose

ログ処理に特化したサービスで、流し込んだデータを塊にし圧縮してredshift、s3、elastic searchのいずれかに投げることができる。特徴は負荷に応じた同時実行数やインスタンス数の上限等を調整する必要がなく、使用者がスケーラビリティを気にしなくて良いこと。

現状では日本展開していないので西海岸で日本に近そうなオレゴン(us-west-2)で作る。

スクリーンショット 2017-07-31 1.51.50.png

まずはバゲット名を指定し、s3にアクセスする用のロールを作る。s3に投げられると

バケット名/2017/07/30/時刻/ログ名

という風にログがモリモリ保存されていくが、s3 buffer sizeは最後のログファイルのサイズの上限で、S3 buffer intervalは1個のログの時間幅の上限。ちなみに時間はUTC。

また、rubyからjson形式で投げる場合は以下のようになるが、

firehose = Aws::Firehose::Client.new(region: 'us-west-2')
firehose.put_record(delivery_stream_name: ENV['KINESIS_DELIVERY_STREAM_NAME'], record: {data: data.to_json + "\n"})

json形式のログをAWS Athenaで解析できるようにする場合は 以下のように1つ1つのobjectが改行されている必要があるため1個のobjectだけを送る場合は末端に改行コードを入れる必要がある。

xxx-logs-stream-1-2017-07-06-01-04-09-466c7aa2-754a-11e7-88d3-5f6e237864f8.gz
{"user_id":1111,"coupon_id":2222, "created_at" :  "2017-07-06T01:03:36.356Z"}
{"user_id":3333,"coupon_id":4444, "created_at" :  "2017-07-06T01:03:37.356Z"}

s3

言わずもがなの何でも置いておける格安ストレージ(1GBあたり2円)

Athena

s3に放り込まれたログをSQLで検索できるシステム。BigQueryと同じクエリがスキャンしたデータ量に応じて課金される料金体系(1TBあたり$5)だが、以下のようにパーティションを作成すると指定されたフォルダ以外のログはスキャンせずパケ死しなくなる。なお、firehoseのYYYY/MM/DD形式のディレクトリにはデフォでパーティションが設定されてはいないので日毎にパーティションを作りたい場合は一日に一度の定期タスクでAthenaのクエリを叩いて作ってやる必要がある(ダルい!)。

ALTER TABLE xxxx_logs.logs add partition (dt='20190630')
LOCATION 's3://xxx-logs/2019/06/30';
SELECT count(id) form logs where dt=20190630

使用するデータベースとテーブル名を設定し、ログが保存されているバゲットのリンク、データの形式、およびカラム(日時はdate型でパースできるよう整形するのが面倒な場合はstring型にした方が良いかも)を入力するとテーブルを作成するcreate文が作られ、成功すれば以後そのバゲットを検索できるようになる。

スクリーンショット 2017-07-31 2.38.47.png

なお、単なるs3ビューワーなので、以下のように新旧の形式が混在したログが入っていても整合性を意識せずに検索できるし、いつでも作り直せる。

{id: 1, latitude: 38.2345, longitude: 121.9876, user_id: 1
{id: 2, latitude: 38.2345, longitude: 121.9876, user_id: 1, app_id: 1}
select count(id) from logs where app_id is null
=> 1

結果

1.DBのコネクション数が劇的に減った♪
2.一日300MBずつ増えていたDBの増加が止まった!
3.Atehnaの検索早い!(100万件のログを2秒で検索可)

続きを読む

Rails + CircleCI 1.0 + AWS の設定手順 ビルド開始~デプロイ完了まで

はじめに

RailsでCircleCI環境を整えるまでの手順をまとめました。

CircleCIはバージョン2.0がありますが、
Dockerがないといけない?ようなので、バージョン1.0で準備しました。

circleCIとは

簡単に言うと、
githubのpushを検知して、自動でビルドからテスト、デプロイまで自動化してくれるツール。
(デフォルトだと全部ブランチ対象)

1コンテナは無料なので、時間が掛かってもよいのであれば無料枠で十分。

https://circleci.com/

設定ファイル

必要なファイルは以下3つ

① circle.yml
② config/database.yml.ci
③ script/deploy-staging.sh

① circle.yml

circleCIを動かくための設定ファイル。
Railsのアプリケーションディレクトリ直下にファイルを作ります。(gemfileとかと同じ場所)

② config/database.yml.ci

テストするためのDB設定ファイル。Rspecを実行するために必要。

③ script/deploy-staging.sh

テストが終わったらステージング環境へデプロイするためのスクリプトファイル。

設定ファイルの中身

circle.yml
machine:
  timezone:
    Asia/Tokyo
  ruby:
    version:
      2.3.0
dependencies:
  pre:
    - sudo pip install awscli
  override:
    - bundle install:
database:
  pre:
    - mv config/database.yml.ci config/database.yml
  override:
    - bundle exec rake db:create db:schema:load RAILS_ENV=test
    - bundle exec rake db:migrate RAILS_ENV=test
test:
  override:
    - bundle exec rspec spec/
deployment:
  staging:
    branch: master
    commands:
      - sh script/deploy-staging.sh:
          timeout: 1500

ポイント
AWSにデプロイするのにawscliを使用するために、
「sudo pip install awscli」を記述する。(デフォルトは古いらしい)
DB名は「test」じゃないと動かない。

config/database.yml.ci
test:
  adapter: postgresql
  encoding: unicode
  database: test
  pool: 5
script/deploy-staging.sh
#!/bin/sh

export AWS_DEFAULT_REGION="ap-northeast-1"

MYSECURITYGROUP="*"
MYIP=`curl -s ifconfig.me`

aws ec2 authorize-security-group-ingress --group-id $MYSECURITYGROUP --protocol tcp --port 22 --cidr $MYIP/32

bundle exec cap production deploy

aws ec2 revoke-security-group-ingress --group-id $MYSECURITYGROUP --protocol tcp --port 22 --cidr $MYIP/32

ポイント
MYSECURITYGROUPは、AWSのセキュリティーグループIDを指定する。
LFの改行コードで作成する。CRLFだと動かない。

その他設定

CircleCI側にAWSの秘密鍵を登録する。
AWS上で「cat ~/.ssh/id_rsa」で表示されるもの。

CircleCI側にAWSのアクセスキーとシークレットキーを設定

AWSの秘密鍵をAWSの認証キーとして登録。
AWS上で「cat id_rsa >> ~/.ssh/authorized_key」

Port 22をセキュリティグループで空けておく。

続きを読む

AWSクラウド環境の構築からSpring Bootアプリのデプロイまで(初心者向け)

アプリケーションエンジニアとして、普段の仕事でインフラ設計をなかなか経験したことがないですが、AWSを通じてインフラ知識がほぼZeroの私でもインフラ基盤を気軽に構築できて感動しました。今回は、私と同じの初心者に向け、AWSクラウド環境の構築からSpring Bootアプリのデプロイまでの手順を共有します。

環境構成図

AWS構成について、東京リージョン内で2つのアベイラビリティゾーン(AZ)を使用した冗長構成を採用します。EC2インスタンスはその2つのAZに分散配置し、ALB(ロードバランサ)経由でアクセスを分散する構成とします。また、RDSインスタンスはEC2と同様に2つのAZに分散配置するようMulti-AZの構成とします。
この構成はAWSの無料利用枠を超えているため、料金がかかります!ご注意ください。
全体構成図.jpg

まず、VPCを構築します!

外部ネットワークからAWS内のインスタンスに到達できるように、AWS内の各インスタンスにIPアドレスが割り振られ、適切にルーティングされる必要があります。このような仮想ネットワークを提供しているサービスをAmazon Virtual Private Cloud(VPC)と言います。

VPCの構成について

練習とは言え、実戦に近いVPC環境を目指しています。今回のVPCは、インターネット通信用のパブリックサブネットとインターネットから遮断されるプライベートサブネットの2種類で構成されます。その2種類のサブネットへのアクセスを制御するために、それぞれに異なるセキュリティグループを適用します。APサーバーはパブリックサブネット上に構築し、DBサーバーはプライベートサブネット上に構築します。
VPC構成詳細.jpg

VPC作成

AWSアカウントを新規登録した後に、デフォルトのVPC環境が既に作られていましたが、今回はそれを利用せずに、一から以下のVPCを新規構築します。
vpc構成図.jpg

1.AWSマネジメントコンソールVPCをクリック⇒左のメニューからVPCを選択⇒VPCの作成ボタンを押下します。
2.VPCの作成画面に適当な名前を入力し、CIDRブロック欄にIPアドレス範囲に入力します。(今回は「10.0.0.0/16」を入力します。)
3.はい、作成するボタンを押下します。
VPC作成.jpg

サブネット作成

上記のVPCの中にサブネットを作成します。サブネットは複数のAZに跨って作成することはできないので、必ず1つのAZを指定して作成します。負荷分散と冗長化のために、APサーバー用サブネットとDBサーバー用サブネットをそれぞれ2つずつ構築します。
subnet構成.jpg

1.左のメニューからサブネットを選択⇒サブネットの作成ボタンを押下します。
2.サブネットの作成画面に、適当な名前入力し、上記作成されたVPCを選んでCIDRブロックを入力します。(Subnet1は「10.0.0.0/24」とします。)
subnet1作成.jpg
3.上記と同じの手順で、Subnet2、Subnet3、Subnet4を構築します。

Subnet AZ IPv4 CIDRブロック
public-subnet1 ap-northeast-1a 10.0.0.0/24
public-subnet2 ap-northeast-1c 10.0.1.0/24
private-subnet1 ap-northeast-1a 10.0.2.0/24
private-subnet2 ap-northeast-1c 10.0.3.0/24

完成後サブネット一覧画面
subnetList.jpg

インターネットゲートウェイ(IGW)とルートテーブルの作成

インターネットゲートウェイ(IGW)は、名前の通りにインターネットとの出入口であり、VPCと外部ネットワークの間で通信を行うために設置します。
また、上記作成されたサブネットがパブリックサブネットなのか、あるいはプライベートサブネットなのかは、そのサブネットに適用されているルートテーブルによって決まります。
送信先:0.0.0.0/0のターゲットとしてIGWが設定されているルートテーブルが適用されているサブネットはパブリックサブネットです。一方、送信先:0.0.0.0/0のターゲットとしてIGWが設定されていないルートテーブル(デフォルトのまま)が適用されているサブネットはプライベートサブネットです。
igw&rtb.jpg

1.左のメニューからインターネットゲートウェイを選択⇒インターネットゲートウェイの作成ボタンを押下します。
2.適当な名前を入力し、はい、作成するのボタンを押下します。
igw.jpg
3.VPCにアタッチのボタンを押下し、VPCとの紐付けを行います。
igw attache.jpg
4.左のメニューからルートテーブルを選択⇒ルートテーブルの作成ボタンを押下します。
5.パブリックサブネットであるSubnet1のルートテーブルを作成するために、適当な名前を入力し、VPCとの紐付けを行い、はい、作成するのボタンを押下します。
rtb1.jpg
6.上記と同様の手順で、public-rtb2を構築します。今回はプライベートサブネット用ルートテーブルを作成せずに、デフォルトのルートテーブルを使用します。
7.パブリックサブネット用のルートテーブルに、デフォルトゲートウェイ(送信先0.0.0.0/0)のターゲットにIGWを登録します。
route.jpg
ルートテーブル内の「10.0.0.0/16 local」というルート情報は、デフォルトの設定で変更・削除することができません。このデフォルト設定は、VPC内の通信はルートテーブルでは制御できないということで、同じVPC内のサブネットであればサブネット間の通信が可能なっていることを意味しています。

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

セキュリティグループは、AWS内各インスタンスごとのファイアウォールで、受信(インバウンド)と送信(アウトバウンド)のアクセス制御ができます。各インスタンスには少なくとも1つのセキュリティグループを適用する必要があります。
VPC構成詳細.jpg
1.左のメニューからセキュリティグループを選択⇒セキュリティグループの作成ボタンを押下する。
2.APサーバー用セキュリティグループを作成するために、適当な名前を入力し、VPCへの紐付けを行い、はい、作成するのボタンを押下します。
sg作成.jpg
3.上記と同じの手順でDBサーバー用のセキュリティグループを作成します。
private sg.jpg
4.それぞれのセキュリティグループに受信(インバウンド)と送信(アウトバウンド)のルールを作成します。デフォルトでインバウンドは許可されているルールがないため、どこからのアクセスも受け付けません。一方、アウトバウンドは、デフォルトで全ての宛先/ポート番号に対するアクセスを許可するルールが設定されています。
外部からアクセスできように、APサーバー用セキュリティグループでSSHの22ポートとウェブアプリの8085ポートを開けておきます。
public inbound.jpg
一方、DBサーバー用セキュリティグループは、APサーバーからDBアクセスのみを許可するために、Auroraの3306ポートを開けておきます。
private sg rule.jpg

以上でVPCの構築が完了しました。

RDSインスタンスの構築

RDSは、リレーショナルデータベースのマネージャーサービスのことです。RDSで選択できるデータベースエンジンは、以下の6種類です。
・Amazon Aurora
・MySQL
・MariaDB
・PostgreSQL
・Oracle
・MS SQL Server
今回はAurora DBを構築します。Auroraは、MySQLと互換性のあるAWS独自のリレーショナルDBエンジンで、最大で MySQL の5倍のスループットおよび3倍の PostgreSQL スループットのパフォーマンスを持つと言われます。
RDS.jpg

サブネットグループの作成

DBインスタンス作成の前提条件として、VPC内でDBサブネットグループを指定する必要があります。
DB サブネットグループには、特定のリージョン内の少なくとも 2 つのアベイラビリティーゾーンにサブネットが必要です。 VPC に DB インスタンスを作成するときに、DB サブネットグループを選択する必要があります。Amazon RDS は、その DB サブネットグループと優先アベイラビリティーゾーンを使用し、サブネットとそのサブネット内の IP アドレスを選択して DB インスタンスに関連付けます。
1.AWSマネジメントコンソールRDSをクリック⇒左のメニューからサブネットグループを選択⇒サブネットグループの作成ボタンを押下します。
2.名前等を適当に入力し、2つのDB用サブネット(private subnet1、private subnet2)を追加し、作成ボタンを押下します。
db subnet group1.jpg

DBインスタンスの作成

1.左のメニューからインスタンスを選択⇒DBインスタンスの起動ボタンを押下します。
2.エンジンの選択画面にAmazon Auroraを選択します。
db1.jpg
3.DB詳細画面にて、DBインスタンスクラスなどを指定し、次のステップボタンを押下します。
※マルチAZ配置を選んだ場合、料金2倍かかります!
db2.jpg
4.VPC、DBサブネットグループ、優先AZ、およびセキュリティグループを指定します。
db3.jpg
5.設定完了後にDBインスタンスが作成中の状態であることを確認できます。
db4.jpg

EC2インスタンスの構築

やっと、EC2まで辿り着きました。Amazon Elastic Compute Cloud(EC2)は、AWSにおける仮想サーバのことです。今回負荷分散のために、2つのインスタンスを構築します。
EC2.jpg

EC2インスタンスの作成

1.AWSマネジメントコンソールEC2をクリック⇒左のメニューからインスタンスを選択⇒インスタンスの作成ボタンを押下します。
2.インスタンスの種類にAmazon Linuxを選択します。
ec21.jpg
3.インスタンスタイプ選択画面に、無料利用枠対象のタイプを選択します。
ec22.jpg
4.詳細設定画面に、VPCとサブネットを指定します。
ec23.jpg
5.ストレージを追加します。
ec24.jpg
6.APサーバー用セキュリティグループを指定します。
ec25.jpg
7.最後に、EC2にログインするためのキーペアをダウンロードし、インスタンスの作成ボタンを押下し、インスタンス作成が完了です。
ec26.jpg

ELASTIC IPの関連付け

上記のEC2インスタンスに対して、静的なパブリックIPアドレスを付与するために、ELASTIC IPの割り当てが必要です。
1.左のメニューからELASTIC IPを選択⇒新しいアドレスの割り当てボタンを押下します。
eip1.jpg
2.EC2インスタンスに関連付けを行います。
eip2.jpg

EC2環境の初期設定

Tera TermなどのSSHクライアントを使って上記のELASTIC IPを入力し、EC2インスタンスにアクセスします。
ec27.jpg
「ec2-user」ユーザーを使って、先ほどダウンロードしたキーでログインします。
ec28.jpg

tera term.jpg

無事にログインできたら、EC2の初期設定を行います。

# 最新のソフトウェアにアップデート
$ sudo yum update -y
# ホスト名変更
$ sudo hostname ec2-1-cinpo1
$ sudo vim /etc/sysconfig/network
HOSTNAME=ec2-cinpo1;
# ホストファイルを編集し、AWSから払い出された<Private IP>を書く。
$ echo "17X.XX.X.X30 ec2-cinpo1" |sudo tee -a /etc/hosts
# ホスト名確認
$ hostname -f
# タイムゾーン変更
# /etc/sysconfig/clockの編集
$ echo -e 'ZONE="Asia/Tokyo"nUTC=false' | sudo tee /etc/sysconfig/clock
# タイムゾーンファイルの変更
$ sudo ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
# 結果確認
$ date
# Java 8のインストール
$ sudo yum install java-1.8.0-openjdk.x86_64
# Java 8の選択
$ sudo alternatives --config java
# 結果確認
$ java -version

2台目のEC2を構築します。

上記と同じの手順で、2台目のEC2インスタンスを構築します。

Aurora環境へのデータ移植

# Auroraサーバーに接続するために、MySQLクライアントをインストールします。
$ sudo yum install mysql
# Auroraサーバーに接続し、データベースの新規作成やデータの移植を行います。
$ mysql -h <RDSインスタンスのエンドポイント> -u username -p
$ create database sampleDB

...下記省略...

mysql.jpg

Spring Bootアプリのデプロイ

1.Tera TermのSSH SCP転送を利用し、Spring Bootアプリをローカルから上記2つのEC2インスタンスにアップロードします。
2.完了後に、従来通りにSpring Bootアプリを起動します。

$ java -jar XXXXXXXX.jar

boot.jpg

3.この時点で、ELASTIC IP:8085 にアクセスしてみれば、アプリ画面が表示されるはずです。

ロードバランサーの作成

最後に、ロードバランサー(Application Load Balancer [ALB])を適用し、APサーバーの負荷分散を実現します。
ALB.jpg

ターゲットグループの構築

ALB適用の前提条件として、EC2インスタンスをターゲットとしてターゲットグループに登録する必要があります。ALBは、クライアントにとって単一の通信先として機能し、登録済みターゲットに受信トラフィックを分散します。
1.AWSマネジメントコンソールEC2をクリック⇒左のメニューからターゲットグループを選択⇒ターゲットグループの作成ボタンを押下します。
ALB1.jpg
2.ターゲットグループ一覧画面にて選択済みのターゲットグループにターゲットの登録を行います。
ALB2.jpg
3.EC2インスタンスをターゲットとしてターゲットグループに登録します。
ALB3.jpg

ALBの構築

1.左のメニューからロードバランサーを選択⇒ロードバランサーの作成ボタンを押下し、Application Load Balancerを選択します。
ALB4.jpg
2.名前を適当に指定し、リスナーとサブネット等を指定します。
ALB5.jpg
3.セキュリティグループを指定します。
ALB6.jpg
4.先ほど作成されたターゲットグループを指定します。
ALB7.jpg
5.作成中のステータスとなり、1~2分後に利用可能となります。
ALB8.jpg

動作確認

以上、AWS環境の構築からアプリのデプロイまで完成しました。
http://:ポート/にアクセスすれば、アプリ画面が表示されたら完成です。
final1.jpg

続きを読む

一部のRDSの情報の閲覧だけできるポリシーは作れなかった話

ポリシー作ってみた

協力会社の人が、「AWSのRDSのリソースの状況とか自分たちでリアルタイムで見たいから、マネジメントコンソールのアカウント欲しい」というので、特定のRDSの情報を見ることだけできるユーザを作ることに。

で、ポリシーの作成とかあまりやったことなかったので、AmazonRDSReadOnlyAccessのポリシーのJSONのResource"*"からARN指定に変えればいけるんじゃね? と思いコピペ&修正で新しいポリシーをJSONで作ってみる。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "rds:Describe*",
                "rds:ListTagsForResource",
                "ec2:DescribeAccountAttributes",
                "ec2:DescribeAvailabilityZones",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeVpcs"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:rds:ap-northeast-1:999999999999:db:rds-postgresql-test1",
                "arn:aws:rds:ap-northeast-1:999999999999:db:rds-postgresql-test2"
            ]
        },
        {
            "Action": [
                "cloudwatch:GetMetricStatistics",
                "logs:DescribeLogStreams",
                "logs:GetLogEvents"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

だめでした

このポリシーを付けたユーザでマネジメントコンソールにログインしたところ、怪しいメッセージが・・・。

アカウントの属性の取得に失敗しました。特定のコンソール機能に障害が発生している可能性があります。

RDSのインスタンス一覧を表示したところ、また違うエラーメッセージが。そして、一覧には何も表示されず。

User: arn:aws:iam::999999999999:user/testuser1 is not authorized to perform: rds:DescribeDBInstances 
(Service: AmazonRDS; Status Code: 403; Error Code: AccessDenied; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)

その後、JSONの構造を変えてみたり、アクション名を足したり引いたりワイルドカードやめてみたりとかしたけど、何やってもエラーは変わらず。
もしかして、メッセージ通り何かの障害かもしれないと思い、サポートに問い合わせることに。

サポートの回答来た

英語で問い合わせたので、回答も英語。Google自動翻訳が以下の通り。

ポリシーのDescribe*権限は、コンソールからアクセスしたときのリソースレベル権限をサポートしていません。リソースレベルの権限とは、ユーザーがアクションを実行することを許可されているリソースを指定する機能を指します。サポートされていないアクション/権限は、すべてのリソース ‘*’で許可または拒否する必要があります。

たとえば、 ‘rds:DescribeDBInstances’を実行する権限があると言われているエラーの場合、 ‘rds-postgresql-test1’というリソースでこれを許可し、RDSコンソールを開くと、エラーが発生します。この理由は、RDSコンソールがすべてのリソースを記述し、単一のDBインスタンスだけを記述することができないためです。したがって、コンソールを介して ‘rds:DescribeDBInstances’を呼び出すには、すべてのリソース ‘*’にアクセス権を許可する必要があります。

ということで、全てのアクションでリソース指定ができるわけじゃない、ということがわかりました。
DeleteDBInstanceのアクションとかだと指定できるみたいです。編集系だといけるんでしょうか。
AWSのドキュメントにも少し書いてました。

IAM と連携する AWS サービス

API がリソースレベルのアクセス権限をサポートしない場合、ポリシーのそのステートメントは Resource 要素で * を使用する必要があります。

可否の一覧とか無いと思ってググったけど、いい感じのは見つけられなかった。
そういうやり方ができない予感はしていたけど、出来ればポリシー作成時にエラーだして欲しかったぜ。

代替案あったら教えて

結局あきらめてすべてのRDSインスタンスのリードオンリー権限で作りました。
もしかしたら、タグで切り分けるとか他に方法があるかもしれません。
誰か知っていたら教えてください。

続きを読む