AWS x OpenVPNでP2P接続できる環境を作る

目的

遠隔地の端末XにNATを挟まずに接続したい!

端末Xの既存環境はいじりたくない!

(ついでに、クラウドと連携できると夢が広がるね!)


上記実現のために、

  1. VPN接続、かつルーティングのみで構成する
  2. 端末Xが接続するルーター(ex.ラズパイ等)に、VPNクライアント環境を構築する
  3. AWSパブリックサブネット上にVPNサーバーを構築する

想定するネットワークは以下の通り

image

赤、青、黒色のIPアドレスはそれぞれのサブネット内で割り振られたもので、紫色のIPアドレスはVPNネットワーク上で割り振られるアドレスとする

ここで、赤のVPNクライアント(10.8.0.6)から、青のVPNクライアントサブネット上の端末X(192.168.20.5)に、プライベートアドレス指定でアクセス可能な環境を構築する

具体的にやること

上記環境を構築するために、以下の手順で作業を行う

  1. AWSパブリックインスタンスにOpenVPNを導入する
  2. OpenVPN接続用の証明書を発行する
  3. サーバーの準備をする
  4. クライアントの準備をする
    1. mac編
    2. ubuntu編
  5. 各サブネットに接続確認を行う

なお、VPNクライアントすべてに対して、OpenVPN環境は未導入、VPNクライアント兼ルーターにはRaspberryPiを使用し、すでにルーターとして利用可能な状態であるとする

また、赤のVPNクライアントのOSはmacとする

1. AWSにOpenVPNを導入する

前提条件

AWS上のVPC、及びその内部のパブリックサブネットとプライベートサブネットは作成済みであるものとし、作成方法などの解説は行わない

以下に最低限必要な設定を示す

AWSパブリックサブネット上でVPNサーバーとして利用するインスタンスのOSには、Amazon Linux AMI 2016.09.1(HVM)を使用する

ボリュームサイズや処理性能は特に指定しないが、セキュリティグループ設定の際に、ssh及びVPN接続用に以下のポートを空けておく

  • ssh接続用ポート

    • タイプ:SSH
    • プロトコル:TCP
    • ポート範囲:22
    • 送信元:任意
  • VPN接続用ポート
    • タイプ:カスタムUDPルール
    • プロトコル:UDP
    • ポート範囲:1194
    • 送信元:任意

VPN接続用のポート設定は以降で利用する

他、必要に応じてソフトウェアの更新などを行っておく

合わせて、接続確認用にプライベートサブネット上にインスタンス(10.0.1.228)を立ち上げておく

パブリックサブネット上のインスタンスをOpenVPNサーバーとして動かすに当たり、導入が必要なものは以下の二つである

1. OpenVPN(v2.3.12)
2. easy-rsa(証明書方式の場合)(v3.0.0)

まずはsshでインスタンスに接続し、これらを導入する

OpenVPNのインストール

OpenVPNはyumでインストールできる

$ sudo yum install -y openvpn
...
完了しました!

基本的に、自動で自動起動に登録される

easy-rsaのインストール

拡張性、安全性、そして複数のクライアントを管理するVPNを構成するために、easy-rsaを利用する

easy-rsaはバイナリを取得、解凍して導入する

また、以降の作業は/usr/local/EasyRSA/ディレクトリ下で行うので移動しておく

$ wget https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.0-rc2/EasyRSA-3.0.0-rc2.tgz
...
2017-03-28 02:22:13 (107 KB/s) - `EasyRSA-3.0.0-rc2.tgz' へ保存完了 [34886/34886]
$ tar -xvzf EasyRSA-3.0.0-rc2.tgz
...
EasyRSA-3.0.0-rc2/easyrsa
$ sudo mv EasyRSA-3.0.0-rc2 /usr/local/EasyRSA
$ cd /usr/local/EasyRSA/

2. OpenVPN接続用の証明書を発行する

easy-rsaの初期設定

easy-rsaを導入した際には、以下のコマンドで認証情報を生成するための初期設定を行う必要がある

  1. init-pki
  2. build-ca
  3. gen-dh

init-pki

init-pkiを実行し、初期化を行う

$ ./easyrsa init-pki
init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: /usr/local/EasyRSA/pki

ここで生成された/usr/local/EasyRSA/pki/ディレクトリに、以降の手順で生成する認証用ファイルが配置される

build-ca

build-caで認証局を作成する

$ ./easyrsa build-ca
Generating a 2048 bit RSA private key
.+++
..+++
writing new private key to '/usr/local/EasyRSA/pki/private/ca.key'
Enter PEM pass phrase:<パスフレーズ>
Verifying - Enter PEM pass phrase:<パスフレーズ(確認)>
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:<ホスト名等>
CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
/usr/local/EasyRSA/pki/ca.crt
  • <パスフレーズ>:任意のパスワード

    • 証明書の発行の際に必要、覚えておくこと
  • <ホスト名等>:認証局を識別するための名称
    • 本気で運用するならばつけるべき
    • 今回はVPN接続目的のみのため適当な名称 or デフォルトで可

gen-dh

gen-dhでDHパラメータを生成する

  • DH:Diffie-Hellman

    • 公開鍵暗号方式の具体的な方法の一つ
    • DHは公開鍵暗号方式の概念を初めて公開した人たちの名前
$  ./easyrsa gen-dh
Generating DH parameters, 2048 bit long safe prime, generator 2
This is going to take a long time
...

DH parameters of size 2048 created at /usr/local/EasyRSA/pki/dh.pem

以上で、証明書の発行準備は完了である

次の手順から、実際に使用するサーバー/クライアント向けの認証ファイルの生成を行っていく

サーバー用秘密鍵・証明書の生成

build-server-fullでサーバー用秘密鍵と証明書の作成、及び署名を行う

$ ./easyrsa build-server-full server nopass
Generating a 2048 bit RSA private key
..........................+++
..................................+++
writing new private key to '/usr/local/EasyRSA/pki/private/server.key'
-----
Using configuration from /usr/local/EasyRSA/openssl-1.0.cnf
Enter pass phrase for /usr/local/EasyRSA/pki/private/ca.key:<登録したパスフレーズ>
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'server'
Certificate is to be certified until Mar 26 02:26:31 2027 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated
  • 証明書読み込み時にパスフレーズ不要とする場合はnopassオプションを使用する

    • オプションを使用しない場合、VPN接続時に毎回、パスフレーズの入力が求められる
  • 途中でCA証明書生成時に設定したパスフレーズの入力を求められるので入力する

クライアント用秘密鍵・証明書の生成

build-client-fullでクライアント用秘密鍵と証明書の作成、及び署名を行う

以下に、ファイルを生成するコマンドの例を示す

$  ./easyrsa build-client-full client1 nopass

Generating a 2048 bit RSA private key
.....................................................................+++
.........+++
writing new private key to '/usr/local/EasyRSA/pki/private/client1.key'
-----
Using configuration from /usr/local/EasyRSA/openssl-1.0.cnf
Enter pass phrase for /usr/local/EasyRSA/pki/private/ca.key:<登録したパスフレーズ>
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'client1'
Certificate is to be certified until Mar  8 07:31:37 2025 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated
  • client1には任意のクライアント名を指定する

    • ex, client1,client2,client3,and more…
    • ex, alpha,beta,and more…
  • パスフレーズ不要とする場合はnopassオプションを使用する

同様に、必要なだけクライアント用の秘密鍵、証明書の生成を行う

今回はclient1に加え、ルーター兼用クライアント向けにclient2も作成しておく

その際のコマンドは以下のようになる

$  ./easyrsa build-client-full client2 nopass

以上で、認証に必要なファイルの生成は終了である

次に、これらのファイルを配置し、VPN接続を行うための設定を行っていく

3. サーバーの準備をする

これまでに生成したファイルのうち、以下のファイルを/etc/openvpn/に移動する

  • ca証明書:ca.crt
  • サーバー用証明書:server.crt
  • サーバー用秘密鍵:server.key
  • DHパラメータ:dh2048.pem
$ sudo cp pki/ca.crt /etc/openvpn/
$ sudo cp pki/issued/server.crt /etc/openvpn/
$ sudo cp pki/private/server.key /etc/openvpn/
$ sudo cp pki/dh.pem /etc/openvpn/dh2048.pem

VPNサーバー設定ファイルのサンプルをコピーし、それをもとに実際にサーバーとして動かす際の設定を行う

サンプルの置かれているディレクトリは以下の通りである

/usr/share/doc/openvpn-2.3.12/sample/sample-config-files/server.conf

このサーバー設定ファイルも/etc/openvpn/に配置する

$ sudo cp /usr/share/doc/openvpn-2.3.12/sample/sample-config-files/server.conf /etc/openvpn/server.conf

サーバー設定ファイルを環境に合わせて修正する

好みのエディタで開き、編集する

$ sudo vi /etc/openvpn/server.conf

以下に編集例(抜粋)を示す

port 1194
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key
dh dh2048.pem
server 10.8.0.0 255.255.255.0
ifconfig-pool-persist ipp.txt
push "route 10.0.0.0 255.255.0.0"
push "route 192.168.20.0 255.255.255.0"
client-config-dir ccd
route 192.168.20.0 255.255.255.0
client-to-client
keepalive 10 120
comp-lzo
persist-key
persist-tun
status openvpn-status.log
log-append  openvpn.log
verb 3
  • portprototunはそれぞれ接続ポート番号、接続方式(udp/tcp)、インターフェイス形式(tap/tun)を指定する

    • 本環境では、UDP1194ポートをVPN接続用として開放しているので、これを指定する
    • この設定は、後述のクライアント設定ファイルでも使用する
  • cacertkeydhは、各々のサーバー用ファイルの場所を、/etc/openvpn/ディレクトリを基準とした相対パスで入力する
  • serverには、サーバーサブネットアドレス範囲を指定する
  • push "route ..."には、クライアントへ通知する、クライアントからアクセス可能なサブネットを指定する

    • 実際の動作としては、クライアントのルーティングテーブルに、指定したサブネットへアクセスする場合に、VPNサーバーへとルーティングするような設定が追加される
  • client-config-dir ccdオプションは、クライアントサブネットへアクセスする必要がある場合など、クライアントに関する設定が必要な際に有効化する

    • 有効化した際には/etc/openvpn/ccd/ディレクトリを作成しないと、openvpnそのものが正常に起動しなくなるので注意する(次の手順で作成)
    • ディレクトリ名称ccdは任意のディレクトリを指定可能
    • 合わせて、route ...により、クライアントサブネットとして192.168.20.0/24サブネットを、サーバーのルーティングテーブルに追加している
  • client-to-clientオプションは、クライアント間の通信、及びクライアントサブネット間の通信を許可する場合に有効化する
  • 設定項目については以下も参照

上記設定では、サーバー側からVPNクライアントに通知するルーティング設定として、以下の二つのサブネットにアクセスする際に、サーバーへとルーティングするような設定を加えている

  • 192.168.20.0/24
  • 10.0.0.0/16

前者のサブネットは、青のVPNクライアント兼ルーターの属するクライアントサブネットを示す

後者のサブネットは、AWSのVPC上のサブネット10.0.0.0/24(サーバーの立っているパブリックサブネット)と10.0.1.0/24(プライベートサブネット)を内包する

クライアントサブネットへの接続設定

VPNサーバーに、アクセス可能なVPNクライアントサブネットへのルートを登録する

サーバー設定ファイル上で追加したVPNクライアントサブネット192.168.20.0/24への通信をclient2へルーティングするよう設定を加える

まず、/etc/openvpn/ccd/ディレクトリを作成する

$ sudo mkdir /etc/openvpn/ccd

ディレクトリは、サーバー設定ファイル上で行った指定に従う

次に、ccdディレクトリ中にclient2という名称のファイルを生成し、iroute ...にサブネットを指定する

$sudo vi /etc/openvpn/ccd/client2

iroute 192.168.20.0 255.255.255.0

このiroute ...の設定は、OpenVPNサーバーに対し、192.168.20.0/24サブネット(上記設定の場合)への通信を、client2にルートさせるためのものである

サーバーをルーターとして使用可能にする

VPNサーバーにルーターとしての機能を持たせるためには、IPフォワーディングを許可する必要がある

以下のファイルを編集し、net.ipv4.ip_forwardを1に変更する

$ sudo vi /etc/sysctl.conf

net.ipv4.ip_forward = 1

以上で、インスタンス内で実施する必要のある設定は終了である

次に、AWS上で、ルーティング設定の修正を行う

AWS上の設定

AWS上のサブネットとデータの送受信を行うために、以下の設定を行う必要がある

  1. サブネットのルートテーブルへ、VPNネットワークのアドレスを登録
  2. VPNサーバーのインスタンスの送信元/送信先の変更チェックの無効化

ルートテーブルの修正

VPNクライアントからサーバー側サブネットにアクセスするため、サーバー側サブネットのルートテーブルにVPNサブネット(10.8.0.0/24)を登録する

VPNクライアントからサーバー側サブネットに接続するための設定はサーバー側設定ファイル上のpush "route ..."で実施済みであるが、サーバー側サブネットからVPNクライアントに接続するための情報は与えられていない

よって、AWSサブネットに紐づけられたルートテーブルの設定を修正する

  • AWS>[VPC]>[ルートテーブル]>サブネットに関連付けているルートテーブルを選択
    * [ルート]タブ>[編集]>[別ルートの追加]

    • 送信先:10.8.0.0/24(VPNサブネットを指定)
    • ターゲット:OpenVPNインスタンス(のネットワークインターフェイス)を指定

サーバー側サブネットからVPNクライアントサブネットにアクセスする場合には、同様の手順(送信先のみ変更)でルートテーブルに追加する

今回は、サーバー側サブネット-クライアントサブネット間のアクセスはないものと想定し、設定しない

送信元/送信先の変更チェックの無効化

AWSのEC2インスタンスは通常、自身のIPアドレス以外を指定した通信を無視する設定となっている

この状態では、クライアントサブネット間の通信のように、自インスタンス以外に向かう通信をルーティングすることができない

よって、この設定を無効化し、通信を受け取ることができるようにする必要がある

以下の設定を行う

  • AWS>[EC2]>VPNサーバーのインスタンスを選択

    • [アクション]>[ネットワーキング]>[送信元/送信先の変更チェック]
    • ダイアログにて、有効である場合には無効化を行う

02.png

ここまでの設定が終了したならば、念のためインスタンスを再起動させておく

4.1. クライアントの準備をする:mac編

以下の環境を想定し、クライアント側の準備を行う
(先述の想定環境中の赤のVPNクライアントを想定する)

  • OS:mac(macbook pro)
  • VPNクライアント:TunnelBlick(GUI)

クライアントへ必要なファイルを移動

SCPコマンド等を使用し、以下のファイルをクライアントへ移動する

  • ca証明書:ca.crt
  • クライアント用秘密鍵:client1.key等
  • クライアント証明書:client1.crt等

SCPコマンドを使用して、ファイルをダウンロードする際の例を以下に示す

  • 以下はssh接続用のキーが存在する場合の例

    • pemファイル名:xxx.pem
    • ダウンロード元アカウント名:ec2-user(AWSデフォルトの場合)
    • ダウンロード元グローバルIP:xxx.xxx.xxx.xxx(VPNサーバーグローバルIP)
    • ダウンロード元ファイル:/usr/local/EasyRSA/pki/private/ca.key等
    • ダウンロード先ディレクトリ:/etc/openvpn/
  • 接続先のポート番号を指定する場合は、scp -P xxxx -i ...
  • 接続先のパスは絶対パスで指定する
$ sudo scp -i ~/.ssh/xxx.pem ec2-user@xxx.xxx.xxx.xxx:/usr/local/EasyRSA/pki/private/ca.key /etc/openvpn
$ sudo scp -i ~/.ssh/xxx.pem ec2-user@xxx.xxx.xxx.xxx:/usr/local/EasyRSA/pki/issued/client1.crt /etc/openvpn
$ sudo scp -i ~/.ssh/xxx.pem ec2-user@xxx.xxx.xxx.xxx:/usr/local/EasyRSA/pki/private/client1.key /etc/openvpn

取得したファイルは/etc/openvpn/に配置する

クライアント設定ファイルの修正

必要があれば、クライアント設定ファイルのサンプルも、クライアントにコピーし使用する

あるいは、OpenVPNのサイトより、コピーして使用することもできる

ファイル名は任意であるが、ここではclient.confとする

以下にクライアント設定ファイルの設定例(抜粋)を示す

client
dev tun
proto udp
remote <サーバーグローバルIP> 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert client1.crt
key client1.key
comp-lzo
verb 3
  • clientは、この設定ファイルを読んだ端末がクライアント側であることを示す
  • devprotoの指定はサーバー設定ファイル(前述)に従う
  • remoteには、<サーバーグローバルIP>とサーバーポート番号をスペース区切りで記述する

    • <サーバーグローバルIP>にはグローバルIPでなくとも、クライアントからサーバーにアクセスできるIPアドレス、あるいはホスト名を指定する
    • サーバーポート番号の指定はサーバー設定ファイルに従う
  • cacertkeyには、max x tunnelblick環境では、/etc/openvpn/ディレクトリを基準としたパスを指定する

    • 今回は/etc/openvpn/直下のためディレクトリ指定は行う必要はない

Tunnelblickへの登録

作成したclient.confを、Tunnelblickで開く

  • エラーが出た場合は修正する

    • どこが間違っているかは教えてくれる
    • tls設定がサンプル時点で有効になっている場合があるので確認が必要
  • 特に、ca証明書のディレクトリ指定などに注意する

問題なく登録が終わったならば、”接続”できるはずである

4.2. クライアントの準備をする:ubuntu編

インストール手法などは異なるものの、linux系統でOpenVPNをクライアントとして導入する手順は同一である
(今回は、具体的にRaspberryPiにOpenVPNを導入する場合を想定する)

ここで、クライアントが所属するサブネットワークへのアクセスを許可する場合、VPNクライアント側で直接設定をする必要は基本的には存在しない

サブネットへのルーティング設定は、VPNサーバー側の設定のみで良い

ただし、ルーター上でサブネットの設定を行う必要はあるが、すでにルーターとして使用可能な状態であり、設定済みであるとして、今回は説明しない

ここでは以下の環境を想定し、クライアント側の準備を行う

  • OS:ubuntu(macbook pro上でrefind使用)
  • VPNクライアント:OpenVPN(CUI)

OpenVPNのインストール

以下のコマンドでOpenVPNをインストールする

$ sudo apt-get install openvpn

クライアントへ必要なファイルを移動

SCPコマンド等を使用し、以下のファイルをクライアントへ移動する

  • ca証明書:ca.crt
  • クライアント用秘密鍵:client1.key等
  • クライアント証明書:client1.crt等

取得したファイルは/etc/openvpn/に配置する

クライアント設定ファイルの修正

VPNクライアント設定ファイルのサンプルをコピーし、それをもとに実際の設定を行う

サンプルの置かれているディレクトリは以下の通りである

/usr/share/doc/openvpn-2.3.12/sample/sample-config-files/client.conf

サーバー設定ファイルが置かれていたディレクトリと同一である

このクライアント設定ファイルも/etc/openvpn/に配置する

$ sudo cp /usr/share/doc/openvpn-2.3.12/sample/sample-config-files/client.conf /etc/openvpn/client.conf

サーバー設定ファイルを環境に合わせて修正する

好みのエディタで開き、編集する

$ sudo vi /etc/openvpn/client.conf

以下に編集例(抜粋)を示す

client
dev tun
proto udp
remote <サーバーグローバルIP> 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca /etc/openvpn/ca.crt
cert /etc/openvpn/client1.crt
key /etc/openvpn/client1.key
comp-lzo
verb 3
  • 一番上のclientは、このファイルを読んだ端末がクライアント側であることを示す
  • cacertkeyには絶対パスを指定する

なお、上記編集例はサーバー設定ファイルの内容に対応する

VPNサーバーへの接続

VPNのクライアントとして起動させるには、起動の際にクライアント設定ファイルを指定する必要がある

以下のコマンドで、クライアントようコンフィグファイルを指定し、クライアントとしてVPNサーバーへ接続する

$ sudo /usr/sbin/openvpn /etc/openvpn/client.conf
...
... Initialization Sequence Completed

※コンソールを一つ占有することに注意する

5. 各サブネットに接続確認を行う

ここでは、以下のような環境で、クライアントからVPNサーバーを経由し、サーバー側サブネットへtracerouteを行う

ここまでの手順通りに行っていれば、問題なく接続できるはずである

netstatコマンドなどで、対応するルーティングが行われているか確認する

VPNクライアントからping、あるいは以下のようなtracerouteコマンドが通れば、正常に設定できている

$ traceroute 10.0.1.228
traceroute to 10.0.1.228 (10.0.1.228), 64 hops max, 52 byte packets
1 10.8.0.1 (10.8.0.1) xxx ms xxx ms xxx ms
2 10.0.1.228 (10.0.1.228) xxx ms xxx ms xxx ms
$ traceroute 10.8.0.10
traceroute to 10.8.0.10 (10.8.0.10), 64 hops max, 52 byte packets
1 10.8.0.10 (10.8.0.10) xxx ms xxx ms xxx ms
$ traceroute 192.168.20.5
traceroute to 192.168.20.5 (192.168.20.5), 64 hops max, 52 byte packets
1 10.8.0.10 (10.8.0.10) xxx ms xxx ms xxx ms
2 192.168.20.5 (192.168.20.5) xxx ms xxx ms xxx ms

以上

続きを読む

LambdaでRDSチュートリアル

AWSのAPI GatewayとLambdaとRDSを用いて、RDSに保存されているデータを外部に公開するAPIを作成することを想定して、まずは、LambdaからRDSに接続する

想定するテーブル構造

# テーブル名: link_clicks
| カラム名   | description       | 
| path      | urlのパス         |
| clicks    | リンクのクリック数   |
| stat_date | 集計日             |

1. Lambda関数を作成する

lambdaを用いて、RDS(mysql)に接続して、クエリパラメータ(path)に応じた情報を引き出して返すものプログラムをpythonで作成する

1.1 pythonパッケージ(api)を作る

mkdir api

1.2 pythonの依存パッケージをインストールする

mysqlへの接続のためにPyMySQLをインストールする

pip install PyMySQL -t api

1.3 メイン関数を記載する

メイン関数(handler)記載ファイルと設定ファイルを用意する
(YOUR_XXXは置換する)

api/api.py
# -*- coding: utf-8 -*-
import sys
import logging
import rds_config
import pymysql
import datetime as DT
import json

#rds settings
rds_host  = rds_config.db_host
name = rds_config.db_username
password = rds_config.db_password
db_name = rds_config.db_name


logger = logging.getLogger()
logger.setLevel(logging.INFO)

try:
    mysql_client = pymysql.connect(rds_host, user=name, passwd=password, db=db_name, connect_timeout=5)
except:
    logger.error("ERROR: Unexpected error: Could not connect to MySql instance.")
    sys.exit()

logger.info("SUCCESS: Connection to RDS mysql instance succeeded")

def handler(event, context):
    """
    This function fetches content from mysql RDS instance
    """

    path = event["path"]

    query = "select sum(clicks) from link_clicks where path = '%(path)s' group by path" % dict(path=path)
    logger.info(query)

    results = []
    with mysql_client.cursor() as cur:
        cur.execute(query)
        for row in cur:
            clicks = int(row[0])
            results.append({"clicks": clicks})

    return json.dumps(results)
api/rds_config.py
db_host = "YOUR_HOST"
db_username = "YOUR_USERNAME"
db_password = "YOUR_PASSWORD"
db_name = "YOUR_DB_NAME"

1.4 lambdaにプログラムをアップロード

deploy.sh
# zip作成
(cd api && zip -r - *) > api.zip

# create lambda function
## YOUR_ROLEにはlambda-vpc-execution-roleを指定すること
aws lambda create-function \
--region YOUR_REGION \
--function-name api  \
--zip-file fileb://api.zip \
--role YOUR_ROLE \
--handler api.handler \
--runtime python2.7 \
--vpc-config SubnetIds=YOUR_SUBNET_IDS,SecurityGroupIds=YOUR_SECURITY_GROUP_ID

# update lambda function
aws lambda update-function-code \
--region YOUR_REGION \
--function-name  api  \
--zip-file fileb://api.zip

2. Lambda関数を確認する

2.1 Lambda関数がアップロードされたことを確認する

以下にアクセスして、apiファンクションが追加されたことを確認する
https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/functions?display=list

apiファンクションが追加されてる.png

2.2 テストイベントを設定する

actions > configure test event からテストイベントを登録する
設定したものは以下の通り

{
  "path": "/11111"
}

testイベント設定.png
テストイベントとしてpathを設定.png

2.3 テストする

上記画像の save&test をクリックしてテストする
テスト結果が表示される.png

最後に

以上の手順で、lambda => RDSに接続して、データを参照できるようになりました
詳細はawsのドキュメントにも書いてあるので、そちらを参考にしてください
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/vpc-rds-create-lambda-function.html

続きを読む

Terraform Best Practices in 2017

Terraform Best Practices in 2017

以下のブログをベースにver0.9の新機能のstate environmentsや、backend、remote stateを活用してベストプラクティスを考えた。
細かい話は以下のブログを参照いただき、ver0.9に対応した内容だけ記載します。
Terraformにおけるディレクトリ構造のベストプラクティス | Developers.IO

サンプルコード

サンプルコードを置きましたので、イメージが付かない場合は以下を見てみて下さい。
(適当に作ったので間違えてたらプルリクください)
https://github.com/shogomuranushi/oreno-terraform

ディレクトリ構造

├── environments
│   ├── not_immutable
│   │   ├── provider.tf
│   │   ├── backend.tf
│   │   ├── variable.tf
│   │   ├── main.tf
│   │   └── output.tf
│   └── immutable
│       ├── provider.tf
│       ├── backend.tf
│       ├── variable.tf
│       ├── main.tf
│       └── output.tf
└── modules
    ├── compute
    │   ├── ec2.tf
    │   ├── elb.tf
    │   ├── output.tf
    │   ├── userdata.sh
    │   └── variable.tf
    ├── db
    │   ├── main.tf
    │   ├── output.tf
    │   └── variable.tf
    └── vpc
        ├── main.tf
        ├── output.tf
        └── variable.tf

ディレクトリ構造のポイント

1. environments配下の分け方

  • tfstateファイルで管理する範囲が大きいと問題があった際の影響範囲が大きくなるため実行単位を小さくする
  • 今回の場合は、not_immutableとimmutableで分けて、それぞれの配下でterraformを実行する
  • terraformの実行単位を分けるとterraform間での値の受け渡しが通常とは異なり、 remote state 機能を利用する必要がある
    • 以下のようにdataを定義することで、remote側のoutputを参照できるようになる

    • 注意点
      • 制約としてremote先のmoduleの先のoutputは読み取れないのでmodule直下(root)でoutputを定義する必要がある
      • tfstateの管理方法をs3にした状態でstate environmentsを使った時のs3のprefixは env:/<environment>/ なるため以下のように記述する
remote_state参照方法(backend.tf内に記述)
data "terraform_remote_state" "not_immutable" {
  backend = "s3"
  config {
    bucket = "< backetname >"
    key    = "env:/${terraform.env}/not_immutable/terraform.tfstate"
    region = "< region >"
  }
}
moduleへの渡し方
module "compute" {
    source         = "../../modules/compute"

    vpc            = "${data.terraform_remote_state.not_immutable.vpc}"
}

2. dev/stg/prodなどの環境の分け方

  • ver0.9以前はdev,stg,prodなどはディレクトリを分けることで、tfstateを競合させないようにしていたが、ver0.9で追加された state environments を利用して環境を分ける
  • terraform env new dev を打つことでdevの環境が作られる

    • デフォルトでは直下に terraform.state.d というディレクトリができ、その配下に環境毎にtfstateが管理される
  • terraform env list を打つことで現在のenviromentを参照可能

state_environmentsの実行方法
$ terraform env new dev

$ terraform env list
  default
* dev
  stg
  • その前にtfstateはs3に置いたほうが良いと思うので、以下の記述も入れて terraform init を実行することでtfstateをs3で管理出来る状態になる。その後に terraform apply を実行することでtfstateが生成される

    • なお、s3をbackendにすると /env:/< environment > が補完され < backetname >/env:/< environment >/immutable/terraform.tfstate のように管理される
backend.tf
terraform {
  backend "s3" {
    bucket = "< backet name >"
    key    = "immutable/terraform.tfstate"
    region = "us-west-2"
  }
}
initの実行方法
$ terraform init

state environmentsの活用方法

今まではdevやstg、prodを別のディレクトリで管理していたため、それぞれのディレクトリにvariableを置くような形だったが、state environmentsの登場により1つのディレクトリで複数の環境を扱えるようになった。
そこで如何に効率的に複数の環境を扱えるか考えた結果、以下になった。

  • map関数をガンガン使う

    • environments配下のvariable.tfには以下のようにmapで定義する
  • map関数のkeyの部分をドット区切りでenv情報を入れる
    • 環境毎に値を定義出来る

      • env毎の切り替え方法は、値取得時に "vpc-${lookup(var.common, "${terraform.env}.region", var.common["default.region"])}"
        }
        のように ${terraform.env} にdevやstgが入りvalueとして参照可能になる
    • envの値が無ければdefaultを参照するように定義する方法は以下
      • defaultの指定方法は "vpc-${lookup(var.common, "${terraform.env}.region", var.common["default.region"])}"
        }
        のように ${lookup(key, value, default) で指定可能

それらを踏まえたコードは以下

variable側
variable "common" {
    default = {
        default.region     = "us-west-2"
        default.project    = "oreno-project"

        dev.region         = "us-west-2"
        stg.region         = "us-west-2"
        prd.region         = "ap-northeast-1"
  }
}

# VPC
variable "vpc" {
    type = "map"
    default = {
        default.cidr       = "10.0.0.0/16"
        default.public-a   = "10.0.0.0/24"
        default.public-c   = "10.0.1.0/24"
        default.private-a  = "10.0.2.0/24"
        default.private-c  = "10.0.3.0/24"
    }
}
module呼び出し時
module "vpc" {
    source       = "../../modules/vpc"
    common       = "${var.common}"
    vpc          = "${var.vpc}"
}
module内からvariableの値を取得する時
resource "aws_vpc" "vpc" {
    cidr_block                  = "${lookup(var.vpc, "${terraform.env}.cidr", var.vpc["default.cidr"])}"
    enable_dns_support          = "true"
    enable_dns_hostnames        = "true"
    tags {
        Name                    = "vpc-${lookup(var.common, "${terraform.env}.project", var.common["default.project"])}"
    }
}
参考:こんな感じでproviderでもlookup可能
provider "aws" {
    region = "${lookup(var.common, "${terraform.env}.region", var.common["default.region"])}"
}

まとめ

  1. 影響範囲を小さくするため、terraformの実行単位は小さくしましょう
  2. terraform間の受け渡しは remote state を使いましょう
  3. state environments を使って環境を分けましょう
  4. map関数を使ってmodule等に渡す時などのコードを簡素化しましょう
  5. map関数 & state environments & default定義を使ってvariableを効率化させましょう

以上

続きを読む

Terraformのoutputでmapを利用する方法

やること

Terraformのoutputでmapを利用する

なにが嬉しいか

module間で値を受け渡す時のoutputの定義が短くなる

Befor: 冗長的なoutput

output側の定義
output "vpc_id" {
  value = "${aws_vpc.vpc.id}"
}

output "public-a" {
  value = "${aws_subnet.public-a.id}"
}

output "public-c" {
  value = "${aws_subnet.public-c.id}"
}

output "private-a" {
  value = "${aws_subnet.private-a.id}"
}

output "private-c" {
  value = "${aws_subnet.private-c.id}"
}
moduleから呼び出す時
module "compute" {
  source = "../../xxx"

  vpc_id     = "${module.xxx.vpc_id}"
  public-a   = "${module.xxx.public-a}"
  public-c   = "${module.xxx.public-c}"
  private-a  = "${module.xxx.private-a}"
  private-c  = "${module.xxx.private-c}"
}

After: mapを使ったoutput

outputの際にvalueの値に "${map("key", "value")}" を入れることでmapとして利用可能になる。

output側の定義
output "vpc" {
  value = "${
    map(
      "vpc_id",           "${aws_vpc.vpc.id}",
      "subnet-public-a",  "${aws_subnet.public-a.id}",
      "subnet-public-c",  "${aws_subnet.public-c.id}",
      "subnet-private-a", "${aws_subnet.private-a.id}",
      "subnet-private-c", "${aws_subnet.private-c.id}"
    )
  }"
}
moduleから呼び出す時
module "compute" {
  source = "../../xxx"
  vpc = "${module.xxx.vpc}"
}

スッキリ!!

参照方法

ちなみに以下のようにlookupでkeyを指定することでvalueを参照出来る。

.tf
subnet_ids = ["${lookup(var.vpc, "subnet-private-a")}"]}

続きを読む

Elastic Beanstalkトラブルシューティング集

公式のトラブルシューティングに載ってないものをまとめてみました。
いろいろハマった気もしますが、覚えているやつだけ記載しています。

と、その前に

基本的なデバッグ方法としては以下の通りです。

  1. eb create をするときに –debug をつける
  2. eb ssh して直接インスタンスの中を確認する
  3. eb logs する

インスタンスが生成できていなければ 1. 、インスタンスが生成できていれば 2. 、pingで疎通できれば 3. で探っていく感じになります。

Q1. Application Load Balancer を選択するとエラーになる

eb create するときに Select a load balancer type で application を選択すると、こんな感じのエラーが出る。

eb : Configuration validation exception: Invalid option value: 'null' (Namespace: 'aws:ec2:vpc', OptionName: 'Subnets'): Specify the subnets for the VPC for load balancer type application.

A1. コマンドオプションで指定する

VPC が複数ある環境の場合、Elastic Beanstalk がどの VPC を選んだらいいか判断できないようで、このエラーが発生するみたいです。
なので、 eb create のコマンドオプションで VPC 関連の情報を指定すれば OK です。

eb create xxxxxxxx \
    --elb-type application \
    --vpc.id vpc-xxxxxxxx \
    --vpc.elbsubnets subnet-xxxxxxxx,subnet-xxxxxxxx \
    --vpc.ec2subnets subnet-xxxxxxxx,subnet-xxxxxxxx \
    --vpc.elbpublic \
    --vpc.publicip

Q2. リソースが作成できない

以下のようなエラーが出てリソースの作成に失敗する。

Stack named 'awseb-e-xxxxxxxxxx-stack' aborted operation. Current state: 'CREATE_FAILED' Reason: The following resource(s) failed to create: [ALB, AWSEBV2LoadBalancerTargetGroup, AWSEBBeanstalkMetadata, AWSEBLoadBalancerSecurityGroup].

A2. 権限を付与する

実行ユーザーに AWSElasticBeanstalkFullAccess というポリシーをアタッチすれば OK です。
ここでいう実行ユーザーとは ~/.aws/config で指定している IAM ユーザーのことです。

Q3. composer / bundler などで落ちる

hooks/appdeploy/pre にある shell を実行するタイミングで落ちる。
例えばこんなエラーです。

/var/log/eb-activity.log を見てもだいたい解決しないのはお約束です。

ERROR: [Instance: i-xxxxxxxx] Command failed on instance. Return code: 1 Output: [CMD-AppDeploy/AppDeployStage0/AppDeployPreHook/10_composer_install.sh] command failed with error code 1: /opt/elasticbeanstalk/hooks/appdeploy/pre/10_composer_install.sh
++ /opt/elasticbeanstalk/bin/get-config container -k app_staging_dir
+ EB_APP_STAGING_DIR=/var/app/ondeck
+ cd /var/app/ondeck
+ '[' -f composer.json ']'
+ export COMPOSER_HOME=/root
+ COMPOSER_HOME=/root
+ '[' -d vendor ']'
++ /opt/elasticbeanstalk/bin/get-config optionsettings -n aws:elasticbeanstalk:container:php:phpini -o composer_options
+ PHP_COMPOSER_OPTIONS=--no-dev
+ echo 'Found composer.json file. Attempting to install vendors.'
Found composer.json file. Attempting to install vendors.
+ composer.phar install --no-ansi --no-interaction --no-dev

なんとかかんとか

Hook //opt/elasticbeanstalk/hooks/appdeploy/pre/10_composer_install.sh failed. For more detail, check /var/log/eb-activity.log using console or EB CLI.

この場合は出ているエラーのうち、なんとかかんとかの部分で判断できます。

A3-1. Cannot allocate memory

Cannot allocate memory と出ているなら composer の実行時にメモリが足りていない可能性が高いので、.ebextensions でメモリを増やす設定を追加します。

.ebextensions
option_settings:
  aws:elasticbeanstalk:container:php:phpini:
    memory_limit: 1024M

インスタンスを大きくするとかできるなら楽ちんです。
(swap ファイル作るでも解決できるはずだが調べてない)

A3-2. ErrorException

[ErrorException] Undefined index: hash

ERROR: bundle install failed!
の場合、lock ファイルおかしい可能性があります。

eb ssh をしてみて、lock ファイルを消してライブラリをインストールすると分かったりします。

# ruby なら
cd /var/app/ondeck/
sudo rm Gemfile.lock
bundle install

# php なら
cd /var/app/ondeck/
sudo rm composer.lock
composer.phar install

僕の場合は composer 自体のバージョンがローカル環境とあっていないためにエラーになっていました。
なので、.ebextensions でバージョンを指定して対応しました。

.ebextensions
commands:
  01updateComposer:
    command: export HOME=/root && /usr/bin/composer.phar self-update 1.5-dev

Q4. DB に繋がらない

エラーログをみると DB に繋げないっていうのです。

A4. ローカル IP からのアクセスを変更する

原因としては Application Load Balancer を選択したせいで subnet の IPv4 CIDR が複数のブロックになった可能性が疑われます。

なので、MySQL にログインして該当のブロックアドレスに GRANT してあげれば OK です。

use mysql;
SELECT Host, User, Password, Select_priv, Insert_priv,Update_priv, Delete_priv FROM user; -- 権限を調べてみる

GRANT ALL PRIVILEGES ON database.* to hoge@"ブロックアドレス%" IDENTIFIED BY 'password' WITH GRANT OPTION;

Q5. 環境を削除しても消えない

Elastic Beanstalk のイベントでこんな感じになって環境が消せないという症状です。

The environment termination step failed because at least one of the environment termination workflows failed.

A5. 問い合わせたら消してくれるっぽい

まあ影響ないし置いといたらいいんじゃないかな。。。

続きを読む

インフラチームによくある極小サーバーやバッチジョブをECSで集約したい

すごい。この記事には文字しかない。

話の発端

ほとんどアクセスが無く、あっても昼間しかなく、しかし消すわけにはいかない社内サービスのようなデモサービスのようなサーバーが豪勢にもEC2単独インスタンスを持っていると、CPUを使わなすぎてリソースがもったいなかったり、たまったCPUクレジットを捨ててしまったりしてもったいない。日時バッチサーバーみたいなものも似たような状況で、バッチ稼働時以外は上と同じ無駄を今も発生させている。

では、全部Elastic Container Serviceでコンテナにしてインスタンスを集約して、昼間ためたクレジットを夜中の日時バッチで使うのはどうか、という手を考えた。外国リージョンではAWS Batchってやつが使えるようになっているので、そいつが東京リージョンに来たときにうまいことECSから移行できると最高に素敵だ。

各ミニサービスのECSへの集約

上述ミニサーバー群はまがいなりにもサーバーで、しかもSSLなので、IPを固定したい。

が、コンテナにEIPを付けることはできず、Global IPを持たせたい場合は、かわりにELBを使うのが定石1

なのだが、さして重要でもないのにELBを使うのはオーバースペック、というかお金の無駄なので、Unmanagedなコンテナインスタンスを作成し、その中でnginxを動かして、コンテナにリバースプロキシすればーーー、ということを考えていた。コンテナインスタンス障害時にはまるごと作り直せばいいようなサービスレベルだし、それが簡単にできちゃうのがコンテナの長所だ。

が、じゃあそもそもnginxもコンテナでよくね、っていうことに気付いた。コンテナ間でリバースプロキシしちゃえばいいじゃん。

ELB無しでSSLできるコンテナインスタンスの作成

つまりコンテナインスタンスにEIPをつけたい、ということ。ECSのウィザードからクラスタを作成する場合、コンテナインスタンスのカスタマイズ可能項目は少ない。カスタム可能 (つまりコンテナインスタンス作成時に指定可能) なものは

  • インスタンスタイプ
  • ディスク容量
  • VPC
  • Security Group

だけで、Public IPとPrivate IPは自動的に割り当てられるため、コンテナインスタンス作成時にIPやEIPの指定はできない。なので、このあとに、作成されたEC2インスタンスに対して

  1. EIPを作成
  2. EIPをコンテナインスタンスのインターフェースに割り当て

ってやるとコンテナインスタンスのIPを安全に固定できる。

実際にEIPを付けた直後からコンテナに疎通可能になっていて、しかもECSから見えるコンテナインスタンス情報も自動で更新され、Global IPは新しいEIPになっていることが確認できる。便利な世の中になったもんだ。

コンテナ間リンク

nginxもコンテナなので、リバースプロキシ先はコンテナのリンクでOK、と思っていたのだが、なんとコンテナのリンクは同じタスク定義に属したコンテナ間のみで可能とのこと2。つまりexternal_linkは書けない。でも今回のユースケースだと、コンテナごとにタスク定義を変えるタイミング (Docker imageの更新とか) は異なるので、コンテナごとにタスク定義を作ることになる。

ってことは、コンテナごとに80:10080443:10443みたいなPort Mappingを書き、nginxはこのポートとコンテナインスタンスのローカルアドレスで、各コンテナへリバースプロキシする。まぁPort Mappingは複数コンテナインスタンスにまたがってコンテナを配置をしたい場合とかに結局必要だよね、たぶん。

Unmanaged Container Instanceを作りたい場合

ちなみに、どうしてもオレオレカスタムなUnmanagedコンテナインスタンスを作りたいときは

  1. 自力でEC2インスタンスを作成する

    • AWSが用意しているECS用のAMIを使う
    • か、それ以外を使う場合は自分でAgentのインストールなどが必要
    • めんどくさいけどEC2でインスタンスをいろいろいじれる
  2. ECSクラスタ作成時に空のクラスタを作る
  3. さっきのインスタンスをクラスタに追加する

という手順で可能らしい3

でもnginxもコンテナにしちゃうなら、ECSウィザード経由でデフォルトのAMI使う方が再作成と運用が楽な気がする。できるだけ外の世界へ出て行こうとしないのがパブリッククラウドの鉄則。

AWS Batch

ドキュメントを†熟読† (3分) した感じ、ECSにジョブキューを付けて、順番やリトライ、キューごとの計算環境の割り当て、実行時間やリソースの制限、優先度とかを定義できるようにしたものっぽい。†熟読†して思ったことは、

  • 要はPBS4サービスみたいなもんじゃね? PBSのインストールと運用は意外にめんどくさいらしい。というか需要がニッチすぎて活発には開発されてないらしい。通常SQSとかAMQPが実装された何かを使うんじゃないかしら。
  • GPUインスタンスが使えたりとか、AWSをスパコン的に使いたい人にはグッとくるのかもしれない5が、いるのかそんな人?並列性能上げると青天井でお金かかるし、1週間かかる機械学習だとやっぱり青天井でお金かかるし、そういう人は京とかどっかのスパコン借りた方が安いんじゃ……ぶつぶつ……

というわけで、AWS Lambdaで時間足りない人向け、という位置づけが現実的。

ECSからAWS Batchへの移行を考える

東京リージョンで今でも使えるECSから、今後東京リージョンにも展開されるであろうAWS Batchへの移行を考えると、既存のECSクラスタをAWS BatchのCompute Environment (CE) に変換できるとうれしいぞ。ドキュメントを読んでみると

By default, AWS Batch managed compute environments use the latest Amazon ECS-optimized AMI for compute resources.6

どうやらCEはECSのAMIをそのまま使うだけっぽいので、ECSクラスタを作ったあとにCEに追加できるのでは?と思ったが、 (画面上からは) ECSクラスタをCEに変換することはできなさそう。逆はできる、つまりCEをECSクラスタとして扱うことはできる。というか、CEを作った時点で空のECSクラスタが作成されている。末尾にUUIDとかついた、とてもゴチャったECSクラスタが作成される。

AWSというサービスの哲学として、Manageされるもの (e.g. ECSクラスタ) はManageするもの (e.g. CE) にはできない。これは言われれば分かるけど、気付かないと「不便だ」と不要な精神の不衛生を招く。

というわけで、ECSからそのままBatchに移行はできなさそうなので、今回のように特定のEIPを使い続けたいような場合、ECSコンテナインスタンスのEIPを、CEから生まれたECSコンテナインスタンスに移行時に付け替えることになる。

API使っても瞬断が出るが、まぁそもそもそこまでサービスレベルは高くない。はずだ。

でもこれって、ECSクラスタ間でコンテナインスタンスの移動ができれば解決するんじゃ?

と思って調べてみると

各コンテナインスタンスには、それぞれに固有の状態情報がコンテナインスタンスにローカルで保存され、Amazon ECS にも保存されているため、コンテナインスタンスを 1 つのクラスターから登録解除して別のクラスターに再登録しないでください。コンテナインスタンスリソースを再配置するには、1 つのクラスターからコンテナインスタンスを終了し、新しいクラスター内の Amazon ECS に最適化された最新の AMI で、新しいコンテナインスタンスを起動することをお勧めします。7

ぐはっ……。

サーバー系はECSに残し、バッチ系だけAWS Batchに、という住み分けが清潔なのかもしれないけど、それじゃ集約にならないなぁ。

Refs.

続きを読む

terraform importの使い方メモ

terraform importの使い方メモ

背景

terraformではv0.7.0以降で実装されたimportコマンドを使用して既存インフラをterraform管理下に置くことができます。といってもtfstateへのインポートのみです。しかもv0.9.2時点では一括インポートができないため、リソースIDを指定してインポートする必要があります。さらにその後terraform planで差分がなくなるまでtfファイルを作り込む面倒な作業が待っています。そのような事情もあって現時点ではdtan4/terraformingを使用する人が多いと思われますが、やはり対応リソースの広さに惹かれて本家の機能を使ってみました。

本題

事前にモジュール構造を決める

とりあえず手で環境を作ってHashicorpのbest practiceに従ったモジュール構造でコード化しようと試みました。作った環境は上記のコードとだいたい同じで、パブリックのサブネットとプライベートなサブネットがある単純なVPCです。(詳細は割愛)

インポート

何はともあれ公式のリファレンスに従ってインポートしてみます。vpc-idはマネジメントコンソールから拾ってきます。リソースの多い場合はSDKなどでIDを引っこ抜いてくるスクリプトを作ったほうが良いと思います。

$ terraform import aws_vpc.vpc vpc-********

次にvpcのtfファイルを作成し、planを実行しました。しかし既存リソースは定義なしと見なされ再作成の対象となってしまいます。

$terraform plan

- aws_vpc.vpc

+ module.network.vpc.aws_vpc.vpc

原因は一見してわかるようにモジュール構造が異なるためです。そこでモジュール指定でインポートしてみると、今度はリソース名を解決できないと怒られます。

$ terraform import module.network.vpc.aws_vpc.vpc vpc-********
Error importing: failed to parse resource address 'module.network.vpc.aws_vpc.vpc': Unexpected value for InstanceType field: "vpc"

モジュールの指定方法

インポートの際はplanの表示と違ってvpcモジュール側もmodule.vpcと指定する必要があったのでした。apply等と同じ仕様ですね。

$ terraform import module.network.module.vpc.aws_vpc.vpc vpc-********

この状態でterraform planを実行するとめでたく差分なしとなりvpcは破壊されなくて済むのでした。terraformの管理下におく場合はとりあえずインポートしてからtfファイルを作り始めるというよりモジュール構造を決めてからインポートした方が効率的と思います。

インポートできないリソース

ルートテーブルなどに紐づく関連付けやセキュリティグループのルールは個別のインポートができない仕様です。ルートテーブルに複数の関連付けなどがある場合自動的にリソース名の末尾に数字が付いた名前で別々のリソース定義としてインポートされてきます。

ここで気になるのが、例えばルートテーブルに対する関連付けを動的に生成するような管理をしたい場合です。例えば下記のようなコードでprivateルートテーブルに対する関連付けを一括して管理したいとします。

resource "aws_route_table_association" "private" {
  count          = "${length(var.private_subnet_ids)}"
  subnet_id      = "${element(var.private_subnet_ids, count.index)}"
  route_table_id = "${element(aws_route_table.private.*.id, count.index)}"
}

しかしimportは上位のルートテーブル単位でしかできません。

terraform import module.network.module.route_table.aws_route_table.private rtb-*******

このコマンドの結果はaws_route_table_association.privateaws_route_table_association.private-1などどと別々の定義としてtfstateに書き出されます。こうなると事後でstateに合わせるためにはtfファイルに関連付けの数だけリソース定義を書かないといけなくなりそうです。stateと差分がなければコードなんて何でもいいだろという思想も一理ありますが、やはりここは保守性や拡張性なども考えたいところです。

逆にtfstateをコードに合わせる

推奨はしませんがtfstateを手で編集するか、terraformをカスタマイズしてpull requestを出してしまうかですが、余程腕に自信のあるエンジニアでない限り前者の方が手っ取り早いでしょう。aws_route_table_association.private-1aws_route_table_association.private.1
などどコードに合わせて書き換えてしまいましょう。(自己責任)

tfstateの書き換えは下記の記事を参考にさせて頂きました
Terraforming未対応の既存リソースも自力でtfstateを書いてTerraform管理下に入れる

結論

まだまだ未完成の機能なので単にインポートするだけで綺麗にコード化して管理できるとはいかないようです。v1.0が待たれます。

続きを読む

[AWS] Terraform で EFS 作って、EC2 起動時にマウントさせておく

Terraform を使うと、EFS を作成して EC2 にマウントさせておくなんてことが簡単にできます。
Autoscaling 環境で Web ドキュメントルートを共有したい時とかに便利なんで、みんな使えばいいと思うよ。
なお、この記事の想定読者は AWS はダッシュボードからポチポチしてインスタンス立てたりしてるけど、そろそろインフラをコードで管理したいな。Terraform とか便利そうだねー使ってみたいねーって人です。
てわけで、単純に EC2 立ち上げても面白くないので EFS をマウントさせてみました。

そもそも、Terraform ってなんだ?って人は、以下のページとか参考になると思います。
Terraform簡易チュートリアル on AWS

実際の設定

Terraform は、特定のディレクトリ下にある拡張子が .tf なファイルを全部読み込んでいい感じにリソースを起動してくれます。なので、機能別に .tf 作成していってみましょう。

メイン設定

まず、メインの設定を作成。
プロバイダーとか、設定ファイル内で使用する変数とか設定していってみましょうか。

main.tf
# 今回のプロジェクト名
variable "project" {}
variable "domain" {}

# AWS リソースを起動するリージョンとかの情報
variable "region" { default = "us-west-2" }
variable "azs" {
    default {
        "a" = "us-west-2a"
        "b" = "us-west-2b"
        "c" = "us-west-2c"
    }
}

# AMI ID (Amazon Linux)
variable "ami" { 
    default {
        "us-west-2" = "ami-8ca83fec"
    }
}

# EC2 接続用の SSH 鍵の公開鍵
variable "ssh_public_key" {}

provider "aws" {
    region = "${var.region}"
}

variable で設定した値は tf ファイル内で ${var.region} のようにして参照可能です。
また、terraform の各種コマンドを実行する際に以下のようにパラメータとして変数を渡して上書きすることもできます。

$ terraform plan \
  -var 'project=example' \
  -var 'domain=example.com'

同じディレクトリ内に terraform.tfvars というファイルがあれば、それを読み込んで値が上書きされたりします。この辺の詳細は以下を参照してください。
Input Variables – Terraform by HashiCorp

provider "aws" は、aws を使いますよって宣言です。
以下のように アクセスキーを書いておくこともできますが、それやるとうっかり github とかに公開した時とかに切ない目にあうのでやめたほうが吉でしょう。

provider "aws" {
    access_key = "__ACCESS_KEY__"
    secret_key = "__SECRET_KEY__"
    region = "us-west-2"
}

環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY を読み込んでいい感じでやってくれるので、僕は direnv 使って作業ディレクトリ内で環境変数を変更することで対応してます。
(もちろん、この場合でも .gitignore.envrc を含めておいて間違って公開しないようにしないと切ない目にあうので注意)

VPC の作成

こんな感じの .tf ファイルで VPC と subnet が作成できます。

vpc.tf
## VPC
resource "aws_vpc" "app" {
    cidr_block           = "172.31.0.0/16"
    enable_dns_hostnames = true
    enable_dns_support   = true
    instance_tenancy     = "default"

    tags {
        "Name" = "${var.project}"
    }
}

## Subnet
resource "aws_subnet" "a" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.0.0/20"
    availability_zone       = "${lookup(var.azs,"a")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-a"
    }
}

resource "aws_subnet" "b" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.16.0/20"
    availability_zone       = "${lookup(var.azs,"b")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-b"
    }
}

resource "aws_subnet" "c" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.32.0/20"
    availability_zone       = "${lookup(var.azs,"c")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-c"
    }
}

resource "aws_subnet" の中に ${aws_vpc.app.id} ってのが出てきましたね。
Terraform の中では、管理下にあるリソースの情報を他のリソースの設定でも参照することが可能です。
各リソースで使用できる値が異なってくるので、その辺は公式ドキュメント読みましょう。
例えば aws_vpc で使用できる値は aws_vpc を参照すればわかります。

また、${lookup(var.azs,"a")} ってのも出てきましたね。
これは Terraform の組み込み関数です、lookup は配列の中からキーをもとに値を探す関数です。
詳しくは Built-in Functions を読んでください。

ついでに Internet Gateway と Route Table も設定しておきましょう。

route-table.tf
## Internet Gateway
resource "aws_internet_gateway" "igw" {
    vpc_id = "${aws_vpc.app.id}"
}

## Route Table
resource "aws_route_table" "rtb" {
    vpc_id     = "${aws_vpc.app.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.igw.id}"
    }
}

resource "aws_route_table_association" "route_a" {
    subnet_id = "${aws_subnet.a.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_b" {
    subnet_id = "${aws_subnet.b.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_c" {
    subnet_id = "${aws_subnet.c.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

IAM ロールの作成

次に EC2 に割り当てるための IAM ロールを作ってみましょう。
ポリシーは、AWS が用意している AmazonEC2RoleforDataPipelineRole と、EC2 から CloudwatchLogs にログを送信するためのカスタムポリシーを作ってアタッチしてみます。

iam-role.tf
## For EC2 instance Role
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

## AmazonEC2RoleforDataPipelineRole
resource "aws_iam_role_policy_attachment" "data-pipeline" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforDataPipelineRole"
}

## PutCloudwatchLogs
resource "aws_iam_policy" "put-cloudwatch-logs" {
    name        = "AmazonEC2PutCloudwatchLogs"
    description = ""
    policy      = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "put-cloudwatch-logs" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "${aws_iam_policy.put-cloudwatch-logs.arn}"
}

aws_iam_roleassume_role_policy のところと、aws_iam_policypolicy のところでヒアドキュメントが出てきましたね。
こんな風に複数行にわたるインラインポリシーはヒアドキュメントで記述することが可能です。
また、以下のように別ファイルにしておいて読み込ませることも可能です。
管理しやすい方でやってください。

iam-role.tf
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = "${file("data/instance_role_assume_policy.json")}"
}
data/instance_role_assume_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

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

EC2 から EFS へのアクセスは 2049 番ポートを介して行われるので、EFS が所属するセキュリティグループに穴を開けないといけません。
EC2 は 80, 443, 22 を解放してみます。

security-group.tf
## For EC2
resource "aws_security_group" "ec2" {
    name        = "${var.project}-EC2"
    description = "for ${var.project} EC2"
    vpc_id      = "${aws_vpc.app.id}"

    ingress = [
        {
            from_port       = 80
            to_port         = 80
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 443
            to_port         = 443
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 22
            to_port         = 22
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        }
    ]

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

## For EFS
resource "aws_security_group" "efs" {
    name        = "${var.project}-EFS"
    description = "for ${var.project} EFS"
    vpc_id      = "${aws_vpc.app.id}"

    ingress {
        from_port       = 2049
        to_port         = 2049
        protocol        = "tcp"
        security_groups = ["${aws_security_group.ec2.id}"]
    }

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

EFS の作成

こんな感じで EFS が作成できます。
各サブネットごとにマウントターゲットを作成して、そいつをセキュリティグループに所属させる形ですね。

efs.tf
resource "aws_efs_file_system" "app" {
  tags {
        "Name" = "${var.domain}"
  }
}

resource "aws_efs_mount_target" "app-a" {
  file_system_id  = "${aws_efs_file_system.app.id}"
  subnet_id       = "${aws_subnet.a.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-b" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.b.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-c" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.c.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

EC2 の作成

さて、いよいよ EC2 です。
ここでは、user-data を使って、初回ローンチ時に EFS をマウントさせてしまいます。
さらにマウントした EFS 内に html/ ってディレクトリを作成して、そいつを /var/www/html にシンボリックリンクしてみましょうか。
と言っても、こんな感じで大丈夫です。

ec2.tf
## IAM Instance Profile
resource "aws_iam_instance_profile" "instance_role" {
    name = "instance_role"
    role = "${aws_iam_role.instance_role.name}"
}

## SSH Key
resource "aws_key_pair" "deployer" {
  key_name   = "${var.project}"
  public_key = "${var.ssh_public_key}"
}

## EC2
resource "aws_instance" "app" {
    ami                         = "${lookup(var.ami,var.region)}"
    availability_zone           = "${aws_subnet.a.availability_zone}"
    ebs_optimized               = false
    instance_type               = "t2.micro"
    monitoring                  = true
    key_name                    = "${aws_key_pair.deployer.key_name}"
    subnet_id                   = "${aws_subnet.a.id}"
    vpc_security_group_ids      = ["${aws_security_group.ec2.id}"]
    associate_public_ip_address = true
    source_dest_check           = true
    iam_instance_profile        = "${aws_iam_instance_profile.instance_role.id}"
    disable_api_termination     = false

    user_data                   = <<USERDATA
#!/bin/bash
az="${aws_subnet.a.availability_zone}"
efs_region="${var.region}"
efs_id="${aws_efs_file_system.app.id}"
efs_mount_target="${aws_efs_mount_target.app-a.dns_name}:/"
efs_mount_point="/mnt/efs/$${efs_id}/$${az}"
web_doc_root="/var/www/html"

# EFS Mount
/usr/bin/yum -y install nfs-utils || /usr/bin/yum -y update nfs-utils
if [ ! -d $${efs_mount_point} ]; then
  mkdir -p $${efs_mount_point}
fi
cp -pi /etc/fstab /etc/fstab.$(date "+%Y%m%d")
echo "$${efs_mount_target}    $${efs_mount_point}   nfs4    defaults" | tee -a /etc/fstab
mount $${efs_mount_point}

# create Web document root
if [ -d $${web_doc_root} ]; then
  rm -rf $${web_doc_root}
fi
if [ ! -d $${efs_mount_point}/html ]; then
  mkdir $${efs_mount_point}/html
  chown ec2-user:ec2-user $${efs_mount_point}/html
fi
ln -s $${efs_mount_point}/html $${web_doc_root}
chown -h ec2-user:ec2-user $${web_doc_root}
USERDATA

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name"          = "${var.domain}"
    }
}

user_data は長めのシェルスクリプトなので、可読性が悪いから ${file("data/user_data.sh")} とかってやって別ファイルで管理したいですよね。
でも待ってください、ヒアドキュメントでやってるのは理由があるのです。

ヒアドキュメントで書くと、user_data 用のシェルスクリプトの中で Terraform の変数が使えます。
マウントするには EFS の ID とか、マウントターゲットの dns_name とか必要になってきますが、それを作成前に知らなくてもこのように書いておけるのです。便利ですね。
その代わり、user_data 用のシェルスクリプト内でローカルな環境変数を使いたい場合は $${efs_mount_point} のように書いてあげてくださいね。

ざっと、こんな感じです。
慣れちゃえば、tf ファイルを使い回しできるので便利ですよ。
また、すでに作成済みの AWS リソースを Terraform 管理下に置きたい場合は

$ terraform import aws_instance.app ${instance_id}

のようにして管理下に置くことができます。
管理されているリソースは terraform.tfstate というファイルに書き込まれます。
さらに別プロダクトになるのですが Terraforming と言うツールを使用すると、既存の AWS リソースから Terraform 用の tf ファイルを作成したり、terraform.tfstate を作成したりもできるので便利です。
Terraforming については、Terraforming で既存のインフラを Terraform 管理下におく を参考にしてください。

実際にリソース作成してみる

tf ファイル書いたら

$ terraform plan

で、設定ファイルに誤りがないか?既存のリソースへの影響はどの程度あるのかが確認できます。
実際に反映させたい時は

$ terraform apply

で、おっけ。

では、良い Terraform を!

続きを読む

RDSをAnsibleで管理する

はじめに

AnsibleにはAWSのリソースを操作できるモジュールが豊富に用意されています。

今回は、RDSをAnsibleで管理してみます。
RDSインスタンスを作成するためには、

  • サブネットグループ
  • パラメータグループ

が必要になるので、一気通貫で作成できるようにします。

やること

  • サブネットグループ作成
  • パラメータグループ作成
  • RDSインスタンス作成

ポイント

サブネットグループを作成するためにサブネットIDが、RDSインスタンスを作成するためにセキュリティグループIDがそれぞれ必要となりますが、IDをAnsibleのYAMLに書きたくないので、それぞれ名前からIDを取得する実装とします。

前提

  • AWS関連のモジュール実行にはbotoが必要です。
  • credential情報は環境変数かaws configureでセットしてある必要があります。
  • ec2_group_factsが必要です。

下記リソースを前提に進めます。

  • VPC

    • AnsibleVPC
  • サブネット
    • private-a
    • private-c
  • セキュリティグループ
    • db_server

sample

以下のようなRDSインスタンスを作成します。

  • サブネットグループ

    • private

      • private-a(AZ-a)
      • private-c(AZ-c)
  • パラメータグループ
    • mysql57-sample
  • RDSインスタンス
    • sample-db

      • セキュリティグループ

        • db_server
      • サブネットグループ
        • private
      • パラメータグループ
        • mysql57-sample

ディレクトリ構成

ディレクトリ構成
site.yml
roles/
|--rds/
|  |--tasks/
|  |  |--main.yml
hosts/aws    #inventory
host_vars/
|--localhost.yml

inventory

AWSリソース関連モジュールはすべてlocalhostで実行するので、下記のようなインベントリファイルを用意します。

hosts/aws
[aws]
localhost

vars

こんな感じに変数を定義します。今回はhost_varsで定義しました。
RDSインスタンスのパスワードをそのまま記載していますが、実際はansible-vaultで暗号化したファイルなどに記載してください。

host_vars/localhost.yml
---
my_vars:
  aws:
    common:
      region: ap-northeast-1
    vpc:
      name: AnsibleVPC    # ターゲットのVPC名
    rds:
      subnet_group:
        - name: private
          description: 'private subnet group'
          subnets:
            - private-a #サブネット名
            - private-c #サブネット名
      param_group:
        - name: mysql57-sample
          description: 'MySql5.7 sample'
          engine: mysql5.7
          params:
            character_set_database: utf8
            character_set_server: utf8
      instance:
        - name: sample-db
          db_engine: MySQL
          engine_version: 5.7.16
          multi_zone: no
          zone: a
          size: 5
          instance_type: db.t2.micro
          parameter_group: mysql57-sample
          subnet_group: private
          security_group:
            - db-server #セキュリティグループ名
          username: mysql_admin
          password: mysql_admin
          tags:
            Role: sample-db

Role

まずVPCを特定するためにidが必要ですが、こちらと同様、VPC名でidを取得します。

後続タスクで必要となる、サブネットIDとセキュリティグループIDを取得し、それぞれ名前からIDを参照するためにディクショナリを生成します。

roles/vpc/tasks/main.yml
---
- name: vpc_id取得
  ec2_vpc_net_facts:
    region: "{{ my_vars.aws.common.region }}"
    filters:
      "tag:Name": "{{ my_vars.aws.vpc.name }}"
  register: vpc_net_fact

- name: subnet id取得
  ec2_vpc_subnet_facts:
    region: "{{ my_vars.aws.common.region }}"
    filters:
      vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
      "tag:Name": "{{ item.1 }}"
  with_subelements:
    - "{{ my_vars.aws.rds.subnet_group }}"
    - subnets
  register: subnet_fact
  when: my_vars.aws.rds.subnet_group is defined

- name: subnet dict作成
  set_fact:
    subnets_dict: >-
      {%- set dict = {} -%}
      {%- set inc = 0 -%}
      {%- set subnet_group_cnt = my_vars.aws.rds.subnet_group|length -%}
      {%- for i in range(subnet_group_cnt) -%}
      {%-   set list = [] -%}
      {%-   for j in range(my_vars.aws.rds.subnet_group[i].subnets|length) -%}
      {%-     set _ = list.append(subnet_fact.results[inc+j].subnets[0].id) -%}
      {%-   endfor -%}
      {%-   set _ = dict.update({my_vars.aws.rds.subnet_group[i].name: list}) -%}
      {%-   set inc = inc + subnet_group_cnt -%}
      {%- endfor -%}
      {{ dict }}
  when: my_vars.aws.rds.subnet_group is defined

- name: securitygroup id取得
  ec2_group_facts:
    region: "{{ my_vars.aws.common.region }}"
    filters:
      vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
      group_name: "{{ item.1 }}"
  with_subelements:
    - "{{ my_vars.aws.rds.instance }}"
    - security_group
  register: sg_fact
  when: my_vars.aws.rds.instance is defined

- name: securitygroup dict作成
  set_fact:
    sg_dict: >-
      {%- set dict = {} -%}
      {%- for i in range(sg_fact.results|length) -%}
      {%-   set _ = dict.update({sg_fact.results[i].security_groups[0].group_name: sg_fact.results[i].security_groups[0].group_id}) -%}
      {%- endfor -%}
      {{ dict }}
  when: my_vars.aws.rds.instance is defined

- name: RDS subnet-group作成
  rds_subnet_group:
    state: present
    name: "{{ item.name }}"
    description: "{{ item.description }}"
    region: "{{ my_vars.aws.common.region }}"
    subnets: >-
      {%- set subnetname = item.name -%}
      {%- set list = subnets_dict[item.name] -%}
      {{ list }}
  with_items: "{{ my_vars.aws.rds.subnet_group }}"
  register: rds_subnet_group

- debug: var=rds_subnet_group

- name: RDS パラメータグループ作成
  rds_param_group:
    state: present
    name: "{{ item.name }}"
    description: "{{ item.description }}"
    region: "{{ my_vars.aws.common.region }}"
    engine: "{{ item.engine }}"
    params: "{{ item.params }}"
  with_items: "{{ my_vars.aws.rds.param_group }}"
  register: rds_param_group
  when: my_vars.aws.rds.param_group is defined

- debug: var=rds_param_group

- name: RDS インスタンス作成
  rds:
    command: create
    instance_name: "{{ item.name }}"
    db_engine: "{{ item.db_engine }}"
    engine_version: "{{ item.engine_version }}"
    region: "{{ my_vars.aws.common.region }}"
    multi_zone: "{{ item.multi_zone }}"
    zone: "{{ my_vars.aws.common.region }}{{ item.zone }}"
    size: "{{ item.size }}"
    instance_type: "{{ item.instance_type }}"
    parameter_group: "{{ item.parameter_group }}"
    subnet: "{{ item.subnet_group }}"
    vpc_security_groups: >-
      {%- set list = [] -%}
      {%- for i in range(item.security_group|length) -%}
      {%-   set _ = list.append(sg_dict[item.security_group[i]]) -%}
      {%- endfor -%}
      {{ list }}
    username: "{{ item.username }}"
    password: "{{ item.password }}"
  with_items: "{{ my_vars.aws.rds.instance }}"
  register: rds_instance
  when: my_vars.aws.rds.instance is defined

- debug: var=rds_instance

site.yml

site.yml
---
- name: rds
  hosts: localhost
  connection: local
  roles:
    - role: rds

実行

Command
$ ansible-playbook -i hosts/aws -l localhost site.yml

注意

Ansibleのrdsモジュールにはなぜかストレージタイプを指定するためのオプションがなく、作成されたインスタンスのストレージはマグネティックになってしまいますので適宜変更してください。

まとめ

RDSは、関連リソースを合わせて作成する必要がありますが、Ansibleで自動化できると楽です。
また、今回のplaybookはインスタンスの変更には対応していません。

参考になれば幸いです。

参考

続きを読む

CloudFormationの特徴とはじめ方

はじめに

本記事はCloudformationを始めようとしている人向けです。
細かい技術的な話よりは、特徴だったり始め方の手順だったりが書かれています。
公式ドキュメントを舐めるのが面倒な人はちょうどいいです。

概要

インフラ環境をテンプレート化し、何度実行しても同じ環境を簡単に構築することができる。
AWSのアイコンをペタペタ並べていくことでテンプレートを新規作成する「Design Template」機能や、既存のインフラ環境を自動でテンプレート化してくれる「CloudFormer」という機能がある。
また、コードでテンプレートを作成することも可能であり、デフォルトの言語はJSONとなっている。

メリット

大きな特徴として、以下が挙げられる。

  1. インフラがコードで管理できること
  2. 作成・削除・更新が容易であること

何がもたらされるのか

  • コードで管理できるということは、バージョン管理が容易になる。
    「インフラの成長過程がわかる」とよく言われる所以
  • オペレーションミスなし、作業者の技術力に依存しない環境構築が可能になる
  • 運用中、値変更があってもリスクなしでアップデートできる
  • 開発環境など限定的にしか使用しないインフラのランニングコストを削減できる
  • 既存のインフラ環境をすぐに可視化できる(構成図を作成・更新する工数削減)

CloudFormationの金額

無料で使用できる。立ち上がったリソースに対して課金が発生する。

機能

CloudFormation Designer

おなじみのAWSアイコンを配置し、繋いでいくことでテンプレートが作成できる。
作成したDesignは保存、ダウンロード、ローカルから開く、などができる。
アイコンは単純なAWSサービスだけではなく、ルートテーブルやVPCEndpointなど、普段は見ないアイコンも存在する。
理由は、この機能は構成図を作成するためのものではなく「テンプレートを作成する」機能のため、詳細なポリシーまで指定する必要がある。

CloudFormer

既存で構築されているインフラ環境をテンプレートに落とし込める機能。
自動でCloudFormation用のインスタンスを作成し、テンプレート作成準備を数分で構築する。
すべてのCloudFormationのリソースをサポートしている。
注意点としては、新規インスタンスを一台起動させて実行するため、課金が発生する。

詳しい特徴は以下の通り

  • どのリソースをテンプレートに含めるか制御可能
  • リソースを選択すると、従属するリソースも自動選択(変更可能)
  • EC2インスタンスを選択すると、そのインスタンスが起動された元のAMIが指定される

CloudFormerで生成されたテンプレートを手修正し、最終的なテンプレートとして利用することを推奨している

開始手順

CloudFormerスタックを作成する

  1. Cloudformation画面にて「Create New Stack」をクリック
  2. 「Select a sample template」のプルダウンから「Cloudformer」を選択
  3. 「Next」で次ページ
  4. 必要な情報を入力する
  5. Stack Name CloudFormerのスタック名
  6. Password CloudFormerにてテンプレートを作成するページへログインするためのパスワード
  7. Username CloudFormerにてテンプレートを作成するページへログインするためのユーザ名
  8. VPCSelection CloudFormer用インスタンスが立ち上がるVPC。特に変更はしない

  9. 「Next」で次ページ

  10. 必要な情報を入力する(特に設定しなくても進められる)

  11. TagとKeyの設定。リソースにタグを適用できるため、識別や分類したい場合は設定する

  12. Permissionsの権限がないとCloudformationは実行できないため、必ず実行権を持ったロールアカウントにする

  13. 「Next」で次ページ

  14. サマリを確認後、「Create」をクリック

  15. スタック作成が実行されるため、起動完了の「CREATE_COMPLETE」となるまで待機

Cloudformationテンプレートを作成する

  1. CloudFormerのスタックを選択し、「Outputs」タブを選択
  2. 「Value」に表示されるURLを踏み、テンプレート作成画面へ飛ぶ
  3. 「アクセス保護されてないページ」の警告を通過する
  4. テンプレート化を実施するリージョンを選択し「Continue」をクリック
  5. テンプレートに含めたいリソースの選択が続く。含める含めないを考慮しながら「Continue」をクリックしていく
  6. サマリを確認後、「Done」をクリック
  7. テンプレートが作成されているので、S3のどのバケットに保存するか選択する
  8. 終了

Cloudformerスタックを削除する

  1. テンプレート化が確認されたらスタックを削除(インスタンス分が課金されている)
  2. 終了

JSONテンプレート

テンプレートはデフォルトでJSON形式で記述される。
いくつかのセクションに分かれており、そのうち「Resources」セクションのみ必須で記述する必要がある。

セクションは以下の通り

  • Format Version(任意)

    CloudFormationのテンプレートバージョンを指定する
    
  • Description(オプション)
    テンプレートを説明するテキスト文字列。テンプレートのパラメータ等を入力する際に表示される
    
  • Metadata(オプション)
    テンプレートに関する追加情報を提供する
    
  • Parameters(任意)
    実行時にユーザに入力を求めるパラメータを定義する。インフラ構成は変えず、IPやインスタンスタイプを変えたいなどの運用で便利。
    
  • Mappings(任意)
    HashTableのようなもの。条件パラメータ値の指定に使用できる。リージョンやユーザ入力パラメータによって値が変わるものに利用する。Mappingsを利用することでテンプレートの再利用性が向上する
    
  • 条件(オプション)
    条件つきのリソースの制御が可能
    
  • 変換
    サーバレスアプリケーションの場合は、使用するAWS SAMのバージョンを指定する
    
  • Resources(必須)
    インスタンスやS3など、使用するリソースを指定する。
    
  • Output(任意)
    スタック構築後にCloudFormationから出力させる値(DNS名やEIP値など)
    

テンプレート作成のためのいくつか注意点(共通)

  1. CloudFormation Designerでアイコンを並べていくだけではテンプレートとして機能しない
  2. テンプレート作成し保存しただけではCloudFormationは実行できない
  3. 立ち上げたいインフラ環境のすべてをひとつのテンプレートとして作成すべきではない

1. CloudFormation Designerに関わらず、CloudFormationテンプレートではリソース同士の「依存関係」が重要になる

  • ここで言う依存関係とは、リソースを作成する順序
  • AリソースがBリソースを依存関係に指定している場合、Bリソースが作成されるまでAリソースは作成されない
  • この依存関係は、リソースを構築する上で避けて通ることができない
    ※備考※
    VPC内の一部リソースはゲートウェイを必要とする。
    VPC、ゲートウェイ、ゲートウェイアタッチメントを AWS CloudFormation テンプレートで定義する場合、
    ゲートウェイを必要とするリソースはすべて、そのゲートウェイアタッチメントに依存することになる。
    たとえば、パブリック IP アドレスが割り当てられている Amazon EC2 インスタンスは、
    同じテンプレートで VPC リソースと InternetGateway リソースも宣言されている場合、VPC ゲートウェイのアタッチメントに依存する。
    

現在、次のリソースは関連付けられたパブリック IP アドレスを持ち、VPC 内にある場合VPC ゲートウェイのアタッチメントに依存する。(2017年4月現在)

・Auto Scaling グループ
・Amazon EC2 インスタンス
・Elastic Load Balancing ロードバランサー
・Elastic IP アドレス
・Amazon RDS データベースインスタンス
・インターネットゲートウェイを含む Amazon VPC のルート

2. テンプレート作成後、スタックとして成立させる

  • パラメータを設定すること

    インスタンスタイプなどの値はスタック作成時に設定するケースが多い
    

3. 削除したくないリソースを予め決定しておくこと

  • CloudFormationで起動させたインフラ環境を停止する場合、インフラ環境を削除することになる

    削除対象のテンプレート内に固定IPが割り振られてたインスタンスがあった場合、インフラ環境を再構築した際に割り振りが変更してしまう。
    普遍なリソースはテンプレートを別にする
    

参考

AWS Cloudformationユーザガイド
http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html

CloudFormation超入門
http://dev.classmethod.jp/beginners/chonyumon-cloudformation/

AWS Black Belt Tech シリーズ 2015 – AWS CloudFormation
https://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-tech-2015-aws-cloudformation?qid=9c031227-6000-4a12-bf18-49cda2c6bfe9&v=&b=&from_search=1

続きを読む