CircleCIでRailsアプリをAmazon ECRにpushする

概要

railsアプリのテストとECRへのプッシュが目的。

サンプルコード

circle.yml
machine:
  services:
    - docker
  timezone:
    Asia/Tokyo
  environment:
    CIRCLE_ENV: test
dependencies:
  pre:
    - if [[ -e ~/docker/ruby.tar ]]; then docker load --input ~/docker/ruby.tar; fi
    - if [[ -e ~/docker/awesome-app-$CIRCLE_BRANCH.tar ]]; then docker load --input ~/docker/awesome-app-$CIRCLE_BRANCH.tar; fi
    - bundle --path=vendor/bundle
    - bundle exec rake assets:precompile
  cache_directories:
    - "~/docker"
    - "vendor/bundle"
    - "public/assets"
  override:
    - docker info
    - docker build -t $DOCKER_REPOS/awesome-app:$CIRCLE_BRANCH .
    - mkdir -p ~/docker
    - docker save -o ~/docker/ruby.tar ruby
    - docker save -o ~/docker/awesome-app-$CIRCLE_BRANCH.tar $DOCKER_REPOS/awesome-app:$CIRCLE_BRANCH
database:
  override:
    - bundle exec rake db:create db:schema:load db:migrate rake db:seed_fu
test:
  override:
    - bundle exec rubocop
    - bundle exec rspec
deployment:
  hub:
    branch: /^(master|staging)$/
    commands:
      - $(aws ecr get-login --region $AWS_REGION)
      - docker push $DOCKER_REPOS/awesome-app:$CIRCLE_BRANCH

dependencies

CircleCIだと、dockerイメージのキャッシュがされないので、
docker save & loadするっていうハックが一般的みたい。
~/dockerに逃してるが、将来docker imageがキャッシュされたら再検討。

bundleも忘れずにキャッシュされるようにしておく。

deployment

deploymentのセクションに関して、
master、stagingブランチの時だけECRにpushしたいという意図がある。
CIRCLE_BRANCHには、pushされたブランチ名が入るので、正規表現で切り分ける。

ENV

事前にCircleCI側にENVを設定する必要あり。
画面遷移: Setting->BUILD SETTINGS->Environment Variables

  • AWS_ACCESS_KEY_ID
  • AWS_ACCOUNT_ID
  • AWS_REGION
  • AWS_SECRET_ACCESS_KEY
  • DOCKER_REPOS
  • SECRET_KEY_BASE

続きを読む

OpsWorks管理下のAmazon LinuxをCLIでアップグレードする

はじめに

何かにつけ、GUIが苦手です。WEBブラウザを開くと、トンデモなバナーをクリックしてしまうし、そもそもトンデモなバナーが出来てきてしまうネットワーク広告が怖い。
OpsWorks(AWSマネジメントコンソール)では、広告は表示されないのですが、いつか広告モデルになるかもしれない…
そんな不安から日々のオペレーションは、awscliを用いたCLIで完結したいというのがモチベーションです。
今回は、OpsWorksで唯一GUIなオペレーション(awscliにサブコマンドが用意されていない)となるUpgrade Operating SystemをCLIで実施するコネタを紹介します。

バージョン

動作を確認している環境とそのバージョンは、以下の通りです。

  • OpsWorks(リモート環境)

    • Cehf 11.10
    • Berkshelf 3.2.0
    • Amazon Linux 2016.03 → 2016.09
  • Mac(ローカル環境)
    • aws-cli 1.11.28 Python/2.7.10 Darwin/15.6.0 botocore/1.4.85

Custom JSONの準備

下ごしらえとして、OpsWorksに渡すattributeをCustom JSONとしてJSONフォーマットのファイルにまとめておきます。
OpsWorksのビルドインcookbookであるdependenciesのattributeを上書きします。詳細は、後述します。

$ mkdir ./aws/opsworks && vi $_/upgrade-amazonlinux.json

{
  "dependencies": {
    "os_release_version": "2016.09",
    "allow_reboot": false,
    "upgrade_debs": true
  }
}

コマンド

CLIでAmazon Linuxをアップグレードしていきます。

幸せなchefになるために、独自の味付けをしない、つまり可能な限りビルドインのcookbookを利用するべきです。
Upgrade Operating Systemで実施しているのは、update_dependenciesというdeployになり、dependencies::updateレシピを実行しています。該当のレシピを一読すると、yum -y updateコマンドを実行していました。前述のCustom JSONは、yum -y updateするために必要なattributeとなります。

  • os_release_versionは、アップグレードしたいAmazon Linuxのバージョンを指定します。
  • allow_reboot は、パッケージのアップデートの後に再起動するか指定します。今回は、インスタンスの停止を明示的に実施しますので、falseとしておきます。
  • upgrade_debsは、一見Debianぽいですがパッケージをアップデートするか否かのフラグとして実装されてます。今回は、アップデートするのでtrueとしておきます。

Upgrade Operating Systemの正体を把握できたので、awscliで以下のような一連コマンドを実行していきます。

# 1. Stackのリビジョンを指定
$ aws opsworks --region us-east-1 update-stack --stack-id STACK_ID --custom-cookbooks-source "{"Revision":"UgradeAmazonLinux"}"

# 2. Stackで管理している全EC2インスタンスに対して、update_custom_cookbooksの実行(最新版cookbookを配置)
$ aws opsworks --region us-east-1 create-deployment --stack-id STACK_ID --command "{"Name":"update_custom_cookbooks"}"

# 3. opsworks agentのバージョンアップ(最新版を利用する)
$ aws opsworks --region us-east-1 update-stack --stack-id STACK_ID --agent-version LATEST

# 4. Custom JSONとレシピを指定して、全パッケージをアップデート
$ aws opsworks --region us-east-1 create-deployment --stack-id STACK_ID --instance-ids INSTANCE_ID01 INSTANCE_ID02 --command "{"Name":"execute_recipes","Args":{"recipes":["dependencies::update"]}}" --custom-json file://./aws/opsworks/upgrade-amazonlinux.json

# 5. EC2インスタンスの停止
$ aws opsworks --region us-east-1 stop-instance --instance-id INSTANCE_ID01
$ aws opsworks --region us-east-1 stop-instance --instance-id INSTANCE_ID02

# 6. OpsWorksで保持しているOSのバージョン情報を更新
$ aws opsworks --region us-east-1 update-instance --instance-id INSTANCE_ID01 --os "Amazon Linux 2016.09"

# 7. EC2インスタンスの起動
$ aws opsworks --region us-east-1 start-instance --instance-id INSTANCE_ID01
$ aws opsworks --region us-east-1 start-instance --instance-id INSTANCE_ID02

4でビルドインのcookbookにCustom JSONでattributeを渡し、全パッケージのアップデートを実施します。
5でEC2インスタンスを停止するのは、以下の2つの理由があります。

  • OpsWorksが保持しているEC2インスタンスの情報を更新するためには、該当のEC2インスタンスを停止する必要がある
  • OSアップグレード後は、setupライフサイクルイベントを実施することを推奨されている

setup ライフサイクルイベントは、7の起動時に実行されます。

おわりに

AWSが広告モデルになったら嫌ですね。
Enjoy CLI!

参考

続きを読む

【AWS Lambda】Amazon Linux の Docker イメージを使ってデプロイパッケージを作成する

AWS Lambda の実行環境に無いライブラリを利用する場合には、ローカル環境でライブラリのソース群をダウンロードしておいてそれをデプロイパッケージに含めて Lambda にアップロードする必要があります。

Python の例だとデプロイパッケージを作る際に例えば下記のような手順を踏みます。

$ virtualenv -p python2.7 venv
$ source venv/bin/activate
$ pip install -r requirements.txt
$ zip deploy_package.zip lambda_function.py # 実行スクリプトを zip にする
$ cd venv/lib/python2.7/site-packages
$ zip -r ../../../../deploy_package.zip * # pip でインストールしたライブラリを zip に追加

利用したいライブラリが純粋な Python コードであればこのパッケージは問題無く Lambda 上でも動作するのですが、OS の機能に依存しているようなライブラリの場合、ローカル環境でビルド・インストールされたものを Lambda 上にアップロードしても動きません。
上記の例で言うと pip install は Lambda の実行環境と同じ環境で行う必要があります。

失敗例

pycrypto というライブラリを使う Lambda Function を作って実行します。

requirements.txt
pycrypto==2.6.1
lambda_function.py
from Crypto.PublicKey import RSA


def lambda_handler(event, context):
    return {"messagge": "success"}

上記のファイルを用意してローカルの OS X 環境でデプロイパッケージを作成します。

$ virtualenv -p python2.7 venv
$ source venv/bin/activate
$ pip install -r requirements.txt
$ zip deploy_package.zip lambda_function.py
$ cd venv/lib/python2.7/site-packages
$ zip -r ../../../../deploy_package.zip *

作成した deploy_package.zip を AWS Lambda にアップロードして実行すると次のようなエラーが出ます。
Unable to import module 'lambda_function'

Unable to import module 'lambda_function'

エラーログ全文を見ると Unable to import module 'lambda_function': /var/task/Crypto/Util/_counter.so: invalid ELF header とあります。pycrypto が参照するファイルのヘッダ情報が不正であるようです。原因は pycrypto をインストールした環境が Lambda の実行環境と異なるところにあります。

対策

Amazon Linux の Docker イメージを用いてライブラリのインストールを行うことでこの問題を回避することができます。

先述のファイルと同じ階層にこのような Dockerfile を用意して

Dockerfile
FROM amazonlinux:latest

RUN yum -y update && yum -y install gcc python27-devel
RUN cd /tmp && 
    curl https://bootstrap.pypa.io/get-pip.py | python2.7 - && 
    pip install virtualenv
WORKDIR /app
CMD virtualenv -p python2.7 venv-for-deployment && 
    source venv-for-deployment/bin/activate && 
    pip install -r requirements.txt

このようにコマンドを実行すると venv-for-deployment という名前で、Amazon Linux でビルドされた Python ライブラリコードが作成されます。

$ docker build . -t packager
$ docker run --rm -it -v $(PWD):/app packager

その後は下記のようにデプロイパッケージの zip を作成して AWS Lambda にアップロードします。

$ zip deploy_package.zip lambda_function.py
$ cd venv-for-deployment/lib/python2.7/site-packages
$ zip -r ../../../../deploy_package.zip * .* # dotfile が含まれる場合は ".*" も

実行するとライブラリが import 出来て無事 "success" が表示されます。

success

自動化

ちょっと叩くコマンド量が多いのでこのような Makefile を作っておくと make だけで zip が生成されて便利です。

Makefile
package:
    docker build . -t packager
    docker run --rm -it -v $(PWD):/app packager
    zip deploy_package.zip lambda_function.py
    cd venv-for-deployment/lib/python2.7/site-packages && zip -r ../../../../deploy_package.zip * .*
    echo "Completed. Please upload deploy_package.zip to AWS Lambda"

サンプル

今回用いた Lambda Function のサンプルはこちらのリポジトリに置いています。
https://github.com/morishin/python-lambda-function-test

所感

AWS Lambda 便利なのですがちょっと凝ったことをしようとすると泣きそうになりますね。

続きを読む

AWS ECSでDockerコンテナ管理入門(基本的な使い方、Blue/Green Deployment、AutoScalingなどいろいろ試してみた)

はじめに

Dockerを本番環境で利用するに当たり、私レベルではDockerのクラスタを管理することはなかなか難しい訳です。凄くめんどくさそうだし。
ということでAWS ECS(EC2 Container Service)ですよ。

記事書くまでも無いかなと思ったんですけど意外と手順がWEBにない(気がしました)。ということで、今回は社内でハンズオンでもやろうかと思って細かく書いてみました。

こんな感じのシナリオをやってみたいと思います。

  1. Dockerのイメージを用意する
  2. ECSの使い方の基本
  3. コンテナのリリース
  4. Blue/Green Deployment
  5. AutoScaling/ScaleIn

前準備:Dockerのイメージを用意する

FROM uzresk/docker-oracle-jdk
MAINTAINER uzresk

ADD demo-1.0.jar /root/demo-1.0.jar
CMD java -jar /root/demo-1.0.jar
  • Docker build
[root@centos7 build_demo_ver1.0]# docker build -t uzresk/demo:ver1.0 /vagrant/build_demo_ver1.0
Sending build context to Docker daemon 33.11 MB
Step 1 : FROM uzresk/docker-oracle-jdk
 ---> df2f575c2a0d
Step 2 : MAINTAINER uzresk
 ---> Using cache
 ---> 1995a4e99748
Step 3 : ADD demo-1.0.jar /root/demo-1.0.jar
 ---> 705df0209779
Removing intermediate container cd9ef33d8812
Step 4 : CMD java -jar /root/demo-1.0.jar
 ---> Running in b7bd939a8b5a
 ---> add0783a851f
Removing intermediate container b7bd939a8b5a
Successfully built add0783a851f
  • 起動します

    • アプリケーションは8080で起動しているのでポートフォワードしておきます。
[root@centos7 build_demo_ver1.0]# docker run -itd -p 80:8080 --name demo uzresk/demo:ver1.0
92bda2419bf7285d78f12be5877ae3242b5b13ac14409b3c47d38e2d74a06464
  • ブラウザでこんな画面がでれば成功です。

image

  • Dockerhubにコミットしてpushしておきます。
[root@centos7 build_demo_ver1.0]# docker commit -m "update ver1.0" demo uzresk/demo:ver1.0
[root@centos7 build_demo_ver1.0]# docker push uzresk/demo:ver1.0

ECSの使い方の基本

AWS ECSとはなんなのか?

  • 今回は利用手順について書こうと思うので割愛しますが、AWS Black Belt ECSを読むのがよろしいかと思います。

構成する順番を抑えよう

  • こんな感じの順番で構成していきます。大事なので押さえておきましょう。
  1. クラスタの作成

    • クラスタを動かすためのEC2インスタンスの設定を行います。具体的にはインスタンスタイプ、インスタンス数、VPC、サブネットの設定になります。
  2. タスク定義

    • クラスタ上で動かすコンテナの情報を登録します。コンテナイメージのURLやCPU、メモリのハード/ソフト制限、アプリケーションで利用する環境変数の定義などを行います。
  3. ロードバランサの作成

    • クラスタの上位に位置するロードバランサの設定を行います。スケールアウトやスケールインしてもロードバランサはサービスを見つけ出し配下に組み込むことができます。
  4. サービスの作成

    • クラスタとサービスを結びつけるのがサービスの役割です。タスクの最少数やAutoScalingの設定を行ったりできます。
    • 1つのクラスタに複数サービスを登録することももちろん可能です。

それではさっそくクラスタの作成からやってみましょう。

クラスタの作成

image

image

  • 正常に作成されると「クラスターの表示」ボタンが押せるようになります。

image

タスク定義

  • 次はタスクの定義です。タスクでは

image

  • タスク定義名を入力し、「コンテナの追加」をクリックします。

image

image

  • 作成を押せばタスク定義の作成が完了します。

ELBの作成

  • ELBは以下の設定で作っておきましょう

    • ELB名:app-demo-lb
    • 種類:アプリケーションロードバランサ
    • 2つのAZそれぞれのSubnetを指定
    • セキュリティグループで80/HTTPを通すように設定
  • ターゲットグループは以下のようにクラスタで設定したインスタンスIDをそれぞれ登録してください。

image

サービスの作成

  • クラスターのTOPからdemo-clusterを選択し、サービスタブで「作成」

image

  • タスク定義とクラスタ名は自動で埋まりますので、サービス名とタスクの数を設定します。
  • 今回はAZにそれぞれコンテナを作りたいので2としました。

image

  • 画面の下の方にあるELBの追加を選択します。

image

  • ELB名は作成したもの、リスナーポートは80、ターゲットグループ名は作成したものを選択します。

image

image

  • 「作成」を押して、サービスの画面をみるとPENDINGになっています。

image

  • 少し経つとRUNNINGになっている事が確認できると思います。

image

  • ELBのエンドポイント/app/をブラウザで叩くと画面が表示されるはずです。

image

コンテナを落としてみるとどうなるのか

  • タスクの一覧から、タスクを一つ消してみましょう。

image

  • 数十秒後に見てみると別のタスクIDのインスタンスが表示されているはずです。

image

  • コンテナが起動する数十秒間の間はアプリケーションロードバランサが生きているタスクの方にうまくルーティングしてくれるのかな?と思ったら「502BadGateway」というエラーが画面に返ってきました。
  • ここはALBのヘルスチェックの閾値を短くすることである程度は短くできそうです。
  • ここをさらに短くするには、コンテナ自体を軽くすることと、すぐに起動できるアプリケーションを利用するしかなさそうですね。

コンテナのリリース

  • 新しいコンテナをリリースするには、タスク定義に新しいリビジョンを登録し、サービスを更新することで実現可能です。さっそくやってみましょう。

image

  • コンテナのバージョンを2.0にして、新しいリビジョンを登録します。

image

  • 追加されたリビジョンを選択し、アクション→サービスの更新を押します。

image

  • タスク定義に新しいリビジョンが指定されていることを確認して、「サービスの更新」

image

  • サービスの「デプロイ」タブを覗くと、今はVer1.0が2つ動いていることが確認できます。

image

  • コンテナを一つ落としてみましょう

image

image

  • 実行中の数がそれぞれ1になり、タスクの一覧からもそれぞれが動いていることがわかりますね。
    image

image


Blue/Green Deployment

  • Blue/GreenDeploymentでは新しいリビジョンのアプリ用に、新しいインスタンスを構築して入れ替える必要があります。
  • この為のパラメータがサービスのデプロイメントオプションにある最大率(maximumPercent)です。2台の時にこの値を200%にしておくと、4台まで同時に動かしておくことができることを意味します。
  • 4台のインスタンス上で動かすにはECSのインスタンス台数を事前に追加しておくか、AutoScalingさせておく必要があります。もしECSインスタンスが2台の状態で4つのコンテナを動かそうとすると以下のようなメッセージがでてしまいます。(ポートかぶってるから上がらないよ。ってことですね)

  • さっそくやってみます

service demo-service was unable to place a task because no container instance met all of its requirements. The closest matching container-instance xxxxxxxxxxxxxxxxxxxx is already using a port required by your task. For more information, see the Troubleshooting section.

image

image

  • この状態でサービスの更新画面でタスク定義を新しいリビジョンに指定して「サービスの更新」を押してみます。
  • おお。4台分のコンテナが起動しましたね。

image

  • ちょっと経つと(3分ほど?)、古いタスクから削除されていきます・・・・

image

  • 最期は新しいタスク定義しか残らなくなりました。自動ですよ。自動。便利ですねー。

image


AutoScaling/ScaleIn

  • 次はオートスケールとスケールインを試してみます。
  • 通常のオートスケールではインスタンスだけでしたが、インスタンス上で動くコンテナもスケールする必要があります。
  • 今回は2つのインスタンスで2つのコンテナで動いていたものを、負荷をかけることにより4つのインスタンス上に4つのコンテナにスケールアウトさせて、スケールインさせたいと思います。

サービスのAutoScaling設定

  • タスクの最大数を4にして、スケーリングポリシーの追加を押します。

image

  • スケールアウトポリシーの設定を行います。
  • CPU使用率の1分間の平均が20%超えた場合2タスクスケールアウトする設定にしました。

image

  • スケールインポリシーの設定を行います。

image

  • ポリシーが追加できたら「保存」を押しましょう

image

  • ポリシーが追加されていることを確認して、「サービスの更新」を押します。

image

  • これでサービスの設定はおしまいです。

ClusterのAutoScaling/ScaleInの設定

  • ECSインスタンスのオートスケールのポリシーは、EC2インスタンスのAutoScalingGroupの設定で行います。
  • 最大数を4にします。

image

  • Scaleout/ScaleInのポリシーを設定します。
  • サービスの設定と同じく、クラスタのCPU使用率が20%以上だと2台スケールアウトするように設定しました。

image

うごかしてみる

  • ECSインスタンス上でCPU使用率を強引に(openssl speed -multi 1)あげてみたのですがうまく動きませんでした。
  • ありがちですけどabで負荷をかけてみます。
  • abをインストール
sudo yum install -y httpd24-tools
  • 負荷をかける
ab -n 100000 -c 100 http://localhost/app/loginForm
  • CloudWatch Alerm(CPUが20%以上)があがる

image

  • サービスの必要数が4に変更される

image

  • インスタンスがオートスケールする

image

  • タスクが起動する

image

  • 負荷を解除する

  • CloudWatch Alerm(CPUが20%より小さい)があがる

image

  • 必要数が2に変更される

image

  • コンテナとインスタンスが2ずつに変わる

  • ここまでやって思ったんですが、インスタンス→コンテナの順に起動されたんですからコンテナ→インスタンスの順に落としていった方がよさそうですね。それはスケーリングポリシーのクールダウン時間で調整ができそうです。

ECSインスタンスの起動を待つのか?

  • ECSのインスタンスの起動を行った後にコンテナが起動されるので結局時間が掛かってしまいます。負荷の時間が予測されるのであればECSインスタンスを事前に起動しておいた方がECSのスケールアウトが高速にできそうです。
  • Dockerのメリットの一つである起動の高速化のメリットを享受するにはこのスケジューリングがキーになりそうですね。

どのインスタンスがスケールインするのか?


さいごに

ここまでコンテナ管理のハードルが下がると、今後はアプリケーションをコンテナにして配布するのが普通になってくると思います。
そうなるときのためにDockerについてしっかりと理解し、良い設計を心がけたいものですね

続きを読む

AWS認定ソリューションアーキテクト – プロフェッショナルレベルにリベンジして合格した

2015年8月に受験して不合格だったのですが、このたびAWS認定ソリューションアーキテクト – プロフェッショナルレベルに合格しました!!
やったあ、嬉しい。

総合スコアは71%で際どいところでまだまだ勉強不足感はありますが、前回の受験と比べて変えたところを記録しておきます。

試験勉強

模擬試験

サンプル問題と模擬試験を中心にして勉強を進めました。
サンプル問題は解説記事があるのでそれを参考にしました。

模擬試験は、以下の記事を参考にして各問題がどの分野に属しているのか、各分野でどれくらい誤答があるのかを調べて分析しました。
AWS 認定ソリューションアーキテクト プロフェッショナルに合格したので対策と勉強方を公開 – YOMON8.NET
分類は完璧にはできないですが、弱い分野は誤答数が多く重点的に見る指針になります。

また、各問題でわからない部分はBlackBeltみたり、WhitePaperよんだりして調べていきました。

ちなみに、これでいけると思って試験2ヶ月ほど前に2度目の模擬試験を受けたのですが、1度目と問題が同じにも関わらずギリギリな感じで不合格でした^^;
弱い分野を重点的に見てみると結構思い込みをしてた部分がわかって良かったです。
ただ、自分側の問題だけではなく、模擬試験では日本語訳がまずいところなど本試験の問題に比べてクオリティが低い部分はある気がします。

参考になるかなと思ってDevOpsの模擬試験も受けてみたもののこちらは分析する時間がありませんでした。
やっていればDeployment Managementの分野の点数上げられたかな?

勉強時間

試験勉強の時間は家では子育てで全く確保できず、職場で30分〜1時間ほど地道に進めていきました。
本業務などが忙しかったり他のこと勉強したりとその30分も確保できないことも多かったです。
試験直前の1-2ヶ月もほとんど時間は確保できていなかったですが、模擬試験の分析は終わらせていたのでなんとかなりました。

試験当日

集中力維持のために試験30分前にレッドブル飲みました。
3時間の長丁場では後半集中力が切れてしまいますがちょっとはマシだったかな。

今回は1問ずつ見直さなくてもいいくらいじっくり解いていくスタイルにしてみました。
最後の問題を解いた時点で試験終了数分前の状態でした。
残り1時間くらいで焦ってしまったりしましたが、結果的には良かったです。

やはり難しかったですが、模擬試験解いていたおかげで全く手が出ない感じはしなかったです。

続きを読む

[JAWS-UG CLI] API Gateway: #4 RestAPIの作成(LambdaMicroservice)

  1. API Gateway

前提条件

API Gatewayへの権限

API Gatewayに対してフル権限があること。

AWS CLI

以下のバージョンで動作確認済

  • AWS CLI 1.11.8
コマンド
aws --version

結果(例):

  aws-cli/1.11.34 Python/2.7.10 Darwin/15.6.0 botocore/1.4.91

バージョンが古い場合は最新版に更新しましょう。

コマンド
sudo -H pip install -U awscli

0. 準備

0.1. リージョンの決定

変数の設定
export AWS_DEFAULT_REGION='ap-northeast-1'

0.2. プロファイルの確認

プロファイルが想定のものになっていることを確認します。

変数の確認
aws configure list

結果(例):

        Name                    Value             Type    Location
        ----                    -----             ----    --------
     profile       lambdaFull-prjz-mbp13        env    AWS_DEFAULT_PROFILE
  access_key     ****************XXXX shared-credentials-file
  secret_key     ****************XXXX shared-credentials-file
      region        ap-northeast-1        env    AWS_DEFAULT_REGION

0.3. Lambda関数のARN取得

変数の設定
LAMBDA_FUNC_NAME='microservice_http_endpoint-20170102'
変数の設定
LAMBDA_FUNC_ARN=$( \
        aws lambda get-function \
          --function-name ${LAMBDA_FUNC_NAME} \
          --query 'Configuration.FunctionArn' \
          --output text \
) \
        && echo ${LAMBDA_FUNC_ARN}

1. 事前作業

API名の指定

変数の設定
APIGW_API_NAME='LambdaMicroservice'

同名のREST APIが存在しないことを確認します。

コマンド
aws apigateway get-rest-apis \
        --query "items[?name == \`${APIGW_API_NAME}\`]"

結果(例):

  []

2. APIの作成

2.1. APIの作成

APIを作成するときは、説明も必ず入れるようにしましょう。

変数の設定
APIGW_API_DESC='This is my API for demonstration purposes'
変数の確認
cat << ETX

        APIGW_API_NAME:  ${APIGW_API_NAME}
        APIGW_API_DESC: "${APIGW_API_DESC}"

ETX
コマンド
aws apigateway create-rest-api \
        --name ${APIGW_API_NAME} \
        --description "${APIGW_API_DESC}"

結果(例):

  {
    "id": "xxxxxxxxxx",
    "name": "LambdaMicroservice",
    "description": "This is my API for demonstration purposes",
    "createdDate": 1448002904
  }

2.2. APIの確認

コマンド
aws apigateway get-rest-apis \
        --query "items[?name == \`${APIGW_API_NAME}\`]"

結果(例):

  {
    "id": "xxxxxxxxxx",
    "name": "LambdaMicroservice",
    "description": "This is my API for demonstration purposes",
    "createdDate": 1448002904
  }
変数の設定
APIGW_API_ID=$( \
        aws apigateway get-rest-apis \
          --query "items[?name == \`${APIGW_API_NAME}\`].id" \
          --output text \
) \
        && echo ${APIGW_API_ID}

結果(例):

  xxxxxxxxxx
コマンド
aws apigateway get-rest-api \
        --rest-api-id ${APIGW_API_ID}

結果(例):

  {
    "id": "xxxxxxxxxx",
    "name": "LambdaMicroservice",
    "description": "This is my API for demonstration purposes",
    "createdDate": 1448002904
  }

2.3. アカウントの確認

コマンド
aws apigateway get-account

結果(例):

  {
    "throttleSettings": {
      "rateLimit": 500.0,
      "burstLimit": 1000
    }
  }

2.4. モデルの確認

コマンド
aws apigateway get-models \
        --rest-api-id ${APIGW_API_ID}

結果(例):

  {
    "items": [
      {
          "description": "This is a default empty schema model",
          "schema": "{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\" : \"Empty Schema\",\n  \"type\" : \"object\"\n}",
          "contentType": "application/json",
          "id": "ctcxxf",
          "name": "Empty"
      },
      {
          "description": "This is a default error schema model",
          "schema": "{\n  \"$schema\" : \"http://json-schema.org/draft-04/schema#\",\n  \"title\" : \"Error Schema\",\n  \"type\" : \"object\",\n  \"properties\" : {\n    \"message\" : { \"type\" : \"string\" }\n  }\n}",
          "contentType": "application/json",
          "id": "fpdged",
          "name": "Error"
      }
    ]
  }

3. リソースの作成

3.1. 現在のリソースの確認

コマンド
aws apigateway get-resources \
        --rest-api-id ${APIGW_API_ID}

結果(例):

  {
    "items": [
      {
          "path": "/",
          "id": "xxxxxxxxxx"
      }
    ]
  }

3.2. 現在のリソースのリソースID取得

変数の設定
APIGW_RESOURCE_PATH="/"
変数の設定
APIGW_RESOURCE_ID=$( \
        aws apigateway get-resources \
          --rest-api-id ${APIGW_API_ID} \
          --query "items[?path == \`${APIGW_RESOURCE_PATH}\`].id" \
          --output text \
) \
        && echo ${APIGW_RESOURCE_ID}

結果(例):

  xxxxxxxxxx
コマンド
aws apigateway get-resource \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID}

結果(例):

  {
    "path": "/",
    "id": "xxxxxxxxxx"
  }

3.3. 新しいリソースの作成

変数の設定
APIGW_PARENT_ID="${APIGW_RESOURCE_ID}"
APIGW_PATH_PART='microservice-http-endpoint-python'
変数の確認
cat << ETX

        APIGW_API_ID:    ${APIGW_API_ID}
        APIGW_PARENT_ID: ${APIGW_PARENT_ID}
        APIGW_PATH_PART: ${APIGW_PATH_PART}

ETX
コマンド
aws apigateway create-resource \
        --rest-api-id ${APIGW_API_ID} \
        --parent-id ${APIGW_PARENT_ID} \
        --path-part ${APIGW_PATH_PART}

結果(例):

  {
    "path": "/microservice-http-endpoint-python",
    "pathPart": "microservice-http-endpoint-python",
    "id": "xxxxxx",
    "parentId": "xxxxxxxxxx"
  }

3.4. 新しいリソースのリソースID取得

変数の設定
APIGW_RESOURCE_PATH="${APIGW_RESOURCE_PATH}${APIGW_PATH_PART}" \
        && echo ${APIGW_RESOURCE_PATH}
変数の設定
APIGW_RESOURCE_ID=$( \
        aws apigateway get-resources \
          --rest-api-id ${APIGW_API_ID} \
          --query "items[?path == \`${APIGW_RESOURCE_PATH}\`].id" \
          --output text \
) \
        && echo ${APIGW_RESOURCE_ID}

結果(例):

  xxxxxx

3.5. 新しいリソースの内容確認

コマンド
aws apigateway get-resource \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID}

結果(例):

  {
    "path": "/microservice-http-endpoint-python",
    "pathPart": "microservice-http-endpoint-python",
    "id": "xxxxxx",
    "parentId": "xxxxxxxxxx"
  }

4. Lambda関数の指定

変数の確認
cat << ETX

        AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION}
        LAMBDA_FUNC_ARN:    ${LAMBDA_FUNC_ARN}

ETX
変数の設定
APIGW_INTEG_URI="arn:aws:apigateway:${AWS_DEFAULT_REGION}:lambda:path/2015-03-31/functions/${LAMBDA_FUNC_ARN}/invocations" \
        && echo ${APIGW_INTEG_URI}

結果(例):

  arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:microservice_http_endpoint-20170102 /invocations

5. ANYメソッドの作成

5.1. methodの作成

変数の設定
HTTP_METHOD='ANY'
AUTH_TYPE='NONE'
変数の確認
cat << ETX

        APIGW_API_ID:      ${APIGW_API_ID}
        APIGW_RESOURCE_ID: ${APIGW_RESOURCE_ID}
        HTTP_METHOD:       ${HTTP_METHOD}
        AUTH_TYPE:         ${AUTH_TYPE}

ETX
コマンド
aws apigateway put-method \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD} \
        --authorization-type ${AUTH_TYPE}

結果(例):

  {
    "apiKeyRequired": false,
    "httpMethod": "ANY",
    "authorizationType": "NONE"
  }
コマンド
aws apigateway get-method \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD}

結果(例):

  {
    "apiKeyRequired": false,
    "httpMethod": "ANY",
    "authorizationType": "NONE"
  }

5.2. integrationの作成

コマンド
aws apigateway get-integration \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD}

結果(例)

  A client error (NotFoundException) occurred when calling the GetIntegration operation: No integration defined for method
変数の設定
APIGW_INTEG_TYPE='AWS_PROXY'
変数の設定
APIGW_INTEG_METHOD='POST'
変数の確認
cat << ETX

        APIGW_API_ID:       ${APIGW_API_ID}
        APIGW_RESOURCE_ID:  ${APIGW_RESOURCE_ID}
        HTTP_METHOD:        ${HTTP_METHOD}
        APIGW_INTEG_TYPE:   ${APIGW_INTEG_TYPE}
        APIGW_INTEG_METHOD: ${APIGW_INTEG_METHOD}
        APIGW_INTEG_URI:    ${APIGW_INTEG_URI}

ETX
コマンド
aws apigateway put-integration \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD} \
        --type ${APIGW_INTEG_TYPE} \
        --integration-http-method ${APIGW_INTEG_METHOD} \
        --uri ${APIGW_INTEG_URI}

結果(例):

  {
    "httpMethod": "POST",
    "passthroughBehavior": "WHEN_NO_MATCH",
    "cacheKeyParameters": [],
    "type": "AWS_PROXY",
    "uri": "arn:aws:apigateway::lambda:path/2015-03-31/functions/arn:aws:lambda::XXXXXXXXXXXX:function:\ |LAMBDA_FUNC_NAME|\ /invocations",
    "cacheNamespace": "2t9qak"
  }
コマンド
aws apigateway get-integration \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD}

結果(例):

  {
    "httpMethod": "POST",
    "passthroughBehavior": "WHEN_NO_MATCH",
    "cacheKeyParameters": [],
    "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:microservice_http_endpoint-20170102/invocations",
    "cacheNamespace": "88a637",
    "type": "AWS_PROXY"
  }

5.3. Integration Responseの作成

GETメソッドのリザルトコード200でのIntegration Responseが存在作成しない
ことを確認します。

変数の設定
HTTP_STATUS_CODE='200'
コマンド
aws apigateway get-integration-response \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD} \
        --status-code ${HTTP_STATUS_CODE}

結果:

  A client error (NotFoundException) occurred when calling the GetMethodResponse operation: Invalid Response status code specified

GETメソッドのリザルトコード200でのIntegration Responseを作成します。

変数の確認
cat << ETX

        APIGW_API_ID:       ${APIGW_API_ID}
        APIGW_RESOURCE_ID:  ${APIGW_RESOURCE_ID}
        HTTP_METHOD:        ${HTTP_METHOD}
        HTTP_STATUS_CODE:   ${HTTP_STATUS_CODE}

ETX
コマンド
aws apigateway put-integration-response \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD} \
        --status-code ${HTTP_STATUS_CODE} \
        --selection-pattern '.*'

結果(例):

  {
    "selectionPattern": ".*",
    "statusCode": "200"
  }

GETメソッドのリザルトコード200でのIntegration Responseを確認します。

コマンド
aws apigateway get-integration-response \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD} \
        --status-code ${HTTP_STATUS_CODE}

結果(例):

  {
    "selectionPattern": ".*",
    "statusCode": "200"
  }

GETメソッドのIntegrationにリザルトコード200でのIntegration Responseが
存在することを確認します。

コマンド
aws apigateway get-integration \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD}

結果(例):

  {
    "integrationResponses": {
      "200": {
          "selectionPattern": ".*",
          "statusCode": "200"
      }
    },
    "passthroughBehavior": "WHEN_NO_MATCH",
    "cacheKeyParameters": [],
    "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:microservice_http_endpoint-20161231/invocations",
    "httpMethod": "POST",
    "cacheNamespace": "2t9qak",
    "type": "AWS_PROXY"
  }

5.4. lambda関数の実行権限付与

GETメソッドのリザルトコード200でのIntegration Responseに、Lambda関数を
実行する権限を付与します。

Statement IDを乱数32桁で生成します。 (もっとスマートな方法があれば教え
てください…)

変数の設定
LAMBDA_STAT_ID=$(od -vAn -N16 -tx < /dev/urandom |sed 's/ //g')
変数の設定
LAMBDA_ACTION='lambda:InvokeFunction'
AWS_PRINCIPAL='apigateway.amazonaws.com'
RESOURCE_PATH="${APIGW_RESOURCE_PATH}"
変数の確認
cat << ETX

        AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION}
        AWS_ID:             ${AWS_ID}
        APIGW_API_ID:       ${APIGW_API_ID}
        HTTP_METHOD:        ${HTTP_METHOD}
        RESOURCE_PATH:      ${RESOURCE_PATH}

ETX
変数の設定
SOURCE_ARN="arn:aws:execute-api:${AWS_DEFAULT_REGION}:${AWS_ID}:${APIGW_API_ID}/*/*${RESOURCE_PATH}" \
        && echo ${SOURCE_ARN}
変数の確認
cat << ETX

        LAMBDA_FUNC_NAME:  ${LAMBDA_FUNC_NAME}
        LAMBDA_STAT_ID:    ${LAMBDA_STAT_ID}
        LAMBDA_ACTION:     ${LAMBDA_ACTION}
        AWS_PRINCIPAL:     ${AWS_PRINCIPAL}
        SOURCE_ARN:        ${SOURCE_ARN}

ETX
コマンド
aws lambda add-permission \
         --function-name ${LAMBDA_FUNC_NAME} \
         --statement-id ${LAMBDA_STAT_ID} \
         --action ${LAMBDA_ACTION} \
         --principal ${AWS_PRINCIPAL} \
         --source-arn ${SOURCE_ARN}

結果(例):

  {
    "Statement": "{"Sid":"0230237a9b696476bde295c8bf436f99","Resource":"arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:microservice_http_endpoint-20161231","Effect":"Allow","Principal":{"Service":"apigateway.amazonaws.com"},"Action":["lambda:InvokeFunction"],"Condition":{"ArnLike":{"AWS:SourceArn":"arn:aws:execute-api:ap-northeast-1:XXXXXXXXXXXX:s2ff0lq6l9/*/*/microservice-http-endpoint-python"}}}"
  }
コマンド
aws lambda get-policy \
        --function-name ${LAMBDA_FUNC_NAME} \
      | sed 's/\\//g' | sed 's/\"{/{/' | sed 's/\"$//' \
      | jp.py "Policy.Statement[?Sid == \`${LAMBDA_STAT_ID}\`]"

結果(例)

  [
    {
      "Resource": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:microservice_http_endpoint-20170102",
      "Effect": "Allow",
      "Sid": "0230237a9b696476bde295c8bf436f99",
      "Action": "lambda:InvokeFunction",
      "Condition": {
          "ArnLike": {
              "AWS:SourceArn": "arn:aws:execute-api:ap-northeast-1:XXXXXXXXXXXX:xxxxxxxxxx/*/*/microservice-http-endpoint-python"
          }
      },
      "Principal": {
          "Service": "apigateway.amazonaws.com"
      }
    }
  ]

5.5. Method Responseの作成

コマンド
aws apigateway get-method-response \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD} \
        --status-code ${HTTP_STATUS_CODE}

結果:

  An error occurred (NotFoundException) when calling the GetMethodResponse operation: Invalid Response status code specified
変数の確認
cat << ETX

        APIGW_API_ID:      ${APIGW_API_ID}
        APIGW_RESOURCE_ID: ${APIGW_RESOURCE_ID}
        HTTP_METHOD:       ${HTTP_METHOD}
        HTTP_STATUS_CODE:  ${HTTP_STATUS_CODE}

ETX
コマンド
aws apigateway put-method-response \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD} \
        --status-code ${HTTP_STATUS_CODE} \
        --response-models '{}'

結果(例)

  {
    "responseModels": {},
    "statusCode": "200"
  }
コマンド
aws apigateway get-method-response \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD} \
        --status-code ${HTTP_STATUS_CODE}

結果(例):

  {
    "responseModels": {},
    "statusCode": "200"
  }
コマンド
aws apigateway get-method \
        --rest-api-id ${APIGW_API_ID} \
        --resource-id ${APIGW_RESOURCE_ID} \
        --http-method ${HTTP_METHOD}

結果(例):

  {
    "apiKeyRequired": false,
    "httpMethod": "ANY",
    "methodIntegration": {
      "integrationResponses": {
          "200": {
              "selectionPattern": ".*",
              "statusCode": "200"
          }
      },
      "passthroughBehavior": "WHEN_NO_MATCH",
      "cacheKeyParameters": [],
      "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:microservice-http-endpoint-python/invocations",
      "httpMethod": "POST",
      "cacheNamespace": "xxxxxx",
      "type": "AWS_PROXY"
    },
    "methodResponses": {
      "200": {
          "responseModels": {},
          "statusCode": "200"
      }
    },
    "authorizationType": "NONE"
  }

5.6. メソッドのテスト

(省略)

6. APIのデプロイ

6.1. APIのデプロイ

コマンド
aws apigateway get-deployments \
        --rest-api-id ${APIGW_API_ID}

結果:

  {
    "items": []
  }
変数の設定
APIGW_STAGE_NAME='test'
APIGW_STAGE_DESC='This is a test.'
APIGW_DEPLOY_DESC='Calling Lambda functions MicroserviceEndpoint.'
変数の確認
cat << ETX

        APIGW_API_ID:      ${APIGW_API_ID}
        APIGW_STAGE_NAME:  ${APIGW_STAGE_NAME}
        APIGW_STAGE_DESC:  "${APIGW_STAGE_DESC}"
        APIGW_DEPLOY_DESC: "${APIGW_DEPLOY_DESC}"

ETX
コマンド
aws apigateway create-deployment \
        --rest-api-id ${APIGW_API_ID} \
        --stage-name ${APIGW_STAGE_NAME} \
        --stage-description "${APIGW_STAGE_DESC}" \
        --description "${APIGW_DEPLOY_DESC}"

結果(例):

  {
    "description": "Calling Lambda functions MicroserviceEndpoint.",
    "id": "aody9s",
    "createdDate": 1483327540
  }
コマンド
aws apigateway get-deployments \
        --rest-api-id ${APIGW_API_ID}

結果(例):

  {
    "items": [
      {
          "createdDate": 1483327540,
          "id": "aody9s",
          "description": "Calling Lambda functions MicroserviceEndpoint."
      }
    ]
  }

6.2. API IDの取得

コマンド
APIGW_DEPLOY_ID=$( \
        aws apigateway get-deployments \
          --rest-api-id ${APIGW_API_ID} \
          --query "items[?description == \`${APIGW_DEPLOY_DESC}\`].id" \
          --output text \
) \
        && echo ${APIGW_DEPLOY_ID}

結果(例)

  xxxxxx

6.3. APIの情報の取得

コマンド
aws apigateway get-deployment \
        --rest-api-id ${APIGW_API_ID} \
        --deployment-id ${APIGW_DEPLOY_ID}

結果(例):

  {
    "description": "Calling Lambda functions MicroserviceEndpoint.",
    "id": "aody9s",
    "createdDate": 1483327540
  }

6.4. APIのステージ名の取得

コマンド
APIGW_STAGE_NAME=$( \
        aws apigateway get-stages \
          --rest-api-id ${APIGW_API_ID} \
          --query "item[?description == \`${APIGW_STAGE_DESC}\`].stageName" \
          --output text \
) \
        && echo ${APIGW_STAGE_NAME}

結果(例):

  test
コマンド
aws apigateway get-stage \
        --rest-api-id ${APIGW_API_ID} \
        --stage-name ${APIGW_STAGE_NAME}

結果(例):

  {
    "stageName": "test",
    "description": "This is a test.",
    "cacheClusterEnabled": false,
    "cacheClusterStatus": "NOT_AVAILABLE",
    "deploymentId": "aody9s",
    "lastUpdatedDate": 1483327540,
    "createdDate": 1483327540,
    "methodSettings": {}
  }

7 .APIのテスト

GETメソッドのテスト

変数の設定
PATH_WITH_QUERY_STRING='TableName=microservice-http'
変数の確認
cat << ETX

        APIGW_API_ID:        ${APIGW_API_ID}
        AWS_DEFAULT_REGION:  ${AWS_DEFAULT_REGION}
        APIGW_STAGE_NAME:    ${APIGW_STAGE_NAME}
        APIGW_RESOURCE_PATH: ${APIGW_RESOURCE_PATH}
        PATH_WITH_QUERY_STRING: ${PATH_WITH_QUERY_STRING}

ETX
変数の設定
APIGW_URI="https://${APIGW_API_ID}.execute-api.${AWS_DEFAULT_REGION}.amazonaws.com/${APIGW_STAGE_NAME}${APIGW_RESOURCE_PATH}?${PATH_WITH_QUERY_STRING}" \
        && echo ${APIGW_URI}

表示されたURLをブラウザで開いて、以下のJSONファイルが表示されればOKで
す。

ブラウザ表示の結果:

  {"Count": 1, "Items": [{"Description": "sample", "Name": "taro"}], "ScannedCount": 1, "ResponseMetadata": {"RetryAttempts": 0, "HTTPStatusCode": 200, "RequestId": "EQ6FPKSBTR75S2LEFQ4JPA6407VV4KQNSO5AEMVJF66Q9ASUAABG", "HTTPHeaders": {"x-amzn-requestid": "EQ6FPKSBTR75S2LEFQ4JPA6407VV4KQNSO5AEMVJF66Q9ASUAABG", "content-length": "89", "server": "Server", "connection": "keep-alive", "x-amz-crc32": "2586798783", "date": "Mon, 02 Jan 2017 03:33:26 GMT", "content-type": "application/x-amz-json-1.0"}}}

完了

続きを読む

AWS CodeDeploy を使うときに気をつけたいこと

エムティーアイ Advent Calender 2016 の23日目の記事です。

AWS CodeDeploy (以下、CodeDeploy) は AWS の中の開発者ツール(通称 Code シリーズ)の中の1つで、デプロイ作業をコードで記述することで自動化できるサービスです。
開発者ツールや CodeDeploy 自体の説明は以下などを参考にしてください。

この記事では実際に CodeDeploy を数ヶ月使っていてハマってしまった経験を書いていきます。
どれも経験に基づいた情報であり個人の見解等も入っていますので、正確な情報は公式ドキュメントや、AWS サポート等へご確認ください。

Hooks スクリプトはどんなときでも動くように

Hooks スクリプトにデプロイ時の様々な付随処理を書くことができます。
スクリプトで目的の処理を実行できるようになったら、いろんな条件でデプロイを試してみてエラーが出ないことを確認します。
例えば、まっさらな状態のインスタンスに対してのデプロイ、アプリケーションが稼働中の状態でのデプロイなどです。

僕の場合はとある C# の Web システムで、開発中は正常にデプロイできてたけど本稼働後にデプロイをさせたら失敗するようになってしまいました。
原因は対象インスタンスへの Web リクエストの停止がうまく行えていなかったために、処理中の dll の置き換えができなくなってしまいました。

また、AutoScaling で新たに生成されたインスタンスに対してのデプロイで失敗してしまうこともありました。
特にこの CodeDeploy のターゲットに AutoScaling グループを指定する場合は注意が必要です。
AutoScaling ではインスタンス生成時に CodeDeploy によるデプロイを実施しますが、デプロイに失敗した場合はインスタンスが正常に起動しなかったものとして、インスタンスのを破棄して再生成します。
すると再度失敗して、破棄と生成の無限ループに突入してしまいます。
気づかなければインスタンスがいつまでも InService にならないだけでなく、インスタンスの料金もかかってしまい、目も当てられなくなります・・・。

PowerShell は 32bit 版が動く

Windows での Hooks スクリプトでは直接 PowerShell を実行することはできず、bat ファイルから powershell script.ps1 のように呼び出して使います。
このとき、たとえ OS が 64bit 版だったとしても 32bit 版の PowerShell が実行されてしまいます。
そのため、一部のコマンドレットが使用できません。

これについて困ってる人は結構いるようで、ググるとそれなりに出てきます。
64bit 版の方を実行させるやり方を書いてる人もいましたが、僕はうまく行きませんでした。
仕方なく通常の Windows コマンドや 32bit 版でなんとか回避しましたが、「こうやると 64bit 版が使えたぜ!」っていう情報があったら教えていただけるとうれしいです。

CodeDeploy で配置したファイルはいじってはいけない

AppSpec ファイルの files セクションに書くことでファイル群の配置を行うことができます。
その配置されたファイルに CodeDeploy 以外で変更を加えてしまうと、次のデプロイ時に失敗してしまいます。

CodeDeploy では2回目以降のデプロイの際、 files の destination で指定されたファイルやディレクトリ以下を削除して新しいファイルをコピーしています。
配置したファイルの更新日などのメタデータを見ているようで、デプロイ後にファイルが更新されていると「勝手に消したらまずいんじゃね?」と判断(イメージです)してデプロイを中断します。

ファイルを更新してしまった場合は手動で削除するか、CodeDeploy の作業ディレクトリにある元ファイルをコピーして元に戻すかしてから再デプロイしましょう。
もしくはインスタンスごと作り直してもいいと思います。

インスタンスの時刻がずれてるとデプロイできない

ある日突然、特定のインスタンスだけデプロイに失敗するようになり、しかもマネジメントコンソールで確認するとデプロイライフサイクルにすら入る前にエラーとなっていました。
インスタンスにログインして CodeDeploy エージェントのログを見るとこんなログがありました。

codedeploy-agent-log.txt
2016-11-30 15:46:26 INFO  [codedeploy-agent(2776)]: [Aws::CodeDeployCommand::Client 400 0.062357 0 retries] poll_host_command(host_identifier:"arn:aws:ec2:ap-northeast-1:NNNNNNNNNNNN:instance/i-XXXXXXXX") Aws::CodeDeployCommand::Errors::InvalidSignatureException Signature expired: 20161130T064626Z is now earlier than 20161130T070808Z (20161130T071308Z - 5 min.)

2016-11-30 15:46:26 ERROR [codedeploy-agent(2776)]: InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller: Cannot reach InstanceService: Aws::CodeDeployCommand::Errors::InvalidSignatureException - Signature expired: 20161130T064626Z is now earlier than 20161130T070808Z (20161130T071308Z - 5 min.)

どうやらインスタンスの時刻同期が長いこと失敗し続けていて、インスタンスの時刻が大幅にずれてしまっていたようです。
CodeDeploy ではインスタンスの時刻が5分以上ずれていると、認証に失敗し CodeDeploy エージェントと CodeDeploy との通信が行えません。

ドキュメントにもちゃんと書いてありました。

Troubleshooting “InvalidSignatureException – Signature expired: [time] is now earlier than [time]” deployment errors

AWS CodeDeploy requires accurate time references in order to perform its operations. If your instance’s date and time are not set correctly, they may not match the signature date of your deployment request, which AWS CodeDeploy will therefore reject.

OneAtATime で最後の1台のデプロイ失敗は無視される

インスタンス1台1台のデプロイの成功・失敗とは別に、デプロイ全体(デプロイ ID ごと)の成功・失敗という概念があります。
通常、Deployment config が OneAtATime の場合、対象インスタンスに順次デプロイしていく途中で1台でも失敗するとデプロイが中断され全体として失敗となります。

しかし、最後の1台(4台構成なら4番目にデプロイされるもの)が失敗しても全体として成功と評価されます。
これもドキュメントに書かれているので仕様のようですが、その理由は “1台ずつオフラインになることを前提とした設定だからいいよね” (超意訳。あってるよね?)
・・・そうっすか笑。

The overall deployment succeeds if the application revision is deployed to all of the instances. The exception to this rule is if deployment to the last instance fails, the overall deployment still succeeds. This is because AWS CodeDeploy allows only one instance at a time to be taken offline with the CodeDeployDefault.OneAtATime configuration.

このため、全体の評価が成功になっていたとしても失敗したインスタンスがないか確認したほうがいいかもしれません。

停止しているインスタンスにもデプロイの試行がされる

CodeDeploy のターゲットはインスタンスの任意のタグを指定することができます。
この場合、指定したタグの付いたインスタンスであれば停止しているインスタンスに対してもデプロイの施行が行われてしまいます。
もちろん、停止しているインスタンスにあるエージェントがこの指示を受け取れるわけはないので、しばらくするとタイムアウトで必ず失敗します。

ちなみにマネジメントコンソールで Deployment Group を作成する際にタグを指定するとデプロイ対象のインスタンスが確認できますが、このタイミングで停止しているインスタンスも破棄されたインスタンスもマッチします。
(破棄されたインスタンスへはデプロイの施行はされません)

AWS サポートに問い合わせたところ「現時点ではそういう動作をするようになっているが、ドキュメントへの記載はなく、好ましい動作とは言えないので開発部門へフィードバックする」との旨の返答をいただきました。
(「単純にバグなのでは?」という気もしますが、AWS サポートは丁寧で非常に好感が持てます!)

とりあえず、何かの都合で一部インスタンスを破棄はせず停止しておきたいような場合の対応は、現時点ではインスタンスのタグを一時的に変更するのが手っ取り早くていいと思います。

最後に

AWS のサービスは実際に使ってみて初めて知る仕様や、ちょっとしたノウハウが必要なものが多くあり苦戦することもありますが、それもまた面白いところかもしれません。
公式ドキュメントでも日本語化されていないものがあったり、日本語ドキュメントには載ってなくて英語版にだけあるといったことも多々あって英語力の低い僕は大変です・・・。

CodeDeploy もそれに漏れず、いろいろありました。
とは言え、その恩恵は大きく、デプロイのコード化で Excel 方眼紙のデプロイ手順ともおさらばできますね!
AWS CodeBuild が2016年の Re:Invent で発表され、デリバリもどんどん楽になりそうです。

続きを読む

Terraformで始めるRolling deployment

:santa:この記事は Recruit Engineers Advent Calendar 2016 の22日目の記事です。:santa:

昨日はmookjpさんのLet It Crashとは何かでした!ゴイスー!

本日はサーバサイドエンジニアとして従事している私がAWSでインフラ構築をした記事となります。
この記事を書く1週間程前はそろそろ記事の下書きをしておくかー。
と、やる気に満ち溢れていたのですが、悲しきかな・・・PS4 proが届き当時の気持ちはどこかに置いてきてしまったようで、急いで先程記事を書き終えました。

はじめに

日々やらねばいけないことが満ち溢れている中、運用サーバトラブルに対して時間は割きたくないものです。例えば、EC2の突然死やメンテナンスです。
限られた時間の中でより効率的に時間を使いたいという欲求を皆様お持ちではないでしょうか。
そこで楽をしようと思い立ち、本エントリーの構成にしたのです。

使うツール

細かいものは省略して大枠だけ。

AWSで利用したサービス

細かいものは省略します。

  • ALB
  • AutoScaling
  • EC2
  • CloudWatch Events
  • Lambda
  • SNS

Rolling deployment

デプロイにおいてTerraformでやっていること

結論だけ先に書くと、

[Terraformでやっていること]

  • DataSourceを使って最新版のAMIを取得
  • ASGのLaunchConfigurationを作成
  • ASGの更新

その他は基本Lambdaに任せています。
AMIの作成はDroneを使ってPackerを実行しています。

デプロイフロー

まずはアプリケーションリリースまでのデプロイフローを見てみましょう。

AWS Design.png

あまり複雑にならないよう心がけたつもりで、それぞれのSTEPで見たときにシンプルになれば良いかなと思いこのフローにしています。

ステップごとに見るフロー

1. AMI作成

CIサーバ

弊社ではGitHub Enterpriseを使っているので、今までJenkins2を使ってCIを回していましたが、やはりコンテナベースでテストを回さないとジョブがconflictするわけです。
一々レポジトリごとにDockerfile用意するのも面倒だったので、OSS版のDroneに切り替えました。
Droneを一言で表すなら「最高」。

DroneからAMIをビルドする

PackerとAnsibleを使ってAMIを作成しています。
後述しますが、この時新しいAMIを作るうえで利用するAMIは、予め用意しておいたベースのAMIで、そこから新しいバージョンのシステムをデプロイ & ビルド(npm install / bundle install…)してAMIを作成しています。

2. ASG更新

Terraformを使ってASGを更新する

LaunchConfigurationの更新は行えないため、Terraformを使ってLaunchConfigurationを作成し、既存のASGに紐付けています。

3. スケールアウト

新しいAMIを利用したEC2インスタンスを立ち上げ

ASGの更新をトリガーにCloudWatch EventsからLambdaを起動しています。
このLambdaでは下記を実行しています。

  1. 更新したAutoScalingGroupのDesiredCapacityを取得
  2. setDesiredCapacity(DesiredCapacity * 2)を実行して、新しいAMIのインスタンスを立ち上げる

ここまでで新しいバージョンのシステムをリリースすることが出来ました。

4. スケールイン

古いAMIインスタンスの破棄方法

特に何かしていません。
setDesiredCapacity()を使って、一時的にスケールアウトしている状態なので、ASGに設定されたcool down経過後は古いインスタンスからスケールインされます。

とはいえ、いきなりTerminateされても困る

Fluentdを使っているので、いきなりTerminateされてバッファがflushされないまま破棄されても困ります。
なので、Lifecycle Hookを使って下記を実行します。

  1. スケールイン前にSNSへ通知を送る
  2. SNS通知をトリガーにLambdaを実行する
  3. LambdaからSSMのRun Commandを実行
    1. Fluentdのバッファをflush
    2. CompleteLifecycleActionを実行

これでFluentdのバッファをflushさせつつ、スケールインさせることが出来ました。

それぞれ工夫したところ

ゴールデンイメージの作成

デプロイの度に1からAnsibleのプロビジョニングを実行していると、とても時間がかかります。
なのでAMIをbase / application_base / applicationと分けていて、アプリケーションの更新だけであれば、baseから作成したapplication_baseを使って新しいAMIを作っています。

それぞれのAMIは、下記の役割で作っています。

base

基本的にOSの設定(Timezone / Kernel parameters…)であったり、インタプリタのインストールであったり頻繁に変更が行われないものをプロビジョニングしています。

application_base

アプリケーションが依存するLinuxライブラリのインストール等を行っています。

application

アプリケーションのデプロイとビルドを行います。npm installnpm run hogeであったりbundle installはここで行っています。

システムの更新であればapplicationのビルドしか行いませんが、他のレポジトリで管理しているbaseもしくはapplication_baseの構成が変わった場合は、そちらのビルドが走ります。

Packer / Ansible

ディレクトリ構成

実際のものとは異なりますが、共通で読み込む変数とそうでないものでPackerもAnsibleもファイルを分けています。↓のcommon*です。

├── provisioners
│   ├── ansible.cfg
│   ├── base.yml
│   ├── inventories
│   │   ├── common.yml
│   │   ├── common_secrets.yml
│   │   ├── development
│   │   │   ├── group_vars
│   │   │   │   └── development
│   │   │   │       ├── secrets.yml
│   │   │   │       └── vars.yml
│   │   │   └── inventory
│   │   ├── production
│   │   │   ├── group_vars
│   │   │   │   └── production
│   │   │   │       ├── secrets.yml
│   │   │   │       └── vars.yml
│   │   │   └── inventory
│   ├── requirements.yml
│   ├── roles
│   ├── site.yml
├── packer.json
└── variables
    ├── base.json
    ├── common.json

ファイル名が*secretsになっているものはAnsible-vaultで暗号化したファイルです。
復号化はプロビジョニング実行時にtemporaryのpasswordファイルを用意してansibleに読み込ませています。
こんな感じです。

packer.json
"extra_arguments": [
  "--tags",
  "{{user `tags`}}",
  "--vault-password-file",
  ".vault"
]

Ansible Galaxy

極力Ansibleのroleは、Ansible Galaxyのroleとして使えるように書いて、GitHub EnterpriseにあるAnsible Galaxy organizationにレポジトリを作っています。
なので、provisioners/requirements.ymlが置いてあります。

site.yml

実際のものとは異なりますが、こんな感じにしてtagでincludeするymlファイルを制御しています。

---
- vars_files:
    - inventories/common.yml
    - inventories/common_secrets.yml
- include: base.yml tags=base
- include: hoge.yml tags=hoge

site.ymlは極力シンプルに。includeしている各ymlからroleを読み込んでいます。

Terraform

ディレクトリ構成

こちらも実際のものとは異なりますが、PackerやAnsible同様、共通で読み込む変数とそうでないものでこんな感じにしています。
また、Stageごとにtfstateを分けています。

├── environments
│   ├── common.tfvars
│   ├── development
│   │   ├── ami.tf
│   │   ├── main.tf
│   │   ├── provider.tf
│   │   ├── terraform.tfvars
│   │   └── variables.tf
│   └── production
│       ├── ami.tf
│       ├── main.tf
│       ├── provider.tf
│       ├── terraform.tfvars
│       └── variables.tf
├── provider.tf
├── vpc.tf
├── terraform.tfvars
└── variables.tf

Terraform Modules

Ansible Galaxy同様、こちらもorganizationを用意しレポジトリを作っています。
なので上のenvironments以下のディレクトリにあるmain.tfはmoduleを読み込み変数をセットするだけに留めています。

Lambda

Runtime

利用しているLambdaは全てnodejs4.3で記述していて、AWSリソースの操作はAWS SDKを利用しています。

デプロイ

apexを利用してLambdaのデプロイを行っています。
基本はapexのMultiple Environmentsに従った構成にしています。

├── functions
│   ├── hoge1
│   │   ├── function.development.json
│   │   ├── function.production.json
│   │   ├── index.js
│   │   └── package.json
│   └── hoge2
│       ├── function.development.json
│       ├── function.production.json
│       ├── index.js
│       └── package.json
├── project.development.json
└── project.production.json

project.jsonはこんな感じにして、

"nameTemplate": "{{.Project.Name}}_{{ .Project.Environment }}_{{.Function.Name}}",

同一ソースで異なるfunction名かつ、それぞれが異なる環境変数の値を保持することが出来ました。

最後に

AMIのビルドまでをアプリケーションのパッケージングに見立て構築しています。
本来、この思想であればコンテナを使うほうが楽だと思っていますが、アプリケーションのcontainerizedが出来ていなかったのでこうなっています。

また、当初はALBのリスナー付け替えでBlue-Green Deploymentにしようか悩んでいたのですが、瞬断が気になるので結局Rolling deploymentを選択しました。

全体的にざっくりとしか書いていません。本当は短い時間の中で紆余曲折あり今の形になっているのですが、それぞれのもっと細かい話は別の記事で書こうと思います。:innocent:

次の記事は、kadoppeさんです!よろしくお願いします!

続きを読む