ALB(Application Load Balancer)でWebサービスを冗長化する

概要

ALBを使ってアプリケーションを冗長化する手順です。

HTTPS接続でアプリケーションにアクセス出来るところまでをこの記事で紹介します。

前提条件

以下の事前条件が必要です。

  • VPCの作成を行っておく
  • 最低でも2台のWebサーバインスタンスを起動させておく事
  • ロードバランサー用サブネットの作成が行われている事(後で説明します。)

事前準備その1(ロードバランサー用サブネットの作成)

以下は公式サイトに書かれている内容です。

ロードバランサーのアベイラビリティーゾーンを指定します。ロードバランサーは、これらのアベイラビリティーゾーンにのみトラフィックをルーティングします。アベイラビリティーゾーンごとに 1 つだけサブネットを指定できます。ロードバランサーの可用性を高めるには、2 つ以上のアベイラビリティーゾーンからサブネットを指定する必要があります。

今回検証で利用している東京リージョンには ap-northeast-1aap-northeast-1c の2つのアベイラビリティーゾーンが存在するので、それぞれでサブネットの作成を行います。

サービス → VPC → サブネット → 「サブネットの作成」より作成を行います。

ap-northeast-1a で サブネットを作成します。
以下のように入力を行います。

  • ネームタグ

    • account_api_alb_1a
    • 開発環境アカウント用APIのALB用と分かる名前を付けています。分かりやすい名前であれば何でも構いません。
  • VPC

    • 利用対象となるVPCを選択します。
  • IPv4 CIRD block

    • 192.0.30.0/24
    • ネットワークの設計方針にもよりますが今回は 192.0.30.0/24 を割り当てます。

alb_subnet_step1.png

続いて ap-northeast-1c でも同じ要領でサブネットを作成します。
※先程とほとんど同じなので、入力内容に関しての詳細は省略します。

alb_subnet_step2.png

事前準備その2(SSLの証明書の用意)

SSLで接続を可能にするのでSSL証明書の用意が必要です。

今回は検証なので自己証明書を利用する事にします。

以前、LAMP 環境構築 PHP 7 MySQL 5.7(前編) という記事を書きました。

こちらに載っている手順を参考に自己証明書を用意します。

ALB(Application Load Balancer)の新規作成

ここからが本題になります。
サービス → EC2 → ロードバランサー → ロードバランサーの作成 を選択します。

alb_step1.png

Step1 ロードバランサーの設定

基本的な設定を行っていきます。
名前を入力します。(今回はaccount-api-alb)という名前を付けました。

インターネットに公開するサービスを想定しているので、スキーマは「インターネット向け」を選択します。

ロードバランサーのプロトコルにHTTPSを追加します。

alb_step2-1.png

アベイラビリティーゾーンに先程作成したサブネットを割り当てます。

alb_step2-2.png

Step2 セキュリティ設定の構成

SSL証明書の設定を行います。

alb_step2-3.png

証明書の名前は分かりやすい名前でOKです。

プライベートキーには事前準備で作成した、プライベートキーを入れます。
-----BEGIN RSA PRIVATE KEY----- から -----END RSA PRIVATE KEY----- までを全てコピーして下さい。

パブリックキー証明書には -----BEGIN CERTIFICATE----- から -----END CERTIFICATE----- までの内容を全てコピーして下さい。

セキュリティポリシーは ELBSecurityPolicy-2016-08 を選択します。

※2017-05-22 現在、この手順で問題なく証明書の追加が出来るハズなのですが Certificate not found というエラーが発生しロードバランサーの作成に失敗してしまいます。

証明書のアップロードを aws-cli を使って事前に実施するようにしたら上手く行きました。

証明書のアップロード
aws iam upload-server-certificate --server-certificate-name self-certificate --certificate-body file://crt.crt --private-key file://private.key

file:// を付けるのがポイントです。これがないと上手くアップロード出来ませんでした。

--server-certificate-name には任意の名前を入力して下さい。

上手く行くと下記のようなレスポンスが返ってきます。

証明書アップロードのレスポンス
{
    "ServerCertificateMetadata": {
        "ServerCertificateId": "XXXXXXXXXXXXXXXXXXXXX",
        "ServerCertificateName": "self-certificate",
        "Expiration": "2018-05-22T04:14:02Z",
        "Path": "/",
        "Arn": "arn:aws:iam::999999999999:server-certificate/self-certificate",
        "UploadDate": "2017-05-22T05:58:44.754Z"
    }
}

アップロード完了後に「AWS Identity and Access Management(IAM)から、既存の証明書を選択する」を選んで先程アップロードした証明書を選択して下さい。

alb_step2-3.1.png

この問題については 既存の ELB に SSL 証明書を追加しようとすると Server Certificate not found for the key というエラーになる件の解決方法 を参考にさせて頂きました。

Step3 セキュリティグループの設定

セキュリティグループの設定を行います。

alb_step2-4.png

Step4 ルーティングの設定

ターゲットグループの新規作成を行います。

alb_step2-5.png

名前、プロトコル、ヘルスチェック用のURLの設定等を行います。

Step5 ターゲットの登録

ロードバランサーの配下で起動するインスタンスを選択します。

alb_step2-6.png

作成に必要な情報入力は以上となります。

確認画面に進み作成を行いしばらくすると、ロードバランサーが作成され利用可能な状態となります。

※サービス → EC2 → ロードバランサー より確認が出来ます。

alb_step3.png

動作確認

サービス → EC2 → ロードバランサー よりDNSが確認出来るので、動作確認を行います。

curl -kv https://account-api-alb-000000000.ap-northeast-1.elb.amazonaws.com/
*   Trying 0.0.0.0...
* TCP_NODELAY set
* Connected to account-api-alb-000000000.ap-northeast-1.elb.amazonaws.com (0.0.0.0) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: system
> GET / HTTP/1.1
> Host: account-api-alb-000000000.ap-northeast-1.elb.amazonaws.com
> User-Agent: curl/7.51.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Mon, 22 May 2017 07:26:02 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: nginx/1.12.0
< X-Request-Id: 76c7e41f-1a4e-4328-972c-b98055e84395
< Cache-Control: no-cache, private
<
* Curl_http_done: called premature == 0
* Connection #0 to host account-api-alb-000000000.ap-northeast-1.elb.amazonaws.com left intact
{"code":404,"message":"Not Found"}

各Webサーバのログを確認すると、処理が振り分けられているのが、確認出来ます。

本番環境での運用に向けて

ここまで簡単に作成が出来ましたが実環境で運用を行うにはまだまだ考慮が必要な点が多いです。

  • SSL証明書を正式な物にする(自己証明書で運用とかはさすがに厳しいと思います)
  • 独自ドメインでのアクセスを可能にする
  • 各EC2のログに記載されているIPがロードバランサーの物になっている

※これらの手順は順次行っていく予定ですので、準備が出来次第記事を書く予定です。

最後まで読んで頂きありがとうございました。

続きを読む

AWS ECSにてカスタムしたredmineのdockerイメージを動かすまでのメモ(その1)

redmineの構築、プラグインの導入をふつーにやると面倒くさい。
あと、一旦構築した後redmineのバージョンをあげるのもやっぱり面倒くさい。

→ので、dockerにてプラグインのインストールやらなにやらを手順をコード化して簡単にRedmineの導入が
できるようにしました。
なお動作環境はAWSのECS(Elastic Container Service)を使います。

大きな流れ

1.Dockerfileを用意
2.AWSにてElastic Container Service(ECS)のタスクを定義
3.ECSのインスタンスを用意

今回はまず1を用意します。

1.Dockerfileを用意

redmineの公式イメージがdockerhubにあるのでこれをもとにpluginを導入する手順を
dockerfile化していきます。

ポイントは2つです。
1.ベースのイメージはredmine:x.x.x-passengerを利用する
2.DBのマイグレーションが必要ないものはpluginsフォルダに配置し、マイグレーションが必要なものは別フォルダ(install_plugins)に配置。
→コンテナ起動時にマイグレーションを行うようdocker-entrypoint.shに記載しておきます。

インストールするプラグイン一覧

独断と偏見で入れたプラグインです。

No プラグインの名前 概要
1 gitmike githubの雰囲気のデザインテーマ
2 backlogs スクラム開発でおなじみ。ストーリーボード
3 redmine_github_hook redmineとgitを連動
4 redmine Information Plugin redmineの情報を表示可能
5 redmine Good Job plugin チケット完了したら「Good Job」が表示
6 redmine_local_avatars アイコンのアバター
7 redmine_startpage plugin 初期ページをカスタマイズ
8 clipboard_image_paste クリップボードから画像を添付できる 
9 Google Analytics Plugin 閲覧PV測定用
10 redmine_absolute_dates Plugin 日付を「XX日前」 ではなくyyyy/mm/ddで表示してくれる
11 sidebar_hide Plugin サイドバーを隠せる
12 redmine_pivot_table ピボットテーブルできる画面追加
13 redmine-slack 指定したslack channnelに通知可能
14 redmine Issue Templates チケットのテンプレート
15 redmine Default Custom Query 一覧表示時のデフォルト絞り込みが可能に
16 redmine Lightbox Plugin 2 添付画像をプレビューできる
17 redmine_banner Plugin 画面上にお知らせを出せます
18 redmine_dmsf Plugin フォルダで文書管理できる
19 redmine_omniauth_google Plugin googleアカウントで認証可能
20 redmine view customize Plugin 画面カスタマイズがコードで可能

Dockerfile

上記のプラグインをインストール済みとするためのDockerfileです。
なお、ベースイメージは最新(2017/05/19時点)の3.3.3を使用しています。

Dockerfile
FROM redmine:3.3.3-passenger
MAINTAINER xxxxxxxxxxxxxxxxx

#必要コマンドのインストール
RUN apt-get update -y 
 && apt-get install -y curl unzip ruby ruby-dev cpp gcc libxml2 libxml2-dev 
  libxslt1-dev g++ git make xz-utils xapian-omega libxapian-dev xpdf 
  xpdf-utils antiword catdoc libwpd-tools libwps-tools gzip unrtf 
  catdvi djview djview3 uuid uuid-dev 
 && apt-get clean

#timezoneの変更(日本時間)
RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
ENV RAILS_ENV production

#gitmakeテーマのインストール
RUN cd /usr/src/redmine/public/themes 
 && git clone https://github.com/makotokw/redmine-theme-gitmike.git gitmike 
 && chown -R redmine.redmine /usr/src/redmine/public/themes/gitmike



#redmine_github_hookのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/koppen/redmine_github_hook.git

#Redmine Information Pluginのインストール
RUN curl http://iij.dl.osdn.jp/rp-information/57155/rp-information-1.0.2.zip > /usr/src/redmine/plugins/rp-information-1.0.2.zip 
 && unzip /usr/src/redmine/plugins/rp-information-1.0.2.zip -d /usr/src/redmine/plugins/ 
 && rm -f /usr/src/redmine/plugins/rp-information-1.0.2.zip

#Redmine Good Job pluginのインストール
RUN curl -L https://bitbucket.org/changeworld/redmine_good_job/downloads/redmine_good_job-0.0.1.1.zip > /usr/src/redmine/plugins/redmine_good_job-0.0.1.1.zip 
 && unzip /usr/src/redmine/plugins/redmine_good_job-0.0.1.1.zip -d /usr/src/redmine/plugins/redmine_good_job 
 && rm -rf /usr/src/redmine/plugins/redmine_good_job-0.0.1.1.zip

#redmine_startpage pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/txinto/redmine_startpage.git

#Redmine Lightbox Plugin 2 Pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/peclik/clipboard_image_paste.git

#Google Analytics Pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/paginagmbh/redmine-google-analytics-plugin.git google_analytics_plugin

#redmine_absolute_dates Pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/suer/redmine_absolute_dates

#sidebar_hide Pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/bdemirkir/sidebar_hide.git

#redmine_pivot_tableのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/deecay/redmine_pivot_table.git

#redmine-slackのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/sciyoshi/redmine-slack.git redmine_slack

#Redmine Issue Templates Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/akiko-pusu/redmine_issue_templates.git redmine_issue_templates

#Redmine Default Custom Query Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/hidakatsuya/redmine_default_custom_query.git redmine_default_custom_query

#Redmine Lightbox Plugin 2 Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/paginagmbh/redmine_lightbox2.git redmine_lightbox2

#redmine_banner Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/akiko-pusu/redmine_banner.git redmine_banner

#redmine_dmsf Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/danmunn/redmine_dmsf.git redmine_dmsf

#redmine_omniauth_google Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/yamamanx/redmine_omniauth_google.git redmine_omniauth_google

#redmine_omniauth_google Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/onozaty/redmine-view-customize.git view_customize

#redmine_local_avatars用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/ncoders/redmine_local_avatars.git

#backlogsのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN mkdir /usr/src/redmine/install_plugins 
 && cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/AlexDAlexeev/redmine_backlogs.git 
 && cd /usr/src/redmine/install_plugins/redmine_backlogs/ 
 && sed -i -e '11,17d' Gemfile

#database.ymlファイルを置くフォルダを用意
RUN mkdir /config
COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]

EXPOSE 3000
CMD ["passenger", "start"]

docker-entrypoint.sh

次にdocker-entrypoint.shファイルです。
githubに公開されているファイル
(https://github.com/docker-library/redmine/blob/41c44367d9c1996a587e2bcc9462e4794f533c15/3.3/docker-entrypoint.sh)
を元にプラグインのインストールを行うコードを記載していきます。

docker-entrypoint.sh
#!/bin/bash
set -e

case "$1" in
    rails|rake|passenger)
        if [ -e '/config/database.yml' ]; then
                    if [ ! -f './config/database.yml' ]; then
                echo "use external database.uml file"
                ln -s /config/database.yml /usr/src/redmine/config/database.yml
            fi
        fi
                if [ -e '/config/configuration.yml' ]; then
                        if [ ! -f './config/configuration.yml' ]; then
                                echo "use external configuration.uml file"
                                ln -s /config/configuration.yml /usr/src/redmine/config/configuration.yml
                        fi
                fi
        if [ ! -f './config/database.yml' ]; then
            if [ "$MYSQL_PORT_3306_TCP" ]; then
                adapter='mysql2'
                host='mysql'
                port="${MYSQL_PORT_3306_TCP_PORT:-3306}"
                username="${MYSQL_ENV_MYSQL_USER:-root}"
                password="${MYSQL_ENV_MYSQL_PASSWORD:-$MYSQL_ENV_MYSQL_ROOT_PASSWORD}"
                database="${MYSQL_ENV_MYSQL_DATABASE:-${MYSQL_ENV_MYSQL_USER:-redmine}}"
                encoding=
            elif [ "$POSTGRES_PORT_5432_TCP" ]; then
                adapter='postgresql'
                host='postgres'
                port="${POSTGRES_PORT_5432_TCP_PORT:-5432}"
                username="${POSTGRES_ENV_POSTGRES_USER:-postgres}"
                password="${POSTGRES_ENV_POSTGRES_PASSWORD}"
                database="${POSTGRES_ENV_POSTGRES_DB:-$username}"
                encoding=utf8
            else
                echo >&2 'warning: missing MYSQL_PORT_3306_TCP or POSTGRES_PORT_5432_TCP environment variables'
                echo >&2 '  Did you forget to --link some_mysql_container:mysql or some-postgres:postgres?'
                echo >&2
                echo >&2 '*** Using sqlite3 as fallback. ***'

                adapter='sqlite3'
                host='localhost'
                username='redmine'
                database='sqlite/redmine.db'
                encoding=utf8

                mkdir -p "$(dirname "$database")"
                chown -R redmine:redmine "$(dirname "$database")"
            fi

            cat > './config/database.yml' <<-YML
                $RAILS_ENV:
                  adapter: $adapter
                  database: $database
                  host: $host
                  username: $username
                  password: "$password"
                  encoding: $encoding
                  port: $port
            YML
        fi

        # ensure the right database adapter is active in the Gemfile.lock
        bundle install --without development test
        if [ ! -s config/secrets.yml ]; then
            if [ "$REDMINE_SECRET_KEY_BASE" ]; then
                cat > 'config/secrets.yml' <<-YML
                    $RAILS_ENV:
                      secret_key_base: "$REDMINE_SECRET_KEY_BASE"
                YML
            elif [ ! -f /usr/src/redmine/config/initializers/secret_token.rb ]; then
                rake generate_secret_token
            fi
        fi
        if [ "$1" != 'rake' -a -z "$REDMINE_NO_DB_MIGRATE" ]; then
            gosu redmine rake db:migrate
        fi

        chown -R redmine:redmine files log public/plugin_assets

        if [ "$1" = 'passenger' ]; then
            # Don't fear the reaper.
            set -- tini -- "$@"
        fi
                if [ -e /usr/src/redmine/install_plugins/redmine_backlogs ]; then
                        mv -f /usr/src/redmine/install_plugins/redmine_backlogs /usr/src/redmine/plugins/
                        bundle update nokogiri
                        bundle install
                        bundle exec rake db:migrate
                        bundle exec rake tmp:cache:clear
                        bundle exec rake tmp:sessions:clear
            set +e
                        bundle exec rake redmine:backlogs:install RAILS_ENV="production"
                        if [ $? -eq 0 ]; then
                echo "installed backlogs"
                                touch /usr/src/redmine/plugins/redmine_backlogs/installed
            else
                echo "can't install backlogs"
                        fi
            set -e
            touch /usr/src/redmine/plugins/redmine_backlogs/installed
        fi
                if [ -e /usr/src/redmine/install_plugins/redmine_local_avatars ]; then
                        mv -f /usr/src/redmine/install_plugins/redmine_local_avatars /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
                fi
        if [ -e /usr/src/redmine/install_plugins/redmine_slack ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_slack /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_issue_templates ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_issue_templates /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_default_custom_query ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_default_custom_query /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_lightbox2 ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_lightbox2 /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_banner ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_banner /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_dmsf ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_dmsf /usr/src/redmine/plugins/
            bundle install --without development test xapian
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_omniauth_google ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_omniauth_google /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/view_customize ]; then
            mv -f /usr/src/redmine/install_plugins/view_customize /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ ! -f '/usr/src/redmine/plugins/redmine_backlogs/installed' ]; then
            set +e
            bundle exec rake redmine:backlogs:install RAILS_ENV="production"
            if [ $? -eq 0 ]; then
                echo "installed backlogs"
                touch /usr/src/redmine/plugins/redmine_backlogs/installed
            else
                echo "can't install backlogs"
            fi
            set -e
        fi
        set -- gosu redmine "$@"
                ;;
esac

exec "$@"

次はこのイメージをAWSのECSにて動作させます。
(次回に続く)

続きを読む

Amazon Auroraクラスタへの接続にコネクションプーリングを使うときのフェイルオーバー対応

Amazon Auroraでは、フェイルオーバーが発生してWriterとReaderが切り替わる場合、サーバ自体のIPアドレスは変わらず、クラスタエンドポイントおよび読み取りエンドポイントのFQDN(CNAME相当)が指すIPアドレスが入れ替わります。

また、エンドポイントが指すIPアドレスは、フェイルオーバー発生時に数回フラッピングします。

そのため、アプリケーションサーバからコネクションプーリングを使ってAmazon Auroraに接続していると、意図せずWriterからReaderに「降格」した側のサーバ(レプリカ)に再接続してしまい、更新系クエリがエラーになってしまうことがあります。

※Readerへの接続を意図して誤ってWriterに接続されてしまうことは、ある程度までなら許容できると思いますが。

これを避けるため、コネクションプーリングをやめて都度接続に変更するか、もしくは(Javaであれば)MariaDB Connector/Jのような特別なフェイルオーバー対応処理が入ったコネクターを使ってプーリングすることにするのが一般的だと思いますが、

  • Javaではなく、プーリングがないと速度的にキツい(特に、アプリケーション処理の実装の都合上、コネクションの取得~クローズを細かい単位に区切って行っている場合)
  • Javaだが、細かい挙動がMariaDB Connector/JとMySQL Connector/Jで違うために、MariaDB Connector/Jを採用できない

など、どちらの回避策を取るのも難しい場合の対処法を考えてみました。

コネクションプーリングのValidationクエリ

コネクションプーリング環境では、プールされているコネクションが実際に利用可能かチェックするために、プールからコネクションをBorrowする際のValidationクエリを設定できるのが一般的です。
通常、MySQLでは、Validationクエリに「SELECT 1」(または「/* ping */ SELECT 1」など)を指定しますが、

  • DB接続に使うユーザに「EXECUTE」権限を付与しておく
  • information_schema経由でGLOBAL変数「innodb_read_only」を取得し、「OFF」なら「1」を返し、「ON」なら接続エラーを返すストアドファンクションを定義しておく
  • Validationクエリに↑のストアドファンクションを呼び出すSQLを記述する

という方法で、プールからのBorrow時に、誤ってReaderに接続されたコネクションを閉じて再接続するようにします。

※Tomcatで「Tomcat JDBC Pool」を使う場合は、Validationクエリの代わりにValidation用のクラスを定義して、同じようなことをすることも可能です。

Validation用ストアドファンクションを定義する

まず、ストアドファンクションを設置するデータベースを作成します。DB接続するユーザにEXEC権限も付けておきます。

DB作成(例:concheck)・GRANT
mysql> CREATE DATABASE concheck;
mysql> GRANT EXECUTE ON concheck.* TO '接続ユーザ名'@'IPアドレス範囲等';

バイナリログを有効にしている場合は、非決定的なクエリの実行を許可する状態にします。

バイナリログ有効の場合
mysql> SET GLOBAL log_bin_trust_function_creators = 1;

ストアドファンクションを定義します。

ストアドファンクション
mysql> DELIMITER //
mysql> CREATE FUNCTION concheck.validation() RETURNS INT NOT DETERMINISTIC
    -> BEGIN
    -> SELECT @@GLOBAL.innodb_read_only INTO @flag;
    ->   IF @flag = 'OFF' THEN
    ->     RETURN 1;
    ->   ELSE
    ->     SIGNAL SQLSTATE '08S01'
    ->       SET MESSAGE_TEXT = 'A handshake error occured', MYSQL_ERRNO = 1043;
    ->   END IF;
    -> END;
    -> //
mysql> DELIMITER ;

コネクションプーリングの設定でValidationクエリを指定する

例えば、Apache Commons DBCP(DBCP2)の場合は、「validaitonQuery」に「SELECT concheck.validation()」を指定し、「testOnBorrow」に「true」を指定します。
その他、各種タイムアウト値(「validationQueryTimeout」、「maxWaitMillis」など)やプールされているコネクションの検査・異常コネクションの除去に関する設定値も必要に応じて調整します。

※MySQL Connector/Jを使う場合は、接続用URL中に「connectTimeout」および「socketTimeout」を適切に指定します(いずれも単位はミリ秒)。

制限(限界)

使用するコネクションプーリングの種類やサーバの負荷状況等によっては、これだけでは対応が不十分な場合もあります。

例えば、Apache Commons DBCP2では、「maxTotal」や「maxIdle」を大きな値(無制限を示す「-1」を含む)に指定していたとしても、異常コネクションを除去(Eviction)する設定にしていないとフェイルオーバー後のコネクション数が十分に回復しないことがありますし(場合によってはすべてのコネクションがリークした時と同様のロック状態になります)、除去する設定にした場合も、除去を早めるために処理頻度を高くすると、Webアプリケーションサーバの処理が追いつかなくなる(ロック状態ではないが、リクエストがキャンセルされて負荷が下がるまでまともな時間内に応答できない状態になる)ことがあります。

その場合は、異常コネクションの検査・除去については処理頻度を控えめにしておく一方、「二次対応」として「エラーログ等を確認し、必要に応じてWebアプリケーションサーバの再起動やWebアプリケーションのリロードをする」ような外部処理を実装しておくことになると思います。

続きを読む

IAM認証RDS�・非VPC Lambda・API Gateway・CloudFront・WAFで接続元IP制限付きAPIを作成する

主に以下2例に対して情報を少し追加するだけの記事です。

API GatewayのバックをVPC Lambdaにしている場合、APIのくせに返答に時間がかかる場合がある、という問題があります。なので、

  • 非VPC Lambdaから安全にPublic AccessibleなRDSに接続したい
  • かつ、API Gatewayに接続元IP制限をつけたい

というのがやりたいことです。

RDS接続認証でIAMが使えるようになった

執筆時点で公式日本語ドキュメントは無し。

つまりどういうこと?

MySQLの認証プラグインを作ったからRDSイメージにいれておいたよー、という話らしいので見てみる。

select * from plugin;
+-------------------------+-------------+
| name                    | dl          |
+-------------------------+-------------+
| AWSAuthenticationPlugin | aws_auth.so |
+-------------------------+-------------+
1 row in set (0.01 sec)

同じものをPostgreSQLでも作ってくれればPostgreSQLでもIAM認証が使える用になるんですよね? (よく知らないまま希望)

それのなにがうれしいの?

MySQLのuser/password認証がAWS_ACCESS_KEY_IDとAWS_ACCESS_SECRET_KEYに置き換わったような感じです。

……って言っちゃうと「それはそう」案件になるんですが、特定のIAMロールを持つ非VPC Lambdaからのみのアクセスを許可するようなRDS MySQLを作ることができる、というのが特にうれしい点です。

MySQL組み込みの認証機構よりは比較的安全な方法で、RDSをPublic Accessibleにできるようになる、とも言います。RDSがPublic Accessibleなら、Lambdaも非VPCでよくなります。しかもそのLambdaも特定のIAMロールを持つものだけに制限できます。安心だ。

やりたいこと

RDS (IAM & SSL) | AWS Lambda (IAM) | API Gateway (https) | CloudFront (https) | Web Application Firewall

rds-iam.png

の構成を作りたい。社内APIとかですな。なんか登場人物が多いですが、以下の事情があります

  • ソースIP制限をつけたいが、API Gateway自体にソースIP制限機能がない
  • WAFにはあるが、WAFはALBかCloudFrontにしか使えない

よって、

  • CloudFrontをAPI Gatewayの前段に置く
  • WAFをCloudFrontの前段に置く

ことでアクセスをコントロールします。

API Gateway自体に関しては、API Tokenを必須にすることでアクセスを制限します。もちろんCloudFrontからはアクセスできないといけないので、Origin設定でAPI Tokenをヘッダに追加する必要があります。ややこしいなおい。

ちなみに、API Gatewayの認証をIAMにすることもできるんですが、これだとAPIユーザーにAWS v4 Signature Authを実装させることになるので心苦しいです。

うっわめんどくさっ。

やってみよう

RDSをつくる

IAM Database Authentication for MySQL and Amazon Aurora

IAM認証はdb.m1.smallより大きいインスタンスのみでしか使えないので注意。ユーザーの作り方もドキュメントのままですが、

CREATE USER jane_doe IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS' REQUIRE SSL;

DBとLambda間はSSLにしないといけないので、REQUIRE SSLオプションを付けてSSLを強制します。

Lambdaのレンジ:3306のInをAllowしたSecGroupを作る、つってもLambdaのレンジってめっちゃ広そう?
https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-ip-ranges.html

Lambdaをつくる

試しに作ったLambdaは

from __future__ import print_function

import os

import boto3
import mysql.connector

def lambda_handler(event, context):
    print(event, context)
    token = boto3.client('rds').generate_db_auth_token(
        DBHostname=os.environ['RDS_HOST'],
        Port=3306,
        DBUsername=os.environ['RDS_USER']
    )
    conn = mysql.connector.connect(
        host=os.environ['RDS_HOST'],
        user=os.environ['RDS_USER'],
        password=token,
        database=os.environ['RDS_DATABASE'],
        ssl_verify_cert=True,
        ssl_ca='rds-combined-ca-bundle.pem'
    )
    cursor = conn.cursor()
    cursor.execute('SELECT user FROM mysql.user')
    rows = cursor.fetchall()
    result = [str(x[0]) for x in rows]
    return {'users': result}

DBのユーザーをSELECTして返すだけのものです。

Lambda実行時のIAMロールにIAM Database Authポリシーが必要になります。

Attaching an IAM Policy Account to an IAM User or Role

arn:aws:rds-db:region:account-id:dbuser:dbi-resource-id/database-user-name

ここでMySQLのGRANTONTOを指定しているみたいなものと考える。

別のIAMロールを付けてLambdaを実行してみると、みごとにDB接続エラーが発生する。

Lambdaをzipでまとめる

Dockerfileを作ってzipを生成するナウでヤングな最先端のイカしたアレだぜ。デプロイ時に非VPCを選択。

FROM amazonlinux

RUN yum install -y python27-devel python27-pip zip
RUN pip install --upgrade pip
RUN mkdir /opt/rds-iam-auth /opt/build
COPY ./ /opt/rds-iam-auth/
WORKDIR /opt/rds-iam-auth
RUN pip install wheel
RUN pip install -r requirements.txt -t .
RUN zip -r rds-iam-auth.zip *

CMD cp rds-iam-auth.zip /opt/build

悲しいかなLambdaのamazonlinuxに入っているboto3がIAM RDS authに未対応 (執筆時) だったのでrequirement.txtに追記。近々AMIもアップデートされるだろう。

その他をつくる

コンソールからぽちぽちやる作業がたくさんあります。特にここで追記することはないので、先人の記事を参照したいところだ。

注意点まとめ

  • APIエンドポイントを直接使用されないように、API Tokenを必須にしておくこと。
  • WAFはGlobalリージョンで作成しないとCloudFrontのコンソールから選べないので注意。
  • CloudFrontのOrigin設定時に、x-api-tokenヘッダ転送設定を追加すること。
  • CloudFrontの設定反映には時間がかかるの辛いコーヒー飲むしかない。

ためす

WAFで社内のGlobal IPのみアクセスを許可。

$ curl https://hoge.cloudfront.net/
{"users": ["helloworld", "iamuser1", "mysql.sys", "rdsadmin"]}

WiFi変えたりとかして、Global IPを変えてみると

$ curl https://hoge.cloudfront.net/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: jIG5TSJUn9rPZYI0JwUr9wZHFHFa_LRyVmwGC302sHjBgv0sLoPubA==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>

Forbiddenされる。

まとめ

AWSよくできてんなー。

続きを読む

Rails4+Capistrano3+Nginx+Unicorn EC2へのデプロイ < 後編 >

インフラ勉強中の者がAWSにrailsアプリをEC2にデプロイしたので備忘録として残します。
< 後編 >です。

【イメージ】
スクリーンショット 2017-05-09 11.04.30.png

< 前編 >Unicorn の設定までを実施しました。

デプロイチェック

ローカル
$ bundle exec cap production deploy:check

デプロイチェックをすると下記のようなエラーが出るはずです。

~ 省略 ~
00:02 deploy:check:linked_files
ERROR linked file /var/www/deployApp/shared/config/database.yml does not exist on YOUR_IP_ADDRESS

/var/www/deployApp/shared/config/database.yml が存在しないためのエラーです。

shared ディレクトリは、Capistranoが自動生成するディレクトリです。
EC2の qiitaApp の配下を確認します。

EC2
[myname@ip-10-0-1-86 qiitaApp]$ ls -la
drwxrwxr-x 4 ec2-user ec2-user 4096  5  9 04:08 .
drwxrwxrwx 3 root     root     4096  5  9 04:06 ..
-rw-rw-r-- 1 ec2-user ec2-user    6  5  9 03:33 .ruby-version
drwxrwxr-x 2 ec2-user ec2-user 4096  5  9 04:08 releases ← 自動生成
drwxrwxr-x 7 ec2-user ec2-user 4096  5  9 04:08 shared ← 自動生成

config 配下に database.yml を生成すればエラーは出なくなるのですが、次に同じようにsecrets.ymlが存在しないというエラーが出ますので、共に生成して変更します。

EC2上にdatabase.ymlの生成の前にローカルの database.yml を変更します。

ローカル
$ vi config/database.yml 

======================= database.yml ================================

~ 省略 ~
#production:
#  <<: *default
#  database: db/production.sqlite3
   ↓↓↓
production:
  <<: *default
  adapter: mysql2
  database: qiita_db
  host: qiita_db.xxxxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com                                                                                                                                                     
  username: qiita_user
  password: YOUR_DB_PASS
  encoding: utf8

====================================================================

変更したら database.yml 全体をコピーして保存します。

EC2
[ec2-user@ip-10-0-1-86 qiitaApp]$ cd shared/config/
[ec2-user@ip-10-0-1-86 config]$ touch database.yml
[ec2-user@ip-10-0-1-86 config]$ vi database.yml

======================= database.yml ===============================

 ローカルでコピーしたdatabase.ymlを貼り付けます。

====================================================================

同様に secrets.yml もローカルの中身をコピーして(変更はなし)、configの配下に secrets.yml を生成して貼り付けます。

EC2
[ec2-user@ip-10-0-1-86 config]$ touch secrets.yml
[ec2-user@ip-10-0-1-86 config]$ vi secrets.yml

再度、デプロイチェックを実施します。

ローカル
$ bundle exec cap production deploy:check

エラーが出なければdeploy:checkは完了です。

デプロイ

ローカル
$ bundle exec cap production deploy --trace #traceをつけることでバグの箇所を発見しやすくします

エラーが出なければデプロイ完了です。

今回は、scaffoldで簡単なUserアプリを生成しているので、 直接、http://IP_ADDRESS/users/ を叩いて確認してみます。

…おそらく500エラーが返ってきてしまって確認できないと思います。

Unicornの状態とエラーログを確認します。

EC2
[ec2-user@ip-10-0-1-86 config]$ ps ax | grep unicorn
30256 ?        Sl     0:01 unicorn master -c /var/www/qiitaApp/current/config/unicorn.rb -E deployment -D                                             
30311 ?        Sl     0:00 unicorn worker[0] -c /var/www/qiitaApp/current/config/unicorn.rb -E deployment -D                                          
30313 ?        Sl     0:00 unicorn worker[1] -c /var/www/qiitaApp/current/config/unicorn.rb -E deployment -D                                          
30316 ?        Sl     0:00 unicorn worker[2] -c /var/www/qiitaApp/current/config/unicorn.rb -E deployment -D                                          
30334 pts/1    S+     0:00 grep --color=auto unicorn

Unicornは稼働していることが確認できます。
次はエラーログです。

EC2
[ec2-user@ip-10-0-1-86 config]$ cd ../..
[ec2-user@ip-10-0-1-86 qiitaApp]$ tail -f current/log/unicorn_error.log 

この状態で再度ローカルからデプロイします。

ローカル
[ec2-user@ip-10-0-1-86 qiitaApp]$ bundle exec cap production deploy
EC2
I, [2017-05-09T06:57:08.516736 #30256]  INFO -- : executing ["/var/www/qiitaApp/shared/bundle/ruby/2.3.0/bin/unicorn", "-c", "/var/www/qiitaApp/current/config/unicorn.rb", "-E", "deployment", "-D", {8=>#<Kgio::UNIXServer:fd 8>}] (in /var/www/qiitaApp/releases/20170509065649)
I, [2017-05-09T06:57:08.735844 #30256]  INFO -- : inherited addr=/tmp/unicorn.sock fd=8
I, [2017-05-09T06:57:08.736108 #30256]  INFO -- : Refreshing Gem list
I, [2017-05-09T06:57:09.978667 #30311]  INFO -- : worker=0 ready
I, [2017-05-09T06:57:09.981219 #30256]  INFO -- : master process ready
I, [2017-05-09T06:57:09.982336 #30313]  INFO -- : worker=1 ready
I, [2017-05-09T06:57:09.985331 #30316]  INFO -- : worker=2 ready
I, [2017-05-09T06:57:10.155786 #29111]  INFO -- : reaped #<Process::Status: pid 29166 exit 0> worker=0
I, [2017-05-09T06:57:10.155874 #29111]  INFO -- : reaped #<Process::Status: pid 29168 exit 0> worker=1
I, [2017-05-09T06:57:10.155945 #29111]  INFO -- : reaped #<Process::Status: pid 29171 exit 0> worker=2
I, [2017-05-09T06:57:10.155973 #29111]  INFO -- : master complete

この時点ではエラーらしきものが出ていません。

再度 http://IP_ADDRESS/users/ を叩いて確認してみます。

EC2
E, [2017-05-09T06:59:27.508190 #30316] ERROR -- : app error: Missing `secret_token` and `secret_key_base` for 'production' environment, set these values in `config/secrets.yml` (RuntimeError)

Missing secret_token and secret_key_base とあります。
EC2上の qiitaApp/current/config/secrets.yml の secret_key_base:の環境変数(SECRET_KEY_BASE)が設定されていないことが原因です。

EC2
[ec2-user@ip-10-0-1-86 qiitaApp]$ cd current
[ec2-user@ip-10-0-1-86 current]$ bundle exec rake secret
1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz ←コピーする
[ec2-user@ip-10-0-1-86 current]$ vi config/secrets.yml 

============================== secrets.yml ==================================

~ 省略 ~
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
          ↓↓↓
  secret_key_base: 1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz

=============================================================================

本来であれば、直接入力するのではなく、

SECRET_KEY_BASE=1234567890abcd...
export SECRET_KEY_BASE

とかで環境変数のまま使用すべきです。
が、何回実施してもググってもうまくいかなかったので、取り急ぎ直接入力で対応します。
dotenvというgemがあるようなので、それで対応してもいいと思います。

再度、ローカルからデプロイして、urlを叩けば下記の画面が表示されると思います。
スクリーンショット 2017-05-09 16.28.33.png

大変参考にさせていただきました。
capistranoを使ってrailsをnginx+unicorn+mysqlの環境にデプロイする
ローカルで開発したRailsアプリをCapistrano3でEC2にデプロイする
Capistrano3でUnicorn+Nginxな環境にRailsをデプロイする:初心者向け
Railsプロダクション環境 Unicornでsecret_key_baseが設定されていないとエラーが出る

続きを読む

Rails4+Capistrano3+Nginx+Unicorn EC2へのデプロイ < 前編 >

インフラ勉強中の者がAWSにrailsアプリをEC2にデプロイしたので備忘録として残します。
長いので<前編><後編>の2回に分けて実施致します。

【前提】
・今回はscaffoldで作成した簡単なアプリ(userの名前と年齢を登録するアプリ)をデプロイすることをゴールに進めていきます。
・デプロイのディレクトリは /var/www/qiitaAppです。
shared/config 配下のdatabase.ymlsecrets.yml は手動で作成します。
・ローカルへのRubyやRailsのインストールとEC2、RDSのセットアップ(VPCやセキュリティグループ等々の設定)、GitHubのアカウント登録に関しての説明は致しません。
・hostの設定、各環境毎の設定は実施しません。

Rubyのインストール
Ruby on Railsのインストールと設定
AWSの設定

【イメージ】
スクリーンショット 2017-05-09 11.04.30.png

【バージョン】

software version
Ruby 2.3.3
Rails 4.2.7
Nginx 1.10.2
Unicorn 5.3.0
MySql 5.6
Capistrano 3.8.1
capistrano3-unicorn 0.2.1

今回は、デプロイ時にUnicornの再起動もさせるべくcapistrano3-unicornというgemを使用します。
(自身でtaskとして設定する事も可能です。)

GitHubの設定

1)New Repositoryの生成
Repository name → qiitaApp
Public
Create repository

2)ローカルからSSH接続ができるように[SSH keys]を追加する
今回は説明は省略します。
(EC2からもGitHub接続が必要なのでそちらは下記に記載します。)
GitHub 初心者による GitHub 入門(1)

EC2(AmazonLinux)の設定

1) git,rbenvのインストール

[myname@ip-10-0-1-86 ~]$ sudo yum -y install git
[myname@ip-10-0-1-86 ~]$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv #rbenvインストール
[myname@ip-10-0-1-86 ~]$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build #ruby-buildインストール
[myname@ip-10-0-1-86 ~]$ sudo vi .bash_profile #.bash_profileの編集

=ファイルの編集画面=

export PATH
export PATH="$HOME/.rbenv/bin:$PATH" ← 追加
eval "$(rbenv init -)" ← 追加

=================

[myname@ip-10-0-1-86 ~]$ source ~/.bash_profile #環境変数の反映
[myname@ip-10-0-1-86 ~]$ rbenv -v #バージョン確認
rbenv 1.1.0-2-g4f8925a

2) Ruby2.3.3のインストール

[myname@ip-10-0-1-86 ~]$ sudo yum install -y gcc
[myname@ip-10-0-1-86 ~]$ sudo yum install -y openssl-devel readline-devel zlib-devel
[myname@ip-10-0-1-86 ~]$ rbenv install 2.3.3

3) Nginxのインストール・設定

[myname@ip-10-0-1-86 ~]$ sudo yum install nginx
[myname@ip-10-0-1-86 ~]$ cd /etc/nginx/conf.d/
[myname@ip-10-0-1-86 conf.d]$ sudo touch local.conf
[myname@ip-10-0-1-86 conf.d]$ sudo vi local.conf

==================== local.conf ===================================

upstream unicorn {
  server unix:/tmp/unicorn.sock;
}

server {
  listen 80;
  server_name YOUR_IP_ADDRESS;

  access_log /var/log/nginx/sample_access.log;
  error_log /var/log/nginx/sample_error.log;

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://unicorn;
  }
}

=================================================================

4) デプロイするディレクトリと権限の変更

[myname@ip-10-0-1-86 conf.d]$ cd /var/
[myname@ip-10-0-1-86 var]$ sudo mkdir www
[myname@ip-10-0-1-86 var]$ sudo chmod 777 www
[myname@ip-10-0-1-86 var]$ cd www
[myname@ip-10-0-1-86 www]$ mkdir qiitaApp
[myname@ip-10-0-1-86 www]$ cd qiitaApp
[myname@ip-10-0-1-86 qiitaApp]$ rbenv local 2.3.3

5) GitHubへの接続 SSHkey

[myname@ip-10-0-1-86 qiitaApp]$ cd 
[myname@ip-10-0-1-86 ~]$ ssh-keygen -t rsa
$ cat .ssh/id_rsa.pub
ssh-rsa abcdefghijklmnopqrstuvwxyz1234567890 ec2-user@ip-10-0-1-86

[ssh-rsa]から[ec2-user@ip-10-0-1-86]までをコピーしてGitHubのSSH keysにNewKeyとして追加する

5) その他必要なソフトウェアのインストール・設定

  • bundler
[myname@ip-10-0-1-86 ~]$ rbenv exec gem install bundler
[myname@ip-10-0-1-86 ~]$ rbenv rehash
  • Node.js
[myname@ip-10-0-1-86 ~]$ sudo yum install nodejs --enablerepo=epel
  • sqlite
[myname@ip-10-0-1-86 ~]$ sudo yum install sqlite-devel
  • mysql
[myname@ip-10-0-1-86 ~]$ sudo yum install mysql-devel

ローカルの設定

1) railsアプリの生成とgemのインストール

$ rails new qiitaApp
$ cd qiitaApp/
$ git init
$ vi Gemfile

============ Gemfile ===============

gem 'unicorn' #コメントアウトを解除する
gem 'mysql2'                                                                                                                                                                                                       
group :development do
  gem 'capistrano'
  gem 'capistrano-rails'
  gem 'capistrano-bundler'
  gem 'capistrano-rbenv'
  gem 'capistrano3-unicorn'
end

===================================

$ bundle install
$ rails g scaffold user name:string age:integer
$ rake db:migrate

2) Capistranoの設定に必要なファイルの生成

$ bundle exec cap install STAGES=production #自動で下記のディレクトリとファイルを生成
mkdir -p config/deploy
create config/deploy.rb
create config/deploy/production.rb
mkdir -p lib/capistrano/tasks
create Capfile
Capified

3) Capfileの設定

$ vi Capfile #下記を追加
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano3/unicorn'

4) config/deploy.rbの設定

$ vi config/deploy.rb

lock "3.8.1"

set :application, "qiitaApp"
set :repo_url, "git@github.com:YOUR_GITHUB_ACCOUNT/qiitaApp.git"

set :rbenv_type, :user
set :rbenv_ruby, '2.3.3'
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w{rake gem bundle ruby rails}
set :rbenv_roles, :all

set :log_level, :warn 

# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')

# Default value for linked_dirs is []
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')

# Default value for keep_releases is 5
set :keep_releases, 3

set :unicorn_pid, "#{shared_path}/tmp/pids/unicorn.pid"

set :unicorn_config_path, -> { File.join(current_path, "config", "unicorn.rb") }

namespace :deploy do
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end
end

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:restart'
  end
end    

5) config/deploy/production.rbの設定

set :branch, 'master'

server 'EC2-IP-ADDRESS', user: 'ec2-user', roles: %w{app db web} ※

set :ssh_options, {
    keys: %w(~/.ssh/YOUR_EC2_KEY.pem),
    forward_agent: true,
    auth_methods: %w(publickey)
  }

※今回はEC2-userでログインする設定で進めていきます。
本来はデプロイユーザを設定して、そのユーザのみがデプロイ可能にすべきだと思います。

7) Unicornの設定

手動で config 配下に unicorn.rb を生成

$ cd config
$ touch unicorn.rb
$ vi unicorn.rb

========================== unicorn ==============================

APP_PATH   = "#{File.dirname(__FILE__)}/.." unless defined?(APP_PATH)
RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
RAILS_ENV  = ENV['RAILS_ENV'] || 'development'

worker_processes 3

listen "/tmp/unicorn.sock"
pid "tmp/pids/unicorn.pid"

preload_app true

timeout 60
working_directory APP_PATH

# log
stderr_path "#{RAILS_ROOT}/log/unicorn_error.log"
stdout_path "#{RAILS_ROOT}/log/unicorn_access.log"

if GC.respond_to?(:copy_on_write_friendly=)
  GC.copy_on_write_friendly = true
end

before_exec do |server|
  ENV['BUNDLE_GEMFILE'] = APP_PATH + "/Gemfile"
end

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!

  old_pid = "#{ server.config[:pid] }.oldbin"
  unless old_pid == server.pid
    begin
      Process.kill :QUIT, File.read(old_pid).to_i
    rescue Errno::ENOENT, Errno::ESRCH

    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

=================================================================

ここまでで各種設定に関しては終了です。
shared/config 配下のdatabase.ymlsecrets.yml は手動で作成します。
が残っていますが、こちらは後編で実施します。

最後にGitHubにここまでのコードをあげておきます。

$ git add .
$ git commit -m "first commint(任意)"
$ git remote add origin git@github.com:YOUR_GITHUB_ACCOUNT/qiitaApp.git
$ ssh-add ~/.ssh/id_rsa_github
$ git push origin master

前編は以上です。
後編ではデプロイチェック、デプロイ、ログ確認を実施します。

続きを読む

【MySQL】AWS Aurora上でもgh-ostオンラインマイグレーション

【MySQL】gh-ostでオンラインマイグレーションの応用編です。

gh-ostはMySQLのバイナリログを使ってテーブルを同期します。
一方で、AWSのAmazon Auroraは、バイナリログを使わない方法でリードレプリカを作成します。

※Auroraのリードレプリカが一体どういう仕組みなのかここでは省きますが、過去のAWS SummitのDeep DiveとかでAmazonの超技術を垣間見ることができます。
[レポート] Amazon Aurora deep dive ~性能向上の仕組みと最新アップデート~ #AWSSummit | Developers.IO

さてバイナリログを使っていないとなると、gh-ostの利用はどうなるのでしょうか。
gh-ostにもドキュメントがありますが

https://github.com/github/gh-ost/blob/master/doc/rds.md

gh-ost has been updated to work with Amazon RDS however due to GitHub not relying using AWS for databases, this documentation is community driven so if you find a bug please open an issue!

(gh-ostはAmazon RDSでも動くようになったが、我々GitHubはDBにAWSは使ってないので、このドキュメントはコミュニティに懸かっている。だからバグを見つけたら是非issueを上げて欲しい)

とあり、gh-ostの特徴のひとつである「GitHub社での実績」が欠けます。
実際に使ってみました。

準備

下記条件でAWS RDS Auroraインスタンスを作ります。
t2.smallインスタンスが解禁されたので、検証用途でもAuroraを作りやすくなりました。

  • Aurora 5.6.10a
  • db.t2.small
  • writer×1、reader×1

また、接続元のIPアドレスを調べてセキュリティグループでポートを開けておきます。

gh-ostを使うにはバイナリログが必要です。
先述のとおりAuroraのリードレプリカ作成にはバイナリログは使われませんが、Auroraと他のMySQL等とのレプリケーションのためにバイナリログの出力機能は存在しています。

Aurora と MySQL との間、または Aurora と別の Aurora DB クラスターとの間のレプリケーション – Amazon Relational Database Service

gh-ostを利用するには、DB Cluster Parameter Groupにて「binlog_format=ROW」に設定しておく必要があります。

クラスターエンドポイントに接続し、show binary logsでバイナリログファイルが確認できればOKです。

mysql> SHOW VARIABLES LIKE "binlog_format";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 row in set (0.01 sec)

mysql> show binary logs;
+----------------------------+-----------+
| Log_name                   | File_size |
+----------------------------+-----------+
| mysql-bin-changelog.000001 |       120 |
| mysql-bin-changelog.000002 |     16062 |
+----------------------------+-----------+
2 rows in set (0.01 sec)

今回の検証には下記のデータベース/テーブルを使います。

USE sushiya;

CREATE TABLE sushi(
  id   INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(20),
  INDEX(id)
);

gh-ostの実行

まず入手します。

$ wget https://github.com/github/gh-ost/releases/download/v1.0.36/gh-ost-binary-linux-20170403125842.tar.gz
$ tar xzvf ./gh-ost-binary-linux-20170403125842.tar.gz

gh-ostには3つのモードがありますが、Aurora上ではb. Connect to masterにて実行します。

$ ./gh-ost 
  --user="(ユーザー)" 
  --password="(パスワード)" 
  --host="(クラスターエンドポイント)" 
  --port=3306 
  --database="sushiya" 
  --table="sushi" 
  --alter="ADD COLUMN price INT DEFAULT 100, ADD COLUMN created_at DATETIME" 
  --allow-on-master 
  --execute

確認します。

mysql> show create table sushiG
*************************** 1. row ***************************
       Table: sushi
Create Table: CREATE TABLE `sushi` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `price` int(11) DEFAULT '100',
  `created_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.01 sec)

カラムが増えていることを確認できました。

gh-ostの特徴としてa. Connect to replica, migrate on masterモードやc. Migrate/test on replicaモードがありますが、これらを使うためにリードレプリカに接続して実行しようとしても「バイナリログが無効」というエラーで失敗してしまいます。

$ ./gh-ost 
>   --user="(ユーザー)" 
>   --password="(パスワード)" 
>   --host="(リーダーエンドポイント)" 
>   --port=3306 
>   --database="sushiya" 
>   --table="sushi" 
>   --alter="ADD COLUMN price INT DEFAULT 100, ADD COLUMN created_at DATETIME" 
>   --test-on-replica
2017-05-02 01:11:40 FATAL (リーダーエンドポイント):3306 must have binary logs enabled

もしAuroraからバイナリログを受け取って同期しているSlaveデータベースがあれば、c. Migrate/test on replicaモードでの実行も可能なのではないかと思います。

Interactive commandsの使用

通常のgh-ost利用時と同様に、UNIXドメインソケットを使った制御もAurora経由で可能です。

gh-ost/interactive-commands.md at master · github/gh-ost

テーブル切り替えを保留にするフラグファイルを作成します。

$ touch /tmp/ghost.postpone.flag

実行します。

$ ./gh-ost 
  --user="(ユーザー)" 
  --password="(パスワード)" 
  --host="(クラスターエンドポイント)" 
  --port=3306 
  --database="sushiya" 
  --table="sushi" 
  --alter="ADD COLUMN price INT DEFAULT 100, ADD COLUMN created_at DATETIME" 
  --allow-on-master 
  --postpone-cut-over-flag-file=/tmp/ghost.postpone.flag 
  --execute

--postpone-cut-over-flag-fileオプションで、先程作成したフラグファイルを指定するとテーブルの同期だけが進み、切り替えが保留状態になります。

ここからステータスの確認など、gh-ost実行途中での操作ができます。

$ echo status | nc -U /tmp/gh-ost.sushiya.sushi.sock

unposeponeでテーブルの切り替えが実行されます。

$ echo unpostpone | nc -U /tmp/gh-ost.sushiya.sushi.sock

確認します。

mysql> SHOW CREATE TABLE sushiG
*************************** 1. row ***************************
       Table: sushi
Create Table: CREATE TABLE `sushi` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `price` int(11) DEFAULT '100',
  `created_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.01 sec)

カラムが追加されました。

Auroraでgh-ostの必要性がどれくらいあるかはさておき、Auroraでも一部条件下にてgh-ostを利用できることがわかりました。

参考文献

続きを読む

【AWS】Elastic Beastalk

はじめに

タダです。
AWS認定試験勉強のためにElastic Beanstalkのドキュメントを読んだ自分用メモになります。
※違う内容書いているなどありましたらご指摘いただけると幸いです。
※随時アップデートがあれば更新していきます。

サービスの概要

  • Elastic Beanstalkは、ソースコードをアップロードするだけで、ソースコードを実行する環境のプロビジョニング、ロードバランサー、スケーリング、モニタリングなどの細かい作業はサービスを管理するサービス

    • サポートするのは、PHP、Java、Python、Ruby、Node.js、Docker
  • 構成できるのは、ウェブサーバー環境とワーカー環境
    • ウェブサーバー環境は、ELB + AutoScalingでスケーラブルな環境を構成し、環境毎にDNS名を付与する
    • ワーカー環境は、SQS + AutoScalingでスケーラブルなバッチ処理基盤を構成する
      • Sqsdはワーカーホスト内で動作するデーモン

        • 200 OKならSQSのメッセージを削除
        • 200 OK以外ならVisibilityTimeout(SQSの設定)後にSQSからメッセージが取得可能(リトライ)
        • 応答なしならInactivity Timeout(Elastic Beanstalkの設定)後にSQSからメッセージが取得可能(リトライ)
      • 定期的なタスク実行も可能(cron.yamlで定義)

特徴

  • デプロイオプションを使って、簡単に新しいアプリケーションバージョンを実行している環境にデプロイできる
  • CPU平均使用率、リクエスト数、平均レイテンシーなどCloudWatchモニタリングメトリクスにアクセスできる
  • アプリケーションの状態が変化したり、アプリケーションサーバが追加または削除されたりした際にはSNSをつかって通知が行われる
  • アプリケーションサーバにログインせずにサーバのログファイルのアクセスできる
    • S3に保管される
  • AMI、オペレーティングシステム、言語やフレームワーク、およびアプリケーションサーバーまたはプロキシサーバーなどアプリケーションを実行する基盤となるプラットフォームに対する定期的な自動更新を有効にできる
  • アプリケーションサーバ設定(JVM設定など)を調整して環境変数を渡す

サービスの構成要素

  • アプリケーション : トップレベルの論理単位

    • バージョン、環境、環境設定が含まれている入れ物
  • バージョン : デプロイ可能なコード
    • S3上でのバージョン管理
    • 異なる環境に異なるバージョンをデプロイ可能
  • 環境:Webサーバ、ワーカーに応じて構築されるインフラ環境
    • バージョン(ソースコード)をデプロイ
  • 環境設定:その環境に関連するリソースの動作を定義するパラメーター
    • EC2インスタンスタイプ、AutoScalingの設定など
       
      ## 環境のタイプ
  • ロードバランシング、AutoScaling環境
    • 高い可用性と伸縮自在性を兼ね備えた構成
    • ウェブサーバー環境:ELB + AutoScaling
    • ワーカー環境:SQS + AutoScaling
  • シングルインスタンス環境
    • EC21台構成(AutoScalingでmax,minが1に設定されている)
    • 開発環境などの構築のために低コストで構築可能

環境設定

環境に関連するリソースの動作を定義する設定パラメーター

  • 直接設定

    • マネジメントコンソールまたはElastic Beastalk CLI(eb createオプションで実施)
  • 保存済み設定
    • 環境の作成中または実行中の環境に適用できる設定をS3に保存して流用できる(ステージングで使った設定を本番に適用)
  • デフォルト値
  • .ebextensions
    • ウェブ環境のソースコードに.ebextensionsを追加することで環境を設定し、環境に含まれるAWSリソースをカスタマイズできる(JVM設定など)
    • 環境で使用しているリソースのカスタマイズが可能
    • 環境に対する様々な操作を自動化、集約可能
~/workspace/my-app/
|-- .ebextensions
    |-- environmentvariables.config
    |-- healthchekckurl.config
|-- .elasticbeanstalk
    |-- config.yml
|-- index.php

.ebextensionsで実行可能な操作

  • packages:yum,rpmでのパッケージのインストール
  • sources:外部からのアーカイブをダウンロードした場所に展開する
  • users/groups:任意のユーザー/グループを作成
  • commands:デプロイ処理前に実行すべきコマンドやスクリプトを指定
  • container_commands:新バージョンの展開後に実行すべきコマンドやスクリプトを指定
  • option_settings:環境変数の設定
  • Resouteces:追加のリソースを定義(SQSのキュー、DynamoDBテーブル、CloudWatchのアラーム)

.ebextensions Tips

  • セクションごとにファイルを分割する
  • インストールするパッケージのバージョンを明記
    • インスタンスによって異なるバージョンになることを防止
  • カスタムAMIとのトレードオフを検討

Dockerサポート

  • Dockerを構成する場合、Single Container(EC2の中にDockerを実行)またはMulti Container(ECS)を利用可能
  • DockerをElastic Beanstalkでデプロイする場合、Dockerrun.aws.jsonで定義して実行する
{
  "AWSEBDockerrunVersion": "1",
  "Image": {
    "Name": "janedoe/image",
    "Update": "true"
  },
  "Ports": [
    {
      "ContainerPort": "1234"
    }
  ],
  "Volumes": [
    {
      "HostDirectory": "/var/app/mydb",
      "ContainerDirectory": "/etc/mysql"
    }
  ],
  "Logging": "/var/log/nginx"
}

デプロイ形式

Elastic Beastalkのデプロイ形式を以下にまとめる

メソッド 概要
All at once 同時にすべてのインスタンスに新しいバージョンをデプロイする。環境内のすべてのインスタンスは、デプロイが実行される間、短時間だがサービス停止状態になる。
Rolling バッチに新しいバージョンをデプロイする。デプロイフェーズ中、バッチはサービス停止状態になり、バッチのインスタンスによる環境容量への負荷を低減する。
Rolling with additional batch バッチに新しいバージョンをデプロイするが、デプロイ中に総容量を維持するため、インスタンスの新しいバッチをまず起動する
Immutable 変更不可能な更新を実行し、新しいバージョンをインスタンスの新しいグループにデプロイする。

モニタリング

  • 基本(ベーシック)ヘルスレポート

    • 環境のヘルスステータス
    • ELBのヘルスチェック
    • CloudWatchメトリクス
  • 拡張ヘルスレポート
    • OSレベルのメトリクス
    • アプリケーションレベルのメトリクス

拡張ヘルスレポートの主なメトリクス

  • EnvironmnetHealth
  • インスタンスの状態
  • リクエスト総数及び各レスポンスコード毎の数
  • x%の完了のかかった平均時間
  • LoadAverage1min: 1分間のLoad値の平均値
  • RootFilesystemUtil: 使用ディスク容量の割合
  • CPU使用状況詳細

他のAWSサービスとの統合

  • CloudFront

    • Elastic Beanstalkでデプロイしたら、CloudFrontから配信する
  • DynamoDB
    • Elastic BeastalkでDynamoDBのテーブルを作成し、データを書き込む
  • Elasticache
    • セキュリティグループでElastic Beastalkでアクセスできるように設定する
  • RDS
    • セキュリティを高めるために、接続情報をS3に保存し、デプロイの間にデータを取得するようにElastic Beastalkを設定する
    • 設定ファイル(ebextensions)を使用して環境内のインスタンスを設定し、アプリをデプロイする時にS3からファイルを安全に取得できる
  • S3
    • S3バケットは、アプリケーションのバージョン、ログ、その他のサポートファイルファイルを保存する

メンテナンスウィンドウ

メンテナンスウィンドウは、管理プラットフォームの更新が有効化されており、プラットフォームの新バージョンが公開されている場合にElastic Beanstalkでプラットフォームの更新が開始される毎週2時間の時間帯が割り当てられる

EB CLIについて

コマンドラインインターフェースとして、EB CLIがある

  • eb create

    • 初期環境を作成する
  • eb status
    • 環境のステータスを確認する
  • eb health
    • 環境内のインスタンスのヘルス情報とその環境全体の状態を表示する
  • eb events
    • 環境のイベントのリストを確認する
  • eb logs
    • 環境のインスタンスからログを取得する
  • eb open
    • ブラウザでウェブサイトのユーザー環境を開く
  • eb deploy
    • デプロイする
  • eb config
    • 実行する環境に利用可能な設定オプションを確認する
  • eb terminate
    • 環境のターミネート

参考

更新日時

  • 2017/05/01 初回投稿
  • 2017/05/05 環境設定の項目を更新、Dockerサポートを追加

続きを読む