xyk blog

最近は iOS 開発の記事が多めです。

同じiOSバージョンのシミュレータが重複して表示されてしまう場合の解決方法

環境:
Xcode7.0.1

Beta 版と GM をインストールしたら重複して表示されるようになってしまった。

f:id:xyk:20151012172911p:plain

iOSシミュレータを削除するには Xcode > Window > Devicesから削除できる。

f:id:xyk:20151012223020p:plain

削除したいデバイスがたくさんあって個別に面倒な場合は

~/Library/Developer/CoreSimulator

のCoreSimulatorディレクトリごと削除して再起動でもよいらしい(試してない)。

iOS シミュレータ・アプリのディレクトリの場所

環境:
Xcode7.1

アプリデータのディレクトリを調べる

例えば iOSシミュレータにインストールした、あるアプリのDocumentsディレクトリは以下のようになる。

~/Library/Developer/CoreSimulator/Devices/CC8FA744-B3C2-4689-839F-33B504F6168A/data/Containers/Data/Application/3E4EBE82-0BC7-405E-B143-5F40B03EBBA8/Documents

バイスID、アプリIDの部分がランダムな文字列になっていて、パスからは何のアプリなのか判別できない。

このパスはアプリで以下コードを実行することで確認することができる。

// swift3
print(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last!)

最後に使用したシミュレータのデバイスで、最後にインストールしたアプリのDocumentsディレクトリは以下のコマンド一発でFinderを開くことができる。

$ cd ~/Library/Developer/CoreSimulator/Devices/ 
  && cd `ls -t | head -n 1`/data/Containers/Data/Application 
  && cd `ls -t | head -n 1`/Documents 
  && open

iOSシミュレータのディレクトリを調べる

Xcode でアプリ開発時に使用するiOSシミュレータ。  

f:id:xyk:20151026140215p:plain

iOSシミュレータのディレクトリは以下にある。

~/Library/Developer/CoreSimulator/Devices

各シミュレータのディレクトリは以下のようにランダムな文字列の名前となっていて、これを見ただけでは、どのデバイスと紐付いているのかわからない。

f:id:xyk:20151012223634p:plain

このディレクトリがどのデバイスと紐付いているかはXcode > Window > Devicesの Identifier で確認できる。

f:id:xyk:20151027005527p:plain

または以下コマンドからでも確認できる。

$ instruments -s devices
Known Devices:
mbp [502448C4-0A7D-5D96-8EAA-3FE2C32D9C5B]
Apple TV 1080p (9.0) [33BE56A7-BB99-4A45-88E2-3C9C482EDB45]
Apple Watch - 38mm (2.0) [1F3FB32B-2C35-4951-B1D6-C797CEDC2A65]
Apple Watch - 42mm (2.0) [D0E0AA11-7276-4C76-BBFA-2B2571EC13E3]
iPad 2 (9.1) [003458F4-881A-44C9-9E73-815F50790117]
iPad Air (9.1) [CFA403BF-2738-4FF9-A32B-0520D0D8E167]
iPad Air 2 (9.1) [C402142B-130E-4456-B579-E2C032BAFB8C]
iPad Pro (9.1) [01D9E203-1D49-4342-B642-B2FC82C54CB4]
iPad Retina (9.1) [05637EC1-2820-4AB2-BE24-5D8BF7B76DE3]
iPhone 4s (9.1) [8FB284BC-7022-4E3F-9D59-41299CFBB3AC]
iPhone 5 (9.1) [05F41E8B-0C60-4917-B706-DAB2A5C683B8]
iPhone 5s (9.1) [F46048FE-9455-4814-93DF-F5CFCFEE06EA]
iPhone 6 (9.1) [951E310E-D37C-4EF1-83EA-68FFA8233CC0]
iPhone 6 Plus (9.1) [7839119B-EE08-4E39-94C4-9991EC6C804D]
iPhone 6s (9.1) [F5B6F4BB-EEC0-404E-80EA-B200433ACC47]
iPhone 6s (9.1) + Apple Watch - 38mm (2.0) [C67B741D-279E-4DF0-840A-8EE151B75756]
iPhone 6s Plus (9.1) [63933384-7F47-444F-82EE-A3BA10D6A769]
iPhone 6s Plus (9.1) + Apple Watch - 42mm (2.0) [D275AF10-7558-4BED-9B9D-75C65CD7D983]

これで紐付けはわかるが Finder でそのディレクトリまで掘っていくのが面倒くさい。
そこでディレクトリを一発で開くことができるツールを使ってみた。

SimulatorManager

シミュレータ一覧と、シミュレータ内のアプリフォルダが表示されるので選択すれば Finder でディレクトリを開くことができる。

github.com

f:id:xyk:20151022153225p:plain

バイナリのダウンロードはこちら
https://github.com/tue-savvy/SimulatorManager/releases

SimPholders2

こちらはシミュレータにインストールされたアプリ一覧が表示される。
だが、たまに上手く動かないときがある・・・

kfi-apps.com

f:id:xyk:20151012174405p:plain


ちなみに各iOSシミュレータのログディレクトリは以下にある。

~/Library/Logs/CoreSimulator

f:id:xyk:20151012223647p:plain

iOS でファイル保存、読み込み

環境: Swift2.0

アプリ内にデータを保存する場合、どこに保存するのかを調べた。
だいたい以下のディレクトリ(またはこれらの中に作成したサブディレクトリ)のどれかに保存することになるようだ。

  • Documents/
  • Library/
    • Library/Application Support/
    • Library/Caches/
  • tmp/

保存するデータの内容によって使い分ける必要がある。
Apple のガイド
https://developer.apple.com/jp/documentation/FileSystemProgrammingGuide.pdf
から引用する。

Documents/

ユーザが生成したデータを保存するために使います。ファイル共有の機能により、ユーザはこのディレクトリ以下にアクセスできます。したがって、ユーザに見せても構わないファイルのみ置いてください。 このディレクトリの内容はiTunesによってバックアップされます。

ユーザデータはDocuments/以下に置いてください。これは一般に、ユーザに積極的に見せるファイルです。ユーザが自由に作成、インポート、削除、編集する対象です。たとえば描画アプリケーションの場合、ユーザが作成するグラフィックファイルがこれに当たります。テキストエディタであればテキストファイルが該当します。動画/音声アプリケーションの場合、ユーザが後で試聴するためにダウンロードしたファイルもこれに当たります。

Documents/およびApplication Support/以下のファイルは、自動的にバックアップの対象にな ります。NSURLIsExcludedFromBackupKeyキーを指定して -[NSURL setResourceValue:forKey:error:]を実行することにより、バックアップ対象から除外できます。いつでも再生成またはダウンロードできるファイルは、この対象から外してください。大容量のメディアファイルの場合、これは特に重要です。ダウンロードした動画や音声は、バックアップ対象に含めないようにしてください。

Library/

  • Library/Application Support/
  • Library/Caches/

これは、ユーザのデータファイル以外のファイル用の最上位ディレクトリです。通常、標準的なサブディレクトリを用意し、いずれか適当な場所に保存します。iOSアプリケーションは通常、Application Support およびCachesというサブディレクトリを使いますが、独自のサブディレクトリを作成しても構いません。 ユーザに見せたくないファイルはLibraryサブディレクトリ以下に置いてください。ユーザデータのファイル保存用に使ってはなりません。 Libraryディレクトリの内容は、Cachesサブディレクトリ以下を除き、 iTunesによるバックアップの対象になります。

データキャッシュファイルはLibrary/Caches/ディレクトリ以下に置きます。キャッシュデータは、一時データよりは長期間にわたって残しておきたいけれども、補助ファイルほどではない場合に有用です。一般にキャッシュデータは、なくてもアプリケーションの動作に影響しませんが、性能改善の効果が期待できます。例として、データベースのキャッシュファイルや、一時的でいつでもダウンロード可能なデータなどがあります。なお、ディスク空間を確保するため、システムがCaches/ディレクトリを消去することがあるので、必要ならばいつでも生成し直し、あるいはダウンロードできるようになっていなければなりません。

tmp/

このディレクトリは、アプリケーションを次に起動するまで保持する必要のない一時ファイルを書き込むために使用します。不要になったファイルは削除しなければなりません。もっとも、アプリケーションが動作していないときに、システムがこのディレクトリ以下をすべて消去することがあります。 iTunesはこのディレクトリの内容をバックアップしません。

一時データはtmp/ディレクトリに置いてください。一時データとは、長期間にわたって保存しておく必要がないデータのことです。使い終わったら削除して、デバイス上の空間を消費し続けないようにしなければなりません。システムは、アプリケーションが動作していない間、定期的にここにあるファイルを消去します。したがって、一度停止した後、ファイルが残っていることを前提として処理してはなりません。

まとめると以下のようになる。

Documents/
・ユーザが作成したデータ(テキスト、写真)、再作成が不可能なデータなど重要なデータを保存するディレクトリ。
・ユーザに見せたくないファイル(設定ファイルとか)はここに置かない。
・iTunes、iCloud バックアップ対象。

Library/
・ユーザのデータファイル以外を保存するディレクトリ。自分でサブディレクトリを用意して保存する。
・ユーザに見せたくないファイルはここに置く。
・iTunes、iCloud バックアップ対象(Caches ディレクトリを除く)。

Library/Caches/
・一時的なデータを保存するディレクトリ。システムが自動削除する可能性がある。
・後から再度ダウンロードして復旧可能なデータを置くこと。
・iTunes、iCloud バックアップ対象外。

Library/Preferences/
・アプリの設定を保存するディレクトリ。NSUserDefaults のデータはここに保存される。

tmp/
・一時的なデータを保存するディレクトリ。アプリが動作してないときにシステムが自動削除する可能性がある。
・iTunes、iCloud バックアップ対象外。
・使い終わったらアプリ側で削除して容量削減に務める。

tmp/Library/Caches/の使い分けについて

どちらのディレクトリも一時ファイルの保存先として使うが、システムによるファイル削除のタイミングが違う。

ディレクト 非アクティブ中のシステムによる削除 アクティブ中のシステムによる削除 ガイド記載の使用例
tmp/ × 次のアプリの起動で保持する必要のないファイル
Caches/ ダウンロードコンテンツ

ディレクトリパスの取得方法

NSSearchPathForDirectoriesInDomains関数を使う。
またはNSHomeDirectory()でホームディレクトリを取得してそれに連結してもよい。

Documentsディレクト

let path: String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/73FBC4AB-EA27-4ECF-A1FA-9389A25DD2CD/Documents

// または
let path: String = NSHomeDirectory() + "/Documents"

// または
let path: NSURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]

Libraryディレクト

let path: String = NSSearchPathForDirectoriesInDomains(.LibraryDirectory, .UserDomainMask, true)[0]
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/7A7E5AA5-9ED9-4CE3-8559-90A946227DF8/Library

// または
let path: String = NSHomeDirectory() + "/Library"

// または
let path: NSURL = NSFileManager.defaultManager().URLsForDirectory(.LibraryDirectory, inDomains: .UserDomainMask)[0]

Library/Cachesディレクト

let path: String = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0]
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/3959DFC8-25A6-47C4-B246-FE8304B17864/Library/Caches

// または
let path: String = NSHomeDirectory() + "/Library/Caches"

// または
let path: NSURL = NSFileManager.defaultManager().URLsForDirectory(.CachesDirectory, inDomains: .UserDomainMask)[0]

Library/Application Supportディレクト

let path: String = NSSearchPathForDirectoriesInDomains(.ApplicationSupportDirectory, .UserDomainMask, true)[0]
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/9E25C963-2F4B-4985-BA9B-B9DADF52E9EC/Library/Application Support

// または
let path: String = NSHomeDirectory() + "/Library/Application Support"

// または
let path: NSURL = NSFileManager.defaultManager().URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]

tmp/ディレクト

let path: String = NSTemporaryDirectory()
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/6572ECC7-D57A-40C0-8E0E-E52AC8F64012/tmp/
// NSHomeDirectory()と違ってパス末尾にスラッシュが含まれる。~/tmp/


ディレクトリ・ファイルの操作

NSFileManagerによる作成・削除

NSFileManagerクラスを使う。
よく使いそうな操作メソッドの定義。

// NSFileManager

// 存在チェック
public func fileExistsAtPath(path: String) -> Bool

// ディレクトリ作成
public func createDirectoryAtPath(path: String, withIntermediateDirectories createIntermediates: Bool, attributes: [String : AnyObject]?) throws

// ファイル作成
public func createFileAtPath(path: String, contents data: NSData?, attributes attr: [String : AnyObject]?) -> Bool

// 削除
public func removeItemAtPath(path: String) throws

NSDataをファイル書き込み

ファイルパス(StringまたはNSURL)を指定して NSData として書き込む。

// NSData
public func writeToFile(path: String, atomically useAuxiliaryFile: Bool) -> Bool
public func writeToFile(path: String, options writeOptionsMask: NSDataWritingOptions) throws

public func writeToURL(url: NSURL, atomically: Bool) -> Bool
public func writeToURL(url: NSURL, options writeOptionsMask: NSDataWritingOptions) throws

NSDataとしてファイル読み込み

ファイルパス(StringまたはNSURL)から NSData として読み込む。

// NSData
public init?(contentsOfFile path: String)
public init(contentsOfFile path: String, options readOptionsMask: NSDataReadingOptions) throws

public init?(contentsOfURL url: NSURL)
public init(contentsOfURL url: NSURL, options readOptionsMask: NSDataReadingOptions) throws

UIImageをファイル書き込み

UIImageUIImagePNGRepresentationでNSDataに変換してから書き込み。

UIImagePNGRepresentation(image)?.writeToFile(imagePath, atomically: true)

UIImagePNGRepresentation(image)?.writeToURL(imageURL, atomically: true)

UIImageとしてファイル読み込み

ファイルパスから UIImage として読み込む。

// UIImage
public init?(contentsOfFile path: String)

NSURLを使ってローカルファイルを読み書きする場合の注意点

文字列のファイルパスをNSURLに変換して扱う場合、fileURLWithPath:を使うこと。

let url = NSURL(fileURLWithPath: NSTemporaryDirectory() + "dummy.txt")
// 以下は正しくない
// let url = NSURL(string: NSTemporaryDirectory() + "dummy.txt")

Carthage を使ってライブラリを管理する

環境: Xcode 7.0.1
Swift2.0

Carthage(カーセージって読むみたい)というライブラリ管理ツールを使ってみた。
CocoaPods と比べると
・CocoaPods では Static Library のlibPods.aを静的リンクしていたが 、Carthage はxcodebuildコマンドを使ってビルドした Dynamic Framework (iOS8以降)を実行時に動的にリンクする
・CocoaPods のように.xcworkspaceのようなスキームは生成されない、ライブラリは自分でプロジェクトに追加しなければならないが.xcodeprojのまま扱える
・ビルドが早い
などの違いがあるらしい。

github.com

realm.io

インストール

homebrew でインストールできる。

$ brew update
$ brew install carthage

確認

$ carthage version
0.8.0

Cartfile 作成

Cartfileというファイルを作成して、使用したいライブラリを記述していく。
GitHub レポジトリversion を指定する。

$ cat Cartfile
github "Alamofire/Alamofire" >= 3.0.0

carthage update コマンド実行

依存関係があればそれも含めてダウンロードし、そしてビルド& framework を行う。

$ carthage update
*** Fetching Alamofire
*** Checking out Alamofire at "3.0.0"
*** xcodebuild output can be found in /var/folders/ml/2hwc26zx5b30jf3k4tp_kzj00000gn/T/carthage-xcodebuild.h6xNwA.log
*** Building scheme "Alamofire watchOS" in Alamofire.xcworkspace
*** Building scheme "Alamofire OSX" in Alamofire.xcworkspace
*** Building scheme "Alamofire iOS" in Alamofire.xcworkspace

watchOSOSXiOS各プラットホーム分作成された。
iOS のみでよければ--platformで指定すればよい。

$ carthage update --platform iOS

コマンド実行後は以下のようなディレクトリ構成となった。

f:id:xyk:20151011104005p:plain

Xcode に framework を追加

作成された framework を XcodeTarget > General > Linked Frameworks and Binariesに追加する。
+ ボタンから追加するか Finder からドラッグ&ドロップでも追加できる。

f:id:xyk:20151011104033p:plain

ライブラリを使う

import Alamofire

早速 import して実行してみたところ、ビルドは成功するがシミュレータ起動直後に以下のエラーが発生した。

dyld: Library not loaded: @rpath/Alamofire.framework/Alamofire
  Referenced from: /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Bundle/Application/304560B9-432C-4A5D-BCEF-B2328D84DED1/AlamofireSample.app/AlamofireSample
  Reason: image not found

ググってみるとTarget > General > Embedded Binariesにも追加するらしい。

f:id:xyk:20151011111502p:plain

これでエラーは解消された。

むしろはじめからEmbedded Binariesの方に追加するほうがよいかも。Linked Frameworks and Binariesにも同時に追加されるし。

ライブラリを使う分にはここまでの手順で OK だが、アプリを AppStore にサブミットするためには、さらに以下のワークアラウンドを行う必要があるらしい。

Script の追加

Build Phasesの + ボタンをクリックしNew Run Script Phaseを選択し

/usr/local/bin/carthage copy-frameworks

を入力。またInput Files

$(SRCROOT)/Carthage/Build/iOS/Alamofire.framework

を入力。

f:id:xyk:20151011114007p:plain

f:id:xyk:20151011114317p:plain

Git を理解するための濃い記事まとめ

後でまた見返したい Git を理解するための濃い記事まとめ。

www.slideshare.net

koseki.hatenablog.com

koseki.hatenablog.com

d.hatena.ne.jp

ja.astahblog.com

見えないチカラ: 【翻訳】Gitをボトムアップから理解する


チュートリアル

gitの入門用のチュートリアル"Learn Git Branching"を訳した | 48JIGEN *Reloaded*

k.swd.cc

iOS9対応メモ

環境: Xcode7 GM

iOS9 対応で行った作業メモ。

App Transport Security(ATS)対応

HTTPS 接続にする必要あり。今回はとりあえずHTTPを許可するようにInfo.plistに以下の手順で追加する。

  • NSAppTransportSecurityを Dictionary で追加。
  • その下でNSAllowsArbitraryLoadsをBooleanで追加し、YESを設定。

カスタムURLスキーム対応

openURLcanOpenURLのようなメソッドでカスタムURLスキームを呼び出す場合、Info.plistに登録しなければ使えないようになった。
admob SDK を使ってるのだが、アプリ起動時に以下のような許可されてないスキームのエラーが出るようになった。

 -canOpenURL: failed for URL: "itms-books://" - error: "This app is not allowed to query for scheme itms-books"
 -canOpenURL: failed for URL: "kindle://home" - error: "This app is not allowed to query for scheme kindle"

Info.plistに以下の手順で追加する。

  • LSApplicationQueriesSchemesを Array で追加。
  • その下で+を押すとitem 0が追加されるので String でitms-booksを入力。
  • さらに+を押してitem 1を追加し String でkindleを入力。

これで再度起動してみると、スキームが許可されてないエラーから、スキーム起動失敗のエラーに変わった。
これはスキームで起動させるアプリがインストールされていないためなので問題なし。

-canOpenURL: failed for URL: "itms-books://" - error: "(null)"
-canOpenURL: failed for URL: "kindle://home" - error: "(null)"

ここまでの作業でInfo.plistは以下のようになった。

f:id:xyk:20150915220436p:plain

Bitcode をオフ

実機でアプリを起動しようとしたら admob SDK で以下のエラーが出た。

ld: '/.../Google-Mobile-Ads-SDK/GoogleMobileAdsSdkiOS-7.4.1/GoogleMobileAds.framework/GoogleMobileAds(GADGestureIdUtil.o)' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture armv7
clang: error: linker command failed with exit code 1 (use -v to see invocation)

とりあえず Bitcode をオフにすることでビルドが通るようになった。
Target -> Build Settings -> Build Options -> Enable Bitcode を NO に変更。

f:id:xyk:20150915220441p:plain

よく使う Xcode のショートカットメモ

developer.apple.com

Command(⌘) + Option(⌥) + [

選択行(複数行可)を上に移動

Command(⌘) + Option(⌥) + ]

選択行(複数行可)を上に移動

Command(⌘) + Control(^) + 左 or 右

超使う。ソースコード上の定義にジャンプしたり、戻ったり。
トラックパッドの左右スワイプでも同様なことができるがデカいファイル上で行うと固まったりするので、上のコマンドでやるほうがよい。

Command(⌘) + b

ビルド

Command(⌘) + r

ビルド&シミュレータ起動。

Command(⌘) + k

クリーン

Command(⌘) + .

ビルドのキャンセル。

Command(⌘) + 1

プロジェクトナビゲーター表示。左ペインのソースコードツリーのこと。
Command(⌘) + 2,3,4,5… で左ペインをメニューをそれぞれ表示する。

Command(⌘) + Shift(⇧) + j

現在、開いているファイルのプロジェクトナビゲーター上の位置に移動して表示。

Command(⌘) + f

現在開いているファイルに対して検索を行う。

Command(⌘) + Shift(⇧) + f

全ファイルに対して検索を行う。
さらに検索窓の上の 「Find」部分を「Replace」に入れ替えることで置換用窓が現れる。
これを使えば全ファイルを対象に一括置換が可能。置換前にプレビューで確認できるので安心して置換できる。

Command(⌘) + Shift(⇧) + o

ファイル名でインクリメンタル絞り込みを行いつつ検索する。

Command(⌘) + Option(⌥) + Enter / Option(⌥) + ファイルクリック

現在開いているウインドウを分割して2つのファイルを表示する。(画面分割形式 は Assistant Editor の設定で指定できる) ウィンドウを閉じるにはCmd + Enter。表示と違うので注意。

Command(⌘) + Option(⌥) + 0

Utilities の表示・非表示。画面右ペインのこと。
Command(⌘) + Option(⌥) + 2,3,4,5… で右ペインをメニューをそれぞれ表示する。

Command(⌘) + 0

画面左ペインの表示・非表示。

Command(⌘) + Shift(⇧) + <

Scheme 編集画面を開く。

Command(⌘) + Control(^) + [ or ]

Target を切り替える。


番外編

ブレイクポイントの削除

ブレイクポイントを枠外にドラッグで移動させて離すと削除される。
これ知らずに右クリックから Delete Breakpoint で削除してて面倒くさいと思ってた・・

Xcode の Localization でハマったのでメモ

環境:
Xcode 6.1
iOS 8.1 Simulator

結論から言うと Xcode 6.1 と iOS 8.1 Simulator の環境で、 iOS Simulator の言語設定に対応したローカライズファイルLocalizable.stringsを用意しても反映されないバグがあるらしい。

ワークアラウンドとして iOS Simulator 側の言語設定はなく、Xcode の Edit Scheme -> Run -> Option -> Application Language で言語設定すればいけた。

参考:

xcode - iOS 8.1 Simulator Localization broken (NSLocalizedString) - Stack Overflow
http://stackoverflow.com/questions/26504304/ios-8-1-simulator-localization-broken-nslocalizedstring
iOS8のシミュレーターでLocalizationのテストをする
http://www.rizastar.com/blog_xcode/ios8-simulator-localization-test/

ローカライズについてはこの資料が分かりやすかった。

Xcodeローカライズ処方箋 #yhios
http://www.slideshare.net/tomohirokumagai54/xcode-yhios

VPC 内に Elastic Beanstalk + RDS の環境構築して Rails アプリをデプロイする

環境:
Mac
aws-cli 1.7.0
eb-cli 3.0.10
Ruby 2.1.5
Rails 4.2.0

今回のポイント

  • VPC の環境構築(Subnet, IGW, Route Table)
  • VPC 内に Elastic Beanstalk 環境構築
  • Elastic Beanstalk のプラットフォームは Rubyを選択、Rails アプリをデプロイする。
    • Rails でブログアプリケーションを作る例とする。
  • RDS(MySQL) を作成してアプリと連携する。Beanstalk 作成時ではなく別途作成して関連付けしない。
  • ELB、EC2、RDS は VPC内のパブリックサブネットに置く。
    • RDS はプライベートサブネットに置き、接続するには NAT インスタンス経由にすべきだろうけど、今回はパブリックサブネットにおいてインターネットから直接接続できるようにした。
  • 無料枠で試すので EC2、RDS インスタンスは t2.micro を選択。
  • 環境構築はコマンドライン(aws-cli, eb-cli)だけで行う。
    • 管理コンソールからやったほうが簡単だけど1ヶ月もすると手順を忘れてしまうので、すべてコマンドで作成して、同じ環境を再現できるようにしたかった。

VPC からの環境構築は今回はじめてやるので理解が間違ってるところがあるかもしれない。

事前準備

IAM でユーザ作成し、「Administrator Access」権限を付与、AWS CLIaws configureコマンドでregionaws_access_key_idaws_secret_access_keyを設定しておくこと。

ざっくりやること一覧

  • VPC 作成
  • Subnet 作成
  • IGW 作成
  • Rroute Table 作成
  • DB パラメータグループ作成
  • DB サブネットグループ作成
  • VPC セキュリティグループ作成
  • RDS インスタンス作成
  • Rails アプリケーション作成
  • Elastic Beanstalk 環境設定
  • Elastic Beanstalk 環境作成
  • Rails アプリのデプロイ
  • 環境の削除

VPC 環境構築

VPC 作成

VPC を作成する。

$ aws ec2 create-vpc --cidr-block 10.0.0.0/16
{
    "Vpc": {
        "InstanceTenancy": "default",
        "State": "pending",
        "VpcId": "vpc-d42ae5b1",
        "CidrBlock": "10.0.0.0/16",
        "DhcpOptionsId": "dopt-9ed4cafc"
    }
}

VPC に Name タグを追加する。

$ aws ec2 create-tags --resources vpc-d42ae5b1 --tags Key=Name,Value="vpc blog"

VPC の作成と一緒に

も作成される。これらの関係性は以下の図のようになっている。

VPC のセキュリティ - Amazon Virtual Private Cloud
http://docs.aws.amazon.com/ja_jp/AmazonVPC/latest/UserGuide/VPC_Security.html

f:id:xyk:20150218160355p:plain

Route Table(main) を確認する。

$ aws ec2 describe-route-tables --filters "Name=vpc-id,Values=vpc-d42ae5b1"

...

            "RouteTableId": "rtb-f072a695",
            "VpcId": "vpc-d42ae5b1",

...

Route Table(main) に Name タグを追加する。

$ aws ec2 create-tags --resources rtb-f072a695 --tags Key=Name,Value="rtb blog main"

ACL を確認する。

$ aws ec2 describe-network-acls --filters "Name=vpc-id,Values=vpc-d42ae5b1"

...

            "NetworkAclId": "acl-2ace1b4f",
            "VpcId": "vpc-d42ae5b1",

...

ACL に Name タグを追加する。

$ aws ec2 create-tags --resources acl-2ace1b4f --tags Key=Name,Value="acl blog"

VPC セキュリティグループ(default)を確認する。

$ aws ec2 describe-security-groups --filters "Name=vpc-id,Values=vpc-d42ae5b1"

...

            "GroupName": "default",
            "VpcId": "vpc-d42ae5b1",
            "OwnerId": "123456789012",
            "GroupId": "sg-bdc574d8"

...

VPC セキュリティグループに Name タグを追加する。

$ aws ec2 create-tags --resources sg-bdc574d8 --tags Key=Name,Value="sg blog default"
VPCDNS ホスト名を有効にする

VPC 内の RDS に外部から接続できるようにするため。
参考: VPC の DNS サポートを更新する

# 有効にする
$ aws ec2 modify-vpc-attribute --vpc-id vpc-d42ae5b1 --enable-dns-hostnames

# 確認
$ aws ec2 describe-vpc-attribute --vpc-id vpc-d42ae5b1 --attribute enableDnsHostnames

{
    "VpcId": "vpc-d42ae5b1",
    "EnableDnsHostnames": {
        "Value": true
    }
}

Subnet 作成

3つのサブネットを作成する。
3つともパブリックサブネットとする。
2つ目、3つ目は DB 用として使うので availability-zone を分ける。
自分のアカウントではap-northeast-1aap-northeast-1cの2つが使用可能となっている。

$ aws ec2 create-subnet --vpc-id vpc-d42ae5b1 --cidr-block 10.0.0.0/24 --availability-zone ap-northeast-1a

{
    "Subnet": {
        "VpcId": "vpc-d42ae5b1",
        "CidrBlock": "10.0.0.0/24",
        "State": "pending",
        "AvailabilityZone": "ap-northeast-1a",
        "SubnetId": "subnet-f23be885",
        "AvailableIpAddressCount": 251
    }
}

$ aws ec2 create-subnet --vpc-id vpc-d42ae5b1 --cidr-block 10.0.1.0/24 --availability-zone ap-northeast-1a

{
    "Subnet": {
        "VpcId": "vpc-d42ae5b1",
        "CidrBlock": "10.0.1.0/24",
        "State": "pending",
        "AvailabilityZone": "ap-northeast-1a",
        "SubnetId": "subnet-ff3be888",
        "AvailableIpAddressCount": 251
    }
}

$ aws ec2 create-subnet --vpc-id vpc-d42ae5b1 --cidr-block 10.0.2.0/24 --availability-zone ap-northeast-1c

{
    "Subnet": {
        "VpcId": "vpc-d42ae5b1",
        "CidrBlock": "10.0.2.0/24",
        "State": "pending",
        "AvailabilityZone": "ap-northeast-1c",
        "SubnetId": "subnet-d721d58e",
        "AvailableIpAddressCount": 251
    }
}

サブネットに Name タグを追加する。

$ aws ec2 create-tags --resources subnet-f23be885 --tags Key=Name,Value="subnet public blog web"
$ aws ec2 create-tags --resources subnet-ff3be888 --tags Key=Name,Value="subnet public blog db1"
$ aws ec2 create-tags --resources subnet-d721d58e --tags Key=Name,Value="subnet public blog db2"

インターネットゲートウェイ作成

インターネットゲートウェイ(IGW)を作成する。

$ aws ec2 create-internet-gateway

{
    "InternetGateway": {
        "Tags": [],
        "InternetGatewayId": "igw-85d63ce0",
        "Attachments": []
    }
}

IGW に Name タグを追加する。

$ aws ec2 create-tags --resources igw-85d63ce0 --tags Key=Name,Value="igw blog"

VPC に IGW をアタッチする。

$ aws ec2 attach-internet-gateway --internet-gateway-id igw-85d63ce0 --vpc-id vpc-d42ae5b1

確認する。

$ aws ec2 describe-internet-gateways --internet-gateway-id igw-85d63ce0
{
    "InternetGateways": [
        {
            "Tags": [
                {
                    "Value": "igw blog",
                    "Key": "Name"
                }
            ],
            "InternetGatewayId": "igw-85d63ce0",
            "Attachments": [
                {
                    "State": "available",
                    "VpcId": "vpc-d42ae5b1"
                }
            ]
        }
    ]
}

パブリックサブネット用のルートテーブル作成

デフォルトでサブネットが紐付くメインルートテーブルとは別に新たにカスタムルートテーブルを作成する。
こちらをパブリックサブネット用のルートテーブルとして使用する。

$ aws ec2 create-route-table --vpc-id vpc-d42ae5b1

{
    "RouteTable": {
        "Associations": [],
        "RouteTableId": "rtb-2073a745",
        "VpcId": "vpc-d42ae5b1",
        "PropagatingVgws": [],
        "Tags": [],
        "Routes": [
            {
                "GatewayId": "local",
                "DestinationCidrBlock": "10.0.0.0/16",
                "State": "active",
                "Origin": "CreateRouteTable"
            }
        ]
    }
}

ルートテーブルに Name タグを追加する。

$ aws ec2 create-tags --resources rtb-2073a745 --tags Key=Name,Value="rtb blog public"

パブリックサブネット用ルートテーブルにインターネットゲートウェイを関連付ける

$ aws ec2 create-route --route-table-id rtb-2073a745 --destination-cidr-block 0.0.0.0/0 --gateway-id igw-85d63ce0

サブネットのルートテーブルを切り替え

作成した3つのサブネットに設定されているメインルートテーブルから、新たに作成したパブリックサブネット用ルートテーブルに切り替える。

$ aws ec2 associate-route-table --route-table-id rtb-2073a745 --subnet-id subnet-f23be885

{
    "AssociationId": "rtbassoc-70924015"
}

$ aws ec2 associate-route-table --route-table-id rtb-2073a745 --subnet-id subnet-ff3be888

{
    "AssociationId": "rtbassoc-73924016"
}

$ aws ec2 associate-route-table --route-table-id rtb-2073a745 --subnet-id subnet-d721d58e

{
    "AssociationId": "rtbassoc-7d924018"
}

VPC 環境構築はここまで。


RDS 環境構築

ここからは RDS の環境構築を行う。

以前の投稿も参考に。

  • MySQL 5.6(5.6.22) を使う。
  • Beanstalk 環境作成時に同時に RDS も作成できるが、Beanstalk に関連付けすると Beanstalk 環境の削除時に RDS も一緒に削除しようとするので別途作成する。
  • 今回は Multi-AZ Deployment はしない。

DB パラメーターグループ作成

DB パラメーターグループmydbparamgroupを作成する。

$ aws rds create-db-parameter-group \
--db-parameter-group-name mydbparamgroup \
--db-parameter-group-family mysql5.6 \
--description "for myinstance"

{
    "DBParameterGroup": {
        "DBParameterGroupName": "mydbparamgroup",
        "DBParameterGroupFamily": "mysql5.6",
        "Description": "for myinstance"
    }
}

DB パラメーターグループのキャラクタセット関連のパラメータを更新する。

$ aws rds modify-db-parameter-group --db-parameter-group-name mydbparamgroup --parameters \
ParameterName=character_set_client,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=character_set_connection,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=character_set_database,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=character_set_results,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=character_set_server,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=collation_connection,ParameterValue=utf8mb4_general_ci,ApplyMethod=immediate \
ParameterName=collation_server,ParameterValue=utf8mb4_general_ci,ApplyMethod=immediate \
ParameterName=skip-character-set-client-handshake,ParameterValue=0,ApplyMethod=pending-reboot

{
    "DBParameterGroupName": "mydbparamgroup"
}

続いて、init_connect パラメータを更新する。

$ aws rds modify-db-parameter-group --generate-cli-skeleton > init_connect.json

出力されたファイルを以下のように編集する。

{
    "DBParameterGroupName": "mydbparamgroup",
    "Parameters": [
        {
            "ParameterName": "init_connect",
            "ParameterValue": "SET SESSION time_zone = CASE WHEN POSITION('rds' IN CURRENT_USER()) = 1 THEN 'UTC' ELSE 'Asia/Tokyo' END;",
            "Description": "",
            "Source": "",
            "ApplyType": "",
            "DataType": "",
            "AllowedValues": "",
            "IsModifiable": true,
            "MinimumEngineVersion": "",
            "ApplyMethod": "immediate"
        }
    ]
}

そして DB パラメータグループを更新する。--cli-input-jsonオプションでファイルを指定。

$ aws rds modify-db-parameter-group --cli-input-json file://init_connect.json

# 終わったら不要なので削除
$ rm init_connect.json

DB サブネットグループを作成

DB サブネットグループmydbsubnetgroupを作成する。
DB 用に用意した2つのサブネットsubnet public blog db1subnet public blog db2を設定する。

$ aws rds create-db-subnet-group \
  --db-subnet-group-name mydbsubnetgroup \
  --db-subnet-group-description "DB SubnetGroup for myinstance" \
  --subnet-ids subnet-ff3be888 subnet-d721d58e

{
    "DBSubnetGroup": {
        "Subnets": [
            {
                "SubnetStatus": "Active",
                "SubnetIdentifier": "subnet-d721d58e",
                "SubnetAvailabilityZone": {
                    "Name": "ap-northeast-1c"
                }
            },
            {
                "SubnetStatus": "Active",
                "SubnetIdentifier": "subnet-ff3be888",
                "SubnetAvailabilityZone": {
                    "Name": "ap-northeast-1a"
                }
            }
        ],
        "DBSubnetGroupName": "mydbsubnetgroup",
        "VpcId": "vpc-d42ae5b1",
        "DBSubnetGroupDescription": "DB SubnetGroup for myinstance",
        "SubnetGroupStatus": "Complete"
    }
}

DB用 VPC セキュリティグループ作成

DB用 VPC セキュリティグループmyrdsを作成する。

ちなみに DB セキュリティグループと呼ばれるものがあるが、これは VPC 内ではない RDS に適用するもの。
昔の EC2-Classic Platform のアカウントのみ使える。

$ aws ec2 create-security-group \
    --group-name myrds \
    --description "RDS security group" \
    --vpc-id vpc-d42ae5b1

{
    "GroupId": "sg-14c77671"
}

DB用 VPC セキュリティグループに Name タグを追加する。

$ aws ec2 create-tags --resources sg-14c77671 --tags Key=Name,Value="sg blog rds"

RDS MySQL に外部から接続するために Inbound のルールを追加する。

$ aws ec2 authorize-security-group-ingress --group-id sg-14c77671 --protocol tcp --port 3306 --cidr 0.0.0.0/0

RDS インスタンス(MySQL)作成

RDS MySQL インスタンスを作成する。

  • db-instance-identifier はmyinstanceとする。
  • 上記で作成した DB パラメータグループをオプションで指定する。
  • セキュリティグループにデフォルト、上で作成したmyrdsの2つを追加する。
  • RDS MySQL に外部から接続したいので publicly-accessible を追加。
$ aws rds create-db-instance \
--db-instance-identifier myinstance \
--allocated-storage 5 \
--db-instance-class db.t2.micro \
--engine MySQL \
--engine-version 5.6.22 \
--master-username bloguser \
--master-user-password bloguser1234567890 \
--db-name blogdb \
--db-parameter-group-name mydbparamgroup \
--db-subnet-group-name mydbsubnetgroup \
--vpc-security-group-ids sg-bdc574d8 sg-14c77671 \
--storage-type standard \
--availability-zone ap-northeast-1a \
--no-multi-az \
--region ap-northeast-1 \
--publicly-accessible \
--no-auto-minor-version-upgrade

コマンドを実行すると RDS インスタンスの作成がはじまる。しばらく時間がかかるので待つ。
インスタンスの作成が完了したら、以下コマンドでエンドポイントを確認する。

$ aws rds describe-db-instances --db-instance-identifier myinstance | jq '.DBInstances[].Endpoint'
{
  "Port": 3306,
  "Address": "myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"
}

PublicIp、PublicDnsName は以下コマンドで確認できる。
GUI 管理コンソールでは、EC2 Dashboard -> Network interfaces から確認できる。

$ aws ec2 describe-network-interfaces --filters "Name=description,Values=RDSNetworkInterface"

...
            "Description": "RDSNetworkInterface",
            "Association": {
                "PublicIp": "54.92.100.249",
                "PublicDnsName": "ec2-54-92-100-249.ap-northeast-1.compute.amazonaws.com",
                "IpOwnerId": "amazon-rds"
            },
...

上で調べた Endpoint または PublicIp、PublicDnsName のいずれかをホストに指定して MySQL に接続できるか確認する。

$ mysql -h myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -P 3306 -u bloguser -p -D blogdb
$ mysql -h ec2-54-65-198-252.ap-northeast-1.compute.amazonaws.com -P 3306 -u bloguser -p -D blogdb
$ mysql -h 54.65.198.252 -P 3306 -u bloguser -p -D blogdb

RDS 環境構築はここまで。


Rails アプリケーション作成

Rails プロジェクト新規作成

サンプルとしてブログアプリケーションを作成する。
DB には MySQL を使う。ローカル環境に MySQL インストール済み。

$ bundle exec rails new blog -d mysql --skip-bundle
$ cd blog

# アプリケーションサーバとして Puma を選択するので Gemfile に追加
$ echo "gem 'puma'" >> Gemfile

# gem インストール
$ bundle install --path vendor/bundle
scaffold 実行

scaffold コマンドでアプリの雛形を作成する。

$ bin/rails generate scaffold article title:string content:string
ルート修正

config/routes.rb

Rails.application.routes.draw do
  resources :articles
  root 'articles#index'
end
タイムゾーンの設定

config/application.rb

    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local
マイグレーション実行

ローカル環境の MySQL に対してマイグレーションを実行する。

$ bin/rake db:create && bin/rake db:migrate
動作確認

ローカル環境でアプリを起動して MySQL にデータ登録、画面にデータ表示できるか確認する。

$ bin/rails s
本番 DB 設定

本番用の DB 設定を修正する。

config/database.yml

production:
  adapter: mysql2
  encoding: utf8mb4
  database: <%= ENV['RDS_DB_NAME'] %>
  username: <%= ENV['RDS_USERNAME'] %>
  password: <%= ENV['RDS_PASSWORD'] %>
  host: <%= ENV['RDS_HOSTNAME'] %>
  port: <%= ENV['RDS_PORT'] %>
Git コミット

Git リポジトリを作成してソースコード一式を add & commit する。

$ git init && git add -A && git commit -m "first commit"

Rails アプリケーション作成まここまで。


Elastic Beanstalk 環境構築

ここからは Elastic Beanstalk の環境構築を行う。

以前の投稿も参考に。

先ほど作成した Rails プロジェクトのディレクトリ内で以下手順を行う。

アプリケーション枠の作成

eb initコマンドでアプリケーション枠を作成する。SSH キーペアは作成してアップロード済み。

$ eb init blog \
--platform "Ruby 2.1 (Puma)" \
--keyname aws-eb

.gitignore が更新されるので add & commit する。

$ git commit -am "updated .gitignore"

Elastic Beanstalk 環境のカスタマイズ設定

EC2 インスタンスタイムゾーンを日本に変更するため、カスタマイズ設定ファイルを準備する。

.ebextensionsディレクトリを作成する。

$ mkdir -p .ebextensions/scripts

Timezone を設定するシェルスクリプトを作成する。

.ebextensions/scripts/timezone.sh

#!/bin/bash
cp -f /usr/share/zoneinfo/Japan /etc/localtime
sed -i -e 's/ZONE=.*$/ZONE="Asia\/Tokyo"/' /etc/sysconfig/clock
sed -i -e 's/UTC=.*$/UTC=false/' /etc/sysconfig/clock
echo 'ARC=false' >> /etc/sysconfig/clock

.ebextensions ディレクトリ直下に config ファイルを作成する。

この config ファイルはデプロイする度に実行される。
タイムゾーン設定は初回の EC2 インスタンス作成時のみ実行すればよいので、test コマンドを使って2回目以降は実行されないようにした。

.ebextensions/01_timezone.config

container_commands:
  01-change_timezone:
    test: '[ ! -f /root/.not-a-new-instance.txt ]'
    command: bash .ebextensions/scripts/timezone.sh
  02-create_check_file:
    test: '[ ! -f /root/.not-a-new-instance.txt ]'
    command: touch /root/.not-a-new-instance.txt

ここまでできたら Git に add & commit する。

$ git add -A && git commit -m "add extensions"

Elastic Beanstalk 環境の作成

eb createコマンドでインスタンスの作成を行う。オプションについてはこちら

  • 環境名はblog-productionとする。
  • 作成したVPC(vpc blog)を指定する。
  • ELB と EC2 には同じパブリックサブネット(subnet public blog web)を指定する。
  • ELB と EC2 をパブリックサブネットに置くので、それぞれelbpublicpublicipを設定する。
  • デフォルトのセキュリティグループ(sg blog default)を指定する。
  • cname オプションでcom-example-blog-productionを設定する。
    • cname オプションを設定することで ELB に CNAME をつけることができる。URL スワップ時に使用する。
  • sample オプションをつける。

sample オプションをつける理由:
eb createコマンドを実行すると、環境構築 -> Rails アプリデプロイ -> DB マイグレーションまで行われるが、config/database.yml の DB 関連のパラメータは環境変数から取得するようなっており、まだ環境変数は未設定なのでパラメータが取得できず、DBマイグレーションに失敗してしまう。

しかし、eb setenvによる環境変数の設定はeb create実行後でなければできない。

ワークアラウンドとして、1発目のデプロイでは--sampleオプションを与えて、カレントディレクトリの Rails プロジェクトではなく、サンプルアプリケーションをデプロイさせる方法で回避する。

$ eb create blog-production \
--sample \
--cname com-example-blog-production \
--instance_type t2.micro \
--region ap-northeast-1 \
--tier webserver \
--vpc.ec2subnets subnet-f23be885 \
--vpc.elbsubnets subnet-f23be885 \
--vpc.id vpc-d42ae5b1 \
--vpc.securitygroups sg-bdc574d8 \
--vpc.publicip \
--vpc.elbpublic

Environment details for: blog-production
  Application name: blog
  Region: ap-northeast-1
  Deployed Version: None
  Environment ID: e-3qvcc2mas3
  Platform: 64bit Amazon Linux 2014.09 v1.2.0 running Ruby 2.1 (Puma)
  Tier: WebServer-Standard-1.0
  CNAME: com-example-blog-production.elasticbeanstalk.com
  Updated: 2015-02-18 08:19:01.355000+00:00
Printing Status:
INFO: createEnvironment is starting.
INFO: Using elasticbeanstalk-ap-northeast-1-123456789012 as Amazon S3 storage bucket for environment data.
INFO: Created security group named: sg-cac677af
INFO: Created load balancer named: awseb-e-3-AWSEBLoa-19XNPJOJ2L4UJ
INFO: Created security group named: sg-d2c677b7
INFO: Created Auto Scaling launch configuration named: awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingLaunchConfiguration-1JJZT8EC6TQTN
INFO: Waiting for EC2 instances to launch. This may take a few minutes.
INFO: Created Auto Scaling group named: awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingGroup-XQ9W5EU82FGI
INFO: Created Auto Scaling group policy named: arn:aws:autoscaling:ap-northeast-1:123456789012:scalingPolicy:6c971967-dbd5-420c-b396-3e9077ca17c5:autoScalingGroupName/awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingGroup-XQ9W5EU82FGI:policyName/awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingScaleUpPolicy-1QMQK2YUBBKEQ
INFO: Created Auto Scaling group policy named: arn:aws:autoscaling:ap-northeast-1:123456789012:scalingPolicy:ec062391-358f-4296-af75-71b20f5d1677:autoScalingGroupName/awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingGroup-XQ9W5EU82FGI:policyName/awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingScaleDownPolicy-6YRVJB2IR2C2
INFO: Created CloudWatch alarm named: awseb-e-3qvcc2mas3-stack-AWSEBCloudwatchAlarmHigh-O9GN3OBFYQK3
INFO: Created CloudWatch alarm named: awseb-e-3qvcc2mas3-stack-AWSEBCloudwatchAlarmLow-18E1BGLOHWKS
INFO: Added EC2 instance 'i-f59d7eed' to Auto Scaling Group 'awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingGroup-XQ9W5EU82FGI'.
INFO: Application available at com-example-blog-production.elasticbeanstalk.com.
INFO: Successfully launched environment: blog-production

画面確認

$ eb open
# http://com-example-blog-production.elasticbeanstalk.com

Elastic Beanstalk 環境変数の設定

eb setenvコマンドで環境変数を設定する。

$ eb setenv \
SECRET_KEY_BASE=`bin/rake secret` \
RDS_DB_NAME=blogdb \
RDS_USERNAME=bloguser \
RDS_PASSWORD=bloguser1234567890 \
RDS_HOSTNAME=myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com \
RDS_PORT=3306

INFO: Environment update is starting.
INFO: Updating environment blog-production's configuration settings.
INFO: Successfully deployed new configuration to environment.
INFO: Environment update completed successfully.

確認

$ eb printenv
 Environment Variables:
     AWS_SECRET_KEY = None
     RDS_PORT = 3306
     RAILS_SKIP_ASSET_COMPILATION = false
     BUNDLE_WITHOUT = test:development
     RDS_PASSWORD = bloguser1234567890
     SECRET_KEY_BASE = 72ecc50883dd60d2668c5074053c5aae9c7acf5409eccab206053b2cf8e7039871d86ab349fa99cf208350ecbec5091dbd22a3f274b2cb242747d860f5fd6b93
     RACK_ENV = production
     PARAM5 = None
     PARAM4 = None
     PARAM3 = None
     PARAM2 = None
     PARAM1 = None
     RDS_USERNAME = bloguser
     RDS_DB_NAME = blogdb
     RAILS_SKIP_MIGRATIONS = false
     AWS_ACCESS_KEY_ID = None
     RDS_HOSTNAME = myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com

Rails アプリのデプロイ

ここでカレントディレクトリの Rails プロジェクトをデプロイする。

$ eb deploy
INFO: Environment update is starting.
INFO: Deploying new version to instance(s).
INFO: New application version was deployed to running EC2 instances.
INFO: Environment update completed successfully.

画面確認。

$ eb open

アプリ画面が表示されることを確認。
とりあえず適当なデータを1件登録してみる。

ここで Beanstalk で作成した EC2 インスタンスタイムゾーンと RDS に保存されたレコードの時間を確認する。

まず、EC2 インスタンスの PublicDns, PublicIp を確認する。

# 環境名 blog-production でフィルタする
$ aws ec2 describe-instances --filters "Name=tag-value,Values=blog-production" | jq '.Reservations[].Instances[] | {PublicDnsName,PublicIpAddress}'
# またはインスタンスID指定
# $ aws ec2 describe-instances --instance-ids i-f59d7eed --filters "Name=tag-value,Values=blog-production" | jq '.Reservations[].Instances[] | {PublicDnsName,PublicIpAddress}'

{
  "PublicDnsName": "ec2-54-65-4-37.ap-northeast-1.compute.amazonaws.com",
  "PublicIpAddress": "54.65.4.37"
}

EC2 インスタンスSSH ログインしてタイムゾーンを確認。

$ ssh -i ~/.ssh/aws-eb ec2-user@54.65.4.37

[ec2-user@ip-10-0-0-189 ~]$ date
2015年  2月 18日 水曜日 17:34:28 JST

RDS MySQL にコマンド接続して、セッションの現在時間、テーブルの登録データの日時を確認。

$ mysql -h myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -P 3306 -u bloguser -p -D blogdb

mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2015-02-18 17:41:00 |
+---------------------+
1 row in set (0.01 sec)

mysql> select * from articles;
+----+-------+---------+---------------------+---------------------+
| id | title | content | created_at          | updated_at          |
+----+-------+---------+---------------------+---------------------+
|  1 | foo   | bar     | 2015-02-18 17:35:40 | 2015-02-18 17:35:40 |
+----+-------+---------+---------------------+---------------------+
1 row in set (0.01 sec)

mysql> \q

次回以降のデプロイでは DB マイグレーションが自動実行されないように環境変数RAILS_SKIP_MIGRATIONSを false から true に変更しておく。

$ eb setenv RAILS_SKIP_MIGRATIONS=true

ここまでで環境構築からアプリデプロイまでの工程が完了した。


後片付け

ここからは、ここまで作成してきたもの削除して元に戻す作業を行う。

Elastic Beanstalk リソース群の削除

ELB、EC2 などの Beanstalk リソース群のすべてを削除する。

$ eb terminate

The environment "blog-production" and all associated instances will be terminated.
To confirm, type the environment name: blog-production
INFO: terminateEnvironment is starting.
INFO: Deleted CloudWatch alarm named: awseb-e-3qvcc2mas3-stack-AWSEBCloudwatchAlarmLow-18E1BGLOHWKS
INFO: Deleted CloudWatch alarm named: awseb-e-3qvcc2mas3-stack-AWSEBCloudwatchAlarmHigh-O9GN3OBFYQK3
INFO: Deleted Auto Scaling group policy named: arn:aws:autoscaling:ap-northeast-1:123456789012:scalingPolicy:ec062391-358f-4296-af75-71b20f5d1677:autoScalingGroupName/awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingGroup-XQ9W5EU82FGI:policyName/awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingScaleDownPolicy-6YRVJB2IR2C2
INFO: Deleted Auto Scaling group policy named: arn:aws:autoscaling:ap-northeast-1:123456789012:scalingPolicy:6c971967-dbd5-420c-b396-3e9077ca17c5:autoScalingGroupName/awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingGroup-XQ9W5EU82FGI:policyName/awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingScaleUpPolicy-1QMQK2YUBBKEQ
INFO: Waiting for EC2 instances to terminate. This may take a few minutes.
INFO: Deleted Auto Scaling group named: awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingGroup-XQ9W5EU82FGI
INFO: Deleted Auto Scaling launch configuration named: awseb-e-3qvcc2mas3-stack-AWSEBAutoScalingLaunchConfiguration-1JJZT8EC6TQTN
INFO: Deleted load balancer named: awseb-e-3-AWSEBLoa-19XNPJOJ2L4UJ
INFO: Deleted security group named: sg-d2c677b7
INFO: Deleted security group named: sg-cac677af
INFO: Deleting SNS topic for environment blog-production.
INFO: terminateEnvironment completed successfully.

RDS 関連の削除

RDS インスタンスの削除

データも削除されるので注意!

# スナップショットは作らず削除
$ aws rds delete-db-instance --db-instance-identifier myinstance --skip-final-snapshot

# スナップショットを作ってから削除
$ aws rds delete-db-instance --db-instance-identifier myinstance \
--no-skip-final-snapshot \
--final-db-snapshot-identifier myinstance-final-snapshot

インスタンス削除が完了するまで待つ。

DB パラメーターグループの削除

DB パラメーターグループmydbparamgroupを削除する。

$ aws rds delete-db-parameter-group --db-parameter-group-name mydbparamgroup
DB サブネットグループの削除

DB サブネットグループmydbsubnetgroupを削除する。

$ aws rds delete-db-subnet-group --db-subnet-group-name mydbsubnetgroup
VPC セキュリティグループの削除

VPC セキュリティグループmyrdsを削除する。

$ aws ec2 delete-security-group --group-id sg-14c77671

VPC 関連の削除

Subnet 削除
$ aws ec2 delete-subnet --subnet-id subnet-f23be885
$ aws ec2 delete-subnet --subnet-id subnet-ff3be888
$ aws ec2 delete-subnet --subnet-id subnet-d721d58e
カスタム Route Table 削除
$ aws ec2 delete-route-table --route-table-id rtb-2073a745
IGW を VPC からデタッチする
$ aws ec2 detach-internet-gateway --internet-gateway-id igw-85d63ce0 --vpc-id vpc-d42ae5b1
IGW 削除
$ aws ec2 delete-internet-gateway --internet-gateway-id igw-85d63ce0
VPC 削除
$ aws ec2 delete-vpc --vpc-id vpc-d42ae5b1

同時に

  • Route Table(main)
  • Network ACL
  • Security Group(default)

も削除される。

ちなみにコマンドから削除する場合は、親の VPC から削除することはできず、上記のように子の方から順に依存関係を解除しないとその親は削除できない。
一方、GUIの管理コンソールから削除する場合は、親の VPC から削除することが可能で、その下に紐付くものすべて削除される(インスタンスが残っている場合などは無理)。

RDS(MySQL)の文字コードとタイムゾーンの設定

環境:
Mac
aws-cli 1.7.0
jq 1.4
RDS MySQL(5.6.22)

RDS MySQL文字コードタイムゾーンの設定を行ったのでその手順メモ。
操作はすべて aws-cliコマンドラインで行う。

1. 文字コードの設定

RDS MySQL文字コードおよび照合順序はデフォルトの状態でインスタンスを立ち上げると以下のようになる。

mysql> show global variables like 'character\_set\_%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | latin1 |
| character_set_connection | latin1 |
| character_set_database   | latin1 |
| character_set_filesystem | binary |
| character_set_results    | latin1 |
| character_set_server     | latin1 |
| character_set_system     | utf8   |
+--------------------------+--------+
7 rows in set (0.01 sec)

mysql> show global variables like 'collation%';
+----------------------+-------------------+
| Variable_name        | Value             |
+----------------------+-------------------+
| collation_connection | latin1_swedish_ci |
| collation_database   | latin1_swedish_ci |
| collation_server     | latin1_swedish_ci |
+----------------------+-------------------+

登録するデータとして絵文字が含まれるので utf8mb4 を使えるように設定を行う。

DB パラメーターグループ作成

RDS の設定値を変更するにはDB パラメーターグループを通して行う。
まずは、mysql5.6 用の DB パラメーターグループの雛形を元にして新規の DB パラメーターグループをrds create-db-parameter-groupコマンドで作成する。

  • db-parameter-group-name はmydbparamgroupとする。
$ aws rds create-db-parameter-group --db-parameter-group-name mydbparamgroup --db-parameter-group-family mysql5.6 --description "for myinstance"

{
    "DBParameterGroup": {
        "DBParameterGroupName": "mydbparamgroup",
        "DBParameterGroupFamily": "mysql5.6",
        "Description": "for myinstance"
    }
}

DB パラメーターグループ更新

続いて作成した DB パラメーターグループのキャラクタセット関連のパラメータの初期値をrds describe-db-parametersコマンドで確認する。
ParameterValue はすべて未設定となっている。

$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
 | jq '.Parameters[] | select(.ParameterName | contains("character") or contains("collation")) | { Description,DataType,Source,IsModifiable,ParameterName,ParameterValue,ApplyType }'

{
  "Description": "Don't ignore character set information sent by the client.",
  "DataType": "boolean",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "character-set-client-handshake",
  "ParameterValue": null,
  "ApplyType": "static"
}
{
  "Description": "The character set for statements that arrive from the client.",
  "DataType": "string",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "character_set_client",
  "ParameterValue": null,
  "ApplyType": "dynamic"
}
{
  "Description": "The character set used for literals that do not have a character set introducer and for number-to-string conversion.",
  "DataType": "string",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "character_set_connection",
  "ParameterValue": null,
  "ApplyType": "dynamic"
}
{
  "Description": "The character set used by the default database.",
  "DataType": "string",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "character_set_database",
  "ParameterValue": null,
  "ApplyType": "dynamic"
}
{
  "Description": "The file system character set.",
  "DataType": "string",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "character_set_filesystem",
  "ParameterValue": null,
  "ApplyType": "dynamic"
}
{
  "Description": "The character set used for returning query results to the client.",
  "DataType": "string",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "character_set_results",
  "ParameterValue": null,
  "ApplyType": "dynamic"
}
{
  "Description": "The server's default character set.",
  "DataType": "string",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "character_set_server",
  "ParameterValue": null,
  "ApplyType": "dynamic"
}
{
  "Description": "The collation of the connection character set.",
  "DataType": "string",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "collation_connection",
  "ParameterValue": null,
  "ApplyType": "dynamic"
}
{
  "Description": "The server's default collation.",
  "DataType": "string",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "collation_server",
  "ParameterValue": null,
  "ApplyType": "dynamic"
}
{
  "Description": "Ignore character set information sent by the client.",
  "DataType": "boolean",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "skip-character-set-client-handshake",
  "ParameterValue": null,
  "ApplyType": "static"
}

DB パラメーターグループのキャラクタセット関連のパラメータをrds modify-db-parameter-groupコマンドで更新する。

  • character_set_filesystemcharacter_set_system以外のキャラクタセットutf8mb4を設定する。
  • collation はutf8mb4_general_ciにする。
  • skip-character-set-client-handshakeは TRUE にするとクライアント側が要求してきたキャラクタセットは無視して強制的にサーバ側のcharacter-set-serverで指定したキャラクタセットで応答するようになるらしい。
    今回はアプリのクライアント側でキャラクタセットを指定するのでこの設定はオフにする。
  • ApplyMethod は immediate は即時反映で、pending-reboot は再起動後の反映となる。
    Apply Type が dynamic のものは immediate で、static のものは pending-reboot となるようだ。
$ aws rds modify-db-parameter-group --db-parameter-group-name mydbparamgroup --parameters \
ParameterName=character_set_client,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=character_set_connection,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=character_set_database,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=character_set_results,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=character_set_server,ParameterValue=utf8mb4,ApplyMethod=immediate \
ParameterName=collation_connection,ParameterValue=utf8mb4_general_ci,ApplyMethod=immediate \
ParameterName=collation_server,ParameterValue=utf8mb4_general_ci,ApplyMethod=immediate \
ParameterName=skip-character-set-client-handshake,ParameterValue=0,ApplyMethod=pending-reboot

{
    "DBParameterGroupName": "mydbparamgroup"
}

DB パラメータグループに反映されたかrds describe-db-parametersコマンドで確認してみる。

$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '[ .Parameters[] | select(.ParameterName | contains("character") or contains("collation")) | { key: .ParameterName, value: .ParameterValue } ] | from_entries'

{
  "character-set-client-handshake": null,
  "character_set_client": "utf8mb4",
  "character_set_connection": "utf8mb4",
  "character_set_database": "utf8mb4",
  "character_set_filesystem": null,
  "character_set_results": "utf8mb4",
  "character_set_server": "utf8mb4",
  "collation_connection": "utf8mb4_general_ci",
  "collation_server": "utf8mb4_general_ci",
  "skip-character-set-client-handshake": "0"
}

RDS MySQL インスタンス作成

create-db-instanceコマンドで RDS MySQL インスタンスを作成する。

  • 上記で作成した DB パラメータグループをオプションで指定する。
  • db-instance-identifier はmyinstanceとする。
  • RDS MySQL に外部から接続したいので publicly-accessible を追加。
$ aws rds create-db-instance \
--db-instance-identifier myinstance \
--allocated-storage 5 \
--db-instance-class db.t2.micro \
--engine MySQL \
--engine-version 5.6.22 \
--master-username bloguser \
--master-user-password bloguser1234567890 \
--db-name blogdb \
--db-parameter-group-name mydbparamgroup \
--storage-type standard \
--no-multi-az \
--region ap-northeast-1 \
--publicly-accessible \
--no-auto-minor-version-upgrade

コマンドを実行すると RDS インスタンスの作成がはじまる。しばらく時間がかかるので待つ。

  • VPC 指定をしていないので RDS インスタンスdefault VPC内に作成される。
  • セキュリティグループはdefault VPC security groupが設定される。
  • DBサブネットグループはdefaultが設定される。

インスタンスの作成が完了したら、rds describe-db-instancesコマンドでエンドポイントを確認する。

$ aws rds describe-db-instances --db-instance-identifier myinstance

...
            "Endpoint": {
                "Port": 3306,
                "Address": "myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"
            },
...

PublicIp、PublicDnsName はec2 describe-network-interfacesコマンドで確認できる。
GUI 管理コンソールでは、EC2 Dashboard -> Network interfaces から確認できる。

$ aws ec2 describe-network-interfaces --filters "Name=description,Values=RDSNetworkInterface"

...
            "Description": "RDSNetworkInterface",
            "Association": {
                "PublicIp": "54.92.100.249",
                "PublicDnsName": "ec2-54-92-100-249.ap-northeast-1.compute.amazonaws.com",
                "IpOwnerId": "amazon-rds"
            },
...

デフォルトセキュリティグループに Inbound ルール追加

RDS MySQL に外部から接続するためにdefault VPC security groupに Inbound のルールを追加する。
セキュリティグループ ID を確認。

$ aws rds describe-db-instances --db-instance-identifier myinstance

...
        "VpcSecurityGroups": [
            {
                "Status": "active",
                "VpcSecurityGroupId": "sg-xxxxxxxx"
            }
        ],
...

ec2 authorize-security-group-ingressコマンドで追加する。

$ aws ec2 authorize-security-group-ingress --group-id sg-xxxxxxxx --protocol tcp --port 3306 --cidr 0.0.0.0/0

RDS MySQL のキャラクタセット確認

上で調べた Endpoint または PublicIp、PublicDnsName のいずれかをホストに指定して MySQL に接続する。

$ mysql -h myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -P 3306 -u bloguser -p -D blogdb

MySQL に接続できたら、キャラクタセットの設定を確認する。

mysql> show create database blogdb\G
*************************** 1. row ***************************
       Database: blogdb
Create Database: CREATE DATABASE `blogdb` /*!40100 DEFAULT CHARACTER SET utf8mb4 */
1 row in set (0.01 sec)

mysql> show variables like 'character\_set\_%';
+--------------------------+---------+
| Variable_name            | Value   |
+--------------------------+---------+
| character_set_client     | utf8    |
| character_set_connection | utf8    |
| character_set_database   | utf8mb4 |
| character_set_filesystem | binary  |
| character_set_results    | utf8    |
| character_set_server     | utf8mb4 |
| character_set_system     | utf8    |
+--------------------------+---------+

mysql> show global variables like 'character\_set\_%';
+--------------------------+---------+
| Variable_name            | Value   |
+--------------------------+---------+
| character_set_client     | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database   | utf8mb4 |
| character_set_filesystem | binary  |
| character_set_results    | utf8mb4 |
| character_set_server     | utf8mb4 |
| character_set_system     | utf8    |
+--------------------------+---------+

mysql> show variables like 'collation%';
+----------------------+--------------------+
| Variable_name        | Value              |
+----------------------+--------------------+
| collation_connection | utf8_general_ci    |
| collation_database   | utf8mb4_general_ci |
| collation_server     | utf8mb4_general_ci |
+----------------------+--------------------+

mysql> show global variables like 'collation%';
+----------------------+--------------------+
| Variable_name        | Value              |
+----------------------+--------------------+
| collation_connection | utf8mb4_general_ci |
| collation_database   | utf8mb4_general_ci |
| collation_server     | utf8mb4_general_ci |
+----------------------+--------------------+

global variables は指定通り utf8mb4 になっているが、session の variables は utf8 になっている箇所がある・・・

MySQL クライアント側のキャラクタセットがどうなっているか調べてみる。

今、手元の Mac にインストールした MySQL(Server version: 5.6.19 Homebrew)から接続しているので、そのローカルの MySQL にログインする。

$ mysql -uroot

mysql> show variables like 'character\_set\_%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | utf8   |
| character_set_connection | utf8   |
| character_set_database   | utf8   |
| character_set_filesystem | binary |
| character_set_results    | utf8   |
| character_set_server     | utf8   |
| character_set_system     | utf8   |
+--------------------------+--------+

mysql> show global variables like 'character\_set\_%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | utf8   |
| character_set_connection | utf8   |
| character_set_database   | utf8   |
| character_set_filesystem | binary |
| character_set_results    | utf8   |
| character_set_server     | utf8   |
| character_set_system     | utf8   |
+--------------------------+--------+

mysql> show variables like 'collation%';
+----------------------+-----------------+
| Variable_name        | Value           |
+----------------------+-----------------+
| collation_connection | utf8_general_ci |
| collation_database   | utf8_general_ci |
| collation_server     | utf8_general_ci |
+----------------------+-----------------+

mysql> show global variables like 'collation%';
+----------------------+-----------------+
| Variable_name        | Value           |
+----------------------+-----------------+
| collation_connection | utf8_general_ci |
| collation_database   | utf8_general_ci |
| collation_server     | utf8_general_ci |
+----------------------+-----------------+

character_set_clientなど、すべて utf8 になっていたのでこれが原因だろう。クライアント側でキャラクタセットを指定して再度、RDS MySQL に接続してみる。

コマンドラインのオプションで--default-character-setを指定する。

$ mysql --default-character-set=utf8mb4 -h myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -P 3306 -u bloguser -p

再度、キャラクタセットの設定を確認する。

mysql> show variables like 'character\_set\_%';
+--------------------------+---------+
| Variable_name            | Value   |
+--------------------------+---------+
| character_set_client     | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database   | utf8mb4 |
| character_set_filesystem | binary  |
| character_set_results    | utf8mb4 |
| character_set_server     | utf8mb4 |
| character_set_system     | utf8    |
+--------------------------+---------+

mysql> show global variables like 'character\_set\_%';
+--------------------------+---------+
| Variable_name            | Value   |
+--------------------------+---------+
| character_set_client     | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database   | utf8mb4 |
| character_set_filesystem | binary  |
| character_set_results    | utf8mb4 |
| character_set_server     | utf8mb4 |
| character_set_system     | utf8    |
+--------------------------+---------+

mysql> show variables like 'collation%';
+----------------------+--------------------+
| Variable_name        | Value              |
+----------------------+--------------------+
| collation_connection | utf8mb4_general_ci |
| collation_database   | utf8mb4_general_ci |
| collation_server     | utf8mb4_general_ci |
+----------------------+--------------------+

mysql> show global variables like 'collation%';
+----------------------+--------------------+
| Variable_name        | Value              |
+----------------------+--------------------+
| collation_connection | utf8mb4_general_ci |
| collation_database   | utf8mb4_general_ci |
| collation_server     | utf8mb4_general_ci |
+----------------------+--------------------+

今度はセッションの設定も utf8mb4 となった。

一応、skip-character-set-client-handshakeを TRUE の場合もやってみた。
DB パラメータグループを更新する。

$ aws rds modify-db-parameter-group --db-parameter-group-name mydbparamgroup --parameters \
ParameterName=skip-character-set-client-handshake,ParameterValue=1,ApplyMethod=pending-reboot

変更を反映させるためにrds reboot-db-instanceコマンドで RDS インスタンスの再起動させる。

$ aws rds reboot-db-instance --db-instance-identifier myinstance

同じように RDS MySQL に接続してキャラクタセットを確認したところ、クライアント側で要求したキャラクタセットは無視されて、すべて utf8mb4 となることが確認できた。


2. タイムゾーンの設定

※追記:タイムゾーンの設定が可能になり以下で設定できる。init_connectを使ったワークアラウンドは不要になった

$ aws rds modify-db-parameter-group --db-parameter-group-name mydbparamgroup --parameters \
ParameterName=time_zone,ParameterValue=Asia/Tokyo,ApplyMethod=immediate

Amazon Web Services ブログ: Amazon RDS (MySQL, MariaDB)がlocal time zoneをサポートしました
http://aws.typepad.com/aws_japan/2015/12/amazon-rds-local-timezone-support.html

--
以下、タイムゾーンの変更ができなかった頃のワークアラウンド

RDS MySQLタイムゾーンJST にしたいが、現時点ではタイムゾーンの変更はできず、デフォルトの UTC となっている。
調べてみるとワークアラウンドとして、init_connect変数を使って接続毎にタイムゾーンを設定する方法があるようなので、それをやってみる。
以下を参考にさせてもらった。

AWS - RDS(MySQL)でJSTを使う たった1つの冴えたやり方 - Qiita
http://qiita.com/j3tm0t0/items/089ef96ba131df079ca4
MySQL,RDS - RDS(MySQLエンジンにてタイムゾーンを変更する方法を整理) - Qiita
http://qiita.com/mitzi2funk/items/4726986e8288b1599786
» AWS RDSのタイムゾーンについて Tech Fun.cc
http://techfun.cc/aws/aws-rds-timezone.html

init_connectで単純にSET SESSION time_zone = 'Asia/Tokyo';とやるのでは問題が発生するとのこと。
RDS 管理用のrdsadminというユーザがいて、このユーザが何かする時に Timezone が UTC 以外だと不具合が発生してしまうみたい。
以下のようにrdsadminユーザ以外の接続時のみ Timezone を変更するようにすればよい。

SET SESSION time_zone = CASE WHEN POSITION('rds' IN CURRENT_USER()) = 1 THEN 'UTC' ELSE 'Asia/Tokyo' END;

デフォルトの設定を確認する

RDS MySQL にログインしてタイムゾーンを確認する。

# ローカル Mac の現在時間
$ date
2015年 2月17日 火曜日 01時27分19秒 JST


# RDS MySQL に接続
$ mysql -h myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -P 3306 -u bloguser -p


# 現在時間、タイムゾーン確認
mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2015-02-16 16:27:51 |
+---------------------+

mysql> show variables like '%time_zone';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| system_time_zone | UTC   |
| time_zone        | UTC   |
+------------------+-------+

mysql> show global variables like '%time_zone';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| system_time_zone | UTC   |
| time_zone        | UTC   |
+------------------+-------+

UTC となっている。ローカルは JST なので当然ながら時間はズレている。

init_connect変数を設定

DB パラメータグループのinit_connect変数を更新する。
SET 文を aws-cli の直接コマンドに埋め込むのではなく、ファイルに書いてそれをアップロードする方法で行う。 まずはテンプレートとなるファイルを--generate-cli-skeletonオプションで出力させる。

$ aws rds modify-db-parameter-group --generate-cli-skeleton > init_connect.json

出力されたファイルを以下のように編集する。

{
    "DBParameterGroupName": "mydbparamgroup",
    "Parameters": [
        {
            "ParameterName": "init_connect",
            "ParameterValue": "SET SESSION time_zone = CASE WHEN POSITION('rds' IN CURRENT_USER()) = 1 THEN 'UTC' ELSE 'Asia/Tokyo' END;",
            "Description": "",
            "Source": "",
            "ApplyType": "",
            "DataType": "",
            "AllowedValues": "",
            "IsModifiable": true,
            "MinimumEngineVersion": "",
            "ApplyMethod": "immediate"
        }
    ]
}

そしてDB パラメータグループを更新する。--cli-input-jsonオプションでファイルを指定。

$ aws rds modify-db-parameter-group --cli-input-json file://init_connect.json

# 終わったら不要なので削除
$ rm init_connect.json

設定が反映されるまで待つ。
反映されたら再度 RDS MySQL に接続して確認してみる。

# ローカル Mac の現在時間
$ date
2015年 2月17日 火曜日 01時38分03秒 JST


# RDS MySQL 接続
$ mysql -h myinstance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -P 3306 -u bloguser -p


# 現在時間、タイムゾーン確認
mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2015-02-17 01:38:18 |
+---------------------+

mysql> show variables like '%time_zone';
+------------------+------------+
| Variable_name    | Value      |
+------------------+------------+
| system_time_zone | UTC        |
| time_zone        | Asia/Tokyo |
+------------------+------------+

mysql> show global variables like '%time_zone';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| system_time_zone | UTC   |
| time_zone        | UTC   |
+------------------+-------+

このセッションのtime_zoneAsia/Tokyoになっているのが確認できた。

後片付け

default VPC security groupに追加した Inbound ルールをec2 revoke-security-group-ingressコマンドで取り消す。

$ aws ec2 revoke-security-group-ingress --group-id sg-xxxxxxxx --protocol tcp --port 3306 --cidr 0.0.0.0/0

RDS インスタンスmyinstancerds delete-db-instanceコマンドで削除する(データも消えるので注意!)。

# スナップショットは作らず削除
$ aws rds delete-db-instance --db-instance-identifier myinstance --skip-final-snapshot

# スナップショットを作ってから削除
$ aws rds delete-db-instance --db-instance-identifier myinstance \
--no-skip-final-snapshot \
--final-db-snapshot-identifier myinstance-final-snapshot

インスタンス削除が完了するまで待つ。

DB パラメーターグループmydbparamgrouprds delete-db-parameter-groupコマンドで削除する。

$ aws rds delete-db-parameter-group --db-parameter-group-name mydbparamgroup

以上。


補足

DB パラメータグループの JSON データから jq コマンドで目的のデータを抽出するためにいろいろ試行錯誤しながら試した。
記録しておかないと次に再現できないのでメモしておく。

JSON データ

{
    "Parameters": [
        {
            "Description": "Controls whether user-defined functions that have only an xxx symbol for the main function can be loaded",
            "DataType": "boolean",
            "AllowedValues": "0,1",
            "Source": "engine-default",
            "IsModifiable": false,
            "ParameterName": "allow-suspicious-udfs",
            "ApplyType": "static"
        },
        {
            "Description": "The MySQL installation base directory.",
            "DataType": "string",
            "IsModifiable": false,
            "Source": "system",
            "ParameterValue": "/rdsdbbin/mysql",
            "ParameterName": "basedir",
            "ApplyType": "static"
        },

        ...
    ]
}

上記の元データから jq コマンドを使って、あるパラメータに特定の文字列を含むものだけを抽出して出力してみる。
jq コマンドはjq '.'jq '.Parameters[]'のようにシングルクォートで囲ったフィルタ式を渡すことで実行する。

例1

ParameterName に"character"を含むものを抽出する。

  1. .Parameters[]で Parameters の配列の全ての要素を取り出して出力する。.Parametersとは違う。
  2. contains()で()内の入力値に部分一致するかどうかの真偽値を返す。
  3. select()で ()内の条件が真になれば、その要素を出力する、偽であれば何も返さない。
$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '.Parameters[] | select(.ParameterName | contains("character"))'

下は上と同じように動作する、別の書き方。

$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '.Parameters[] | select(contains({ParameterName: "character"}))'

出力結果:

{
  "Description": "Don't ignore character set information sent by the client.",
  "DataType": "boolean",
  "AllowedValues": "0,1",
  "Source": "engine-default",
  "IsModifiable": true,
  "ParameterName": "character-set-client-handshake",
  "ApplyType": "static"
}
{
  "Description": "The character set for statements that arrive from the client.",
  "DataType": "string",
  "IsModifiable": true,
  "AllowedValues": "big5,dec8,cp850,hp8,koi8r,latin1,latin2,swe7,ascii,ujis,sjis,hebrew,tis620,euckr,koi8u,gb2312,greek,cp1250,gbk,latin5,armscii8,utf8,cp866,keybcs2,macce,macroman,cp852,latin7,utf8mb4,cp1251,cp1256,cp1257,binary,geostd8,cp932,eucjpms",
  "Source": "user",
  "ParameterValue": "utf8mb4",
  "ParameterName": "character_set_client",
  "ApplyType": "dynamic"
}
...
例2

{}を利用することで、JSON の内容を組み替えたオブジェクトを生成して出力してる。

$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '.Parameters[] | select(.ParameterName | contains("character")) | { desc: .Description, name: .ParameterName, value: .ParameterValue }'

出力結果:

{
  "desc": "Don't ignore character set information sent by the client.",
  "name": "character-set-client-handshake",
  "value": null
}
{
  "desc": "The character set for statements that arrive from the client.",
  "name": "character_set_client",
  "value": "utf8mb4"
}
...
例3
  1. "character"を含むものを抽出
  2. ParameterName, ParameterValue を key, value という名前のプロパティに変更してオブジェクトを作成
  3. []で囲んで配列を作成
  4. from_entriesで key-value のペアからオブジェクトへ変換
$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '[ .Parameters[] | select(.ParameterName | contains("character")) | { key: .ParameterName, value: .ParameterValue} ] | from_entries'

出力結果:

{
  "character-set-client-handshake": null,
  "character_set_client": "utf8mb4",
  "character_set_connection": "utf8mb4",
  "character_set_database": "utf8mb4",
  "character_set_filesystem": null,
  "character_set_results": "utf8mb4",
  "character_set_server": "utf8mb4",
  "skip-character-set-client-handshake": "0"
}
例4

"character"または、"collation"を含むものを抽出。orを使う。

$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '.Parameters[] | select(.ParameterName | contains("character") or contains("collation"))'

# 上と同じ
$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '.Parameters[] | select(contains({ParameterName: "character"}) or contains({ParameterName: "collation"}))'
例5

今までものをすべて組み合わせたもの。

desc, name, value のみ

$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '.Parameters[] | select(.ParameterName | contains("character") or contains("collation")) | { desc: .Description, name: .ParameterName, value: .ParameterValue }'

{
  "desc": "Don't ignore character set information sent by the client.",
  "name": "character-set-client-handshake",
  "value": null
}
{
  "desc": "The character set for statements that arrive from the client.",
  "name": "character_set_client",
  "value": "utf8mb4"
}

...

key-value 形式

$ aws rds describe-db-parameters --db-parameter-group-name mydbparamgroup \
  | jq '[ .Parameters[] | select(.ParameterName | contains("character") or contains("collation")) | { key: .ParameterName, value: .ParameterValue } ] | from_entries'

{
  "character-set-client-handshake": null,
  "character_set_client": "utf8mb4",
  "character_set_connection": "utf8mb4",
  "character_set_database": "utf8mb4",
  "character_set_filesystem": null,
  "character_set_results": "utf8mb4",
  "character_set_server": "utf8mb4",
  "collation_connection": "utf8mb4_general_ci",
  "collation_server": "utf8mb4_general_ci",
  "skip-character-set-client-handshake": "0"
}

参考:

jq Manual
http://stedolan.github.io/jq/manual/
軽量JSONパーサー『jq』のドキュメント:『jq Manual』をざっくり日本語訳してみました | Developers.IO
http://dev.classmethod.jp/tool/jq-manual-japanese-translation-roughly/
ゼロから始めるjqチュートリアル - JSONを解析/自在に出力する
http://blog.serverfrog.jp/jq-tutorial/
jq コマンドを使う日常のご紹介 - Qiita
http://qiita.com/takeshinoda@github/items/2dec7a72930ec1f658af