しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

CircleCI を使って Elastic Beanstalk 向けに Windows Server の Custom AMI を作成する

少し Elastic Beanstalk の環境構築を ebextensions だけで行う限界を感じ始めたので、専用の Custom AMI を作成する方法を少し調べて試していました。公式ドキュメントにある手順で基本的には問題ないです。

Windows は例によって sysprep が必要だったり、少し Linux に比べて面倒で時間がかかる印象です。

Elastic Beanstalk の AMI はコミュニティ AMI として公開されているので、その AMI を使って EC2 インスタンスを起動して、あとは RDP などで自由に弄ってから AMI を作り直す。という感じです。

Windows Server 2012 R2 に関しては現時点では以下の AMI が最新です。

  • ami-3df7995a
    • 64bit Windows Server 2012 R2 v1.2.0 running IIS 8.5
  • ami-fecfa199
    • 64bit Windows Server Core 2012 R2 v1.2.0 running IIS 8.5

地味にアップデートがされるので、毎回 RDP でカスタマイズしていては手間がかかりすぎるので、CircleCI を使って自動化を試してみました。EC2 の UserData を使って RDP なしでカスタマイズを行います。

CircleCI で行う処理は以下のような流れです。

  1. EB の AMI と UserData を使って EC2 インスタンスを起動
    • UserData にカスタマイズの内容を書いておく
  2. UserData で EC2Config の sysprep を実行
  3. インスタンスが stopped になるのを待つ
  4. AMI を作成
  5. インスタンスを削除

CircleCI を使った理由は AWS AccessKey の扱いが楽だったり、実行結果が見やすいなどいろいろとあります。AWS CLI を使うだけなので、プラットフォームに依存しないというのもあります。その AWS CLI に食わせる JSON はテンプレ的なものを用意しておきました。

{
    "DryRun": false,
    "ImageId": "ami-fecfa199",
    "KeyName": "shibayan",
    "SecurityGroups": [
        "default"
    ],
    "InstanceType": "c4.xlarge",
    "Monitoring": {
        "Enabled": true
    }
}

ImageId は Elastic Beanstalk の AMI を指定します。RDP でのカスタマイズなどが必要ないので、今回は Windows Server Core 2012 R2 の AMI を使ってみました。処理時間短縮の思惑もあります。

少し大きめのインスタンスを使って処理時間の短縮をここでも図ります。ビルドが成功すると使用したインスタンスは削除するようにしているので、費用的には全然問題にならないレベルです。

一緒に食わせる UserData は PowerShell で書きました。お馴染みの ARR 3.0 をインストールしてみます。

<powershell>
$msipath = Join-Path $env:Temp "requestRouter_amd64.msi"

Invoke-WebRequest http://go.microsoft.com/fwlink/?LinkID=615136 -UseBasicParsing -OutFile $msipath
Start-Process -FilePath $msipath -ArgumentList "/qn" -Wait

& "$env:ProgramFiles\Amazon\Ec2ConfigService\ec2config.exe" -sysprep
</powershell>

ARR 3.0 のインストールが終わるまで待機した後に EC2Config を使って sysprep を実行します。sysprep を実行すると自動的にインスタンスが停止するので、AMI 作成前に stopped になるまで待機するようにします。

CircleCI のビルドを定義するファイルは以下のような感じです。AWS CLI の結果が JSON で返ってくるので、簡単に扱うために jq を入れているぐらいです。

checkout:
  post:
    - chmod +x ./create_custom_ami.sh

machine:
  environment:
    AWS_DEFAULT_REGION: ap-northeast-1

dependencies:
  override:
    - sudo pip install awscli
    - sudo curl -o /usr/bin/jq -L https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 && sudo chmod +x /usr/bin/jq

test:
  override:
    - aws ec2 run-instances --dry-run --cli-input-json file://custom-ami.json --user-data file://userdata.txt |& grep -q "succeeded"

deployment:
  master:
    branch: master
    commands:
      - ./create_custom_ami.sh:
          timeout: 1200

あとは念のためにタイムアウトを長くしていますが、これは UserData で行う処理に依存する部分です。test は CircleCI が成功しても no test 扱いになるので、一応 --dry-run を付けて実行するようにしました。

最後にメインの処理となるデプロイスクリプトです。長く見えますが ec2 wait を使ってステータスが変わるまで待機する部分が半分という感じです。

#!/bin/bash

echo "ec2 run-instances"
instanceId=$(aws ec2 run-instances --cli-input-json file://custom-ami.json --user-data file://userdata.txt | jq -r '.Instances[0].InstanceId')

echo "wait instance-status-ok - ${instanceId}"
aws ec2 wait instance-status-ok --instance-ids ${instanceId}

echo "wait instance-stopped - ${instanceId}"
aws ec2 wait instance-stopped --instance-ids ${instanceId}

echo "ec2 create-image"
imageId=$(aws ec2 create-image --instance-id ${instanceId} --name windows-ami-$(date "+%Y%m%d-%H%M%S") | jq -r '.ImageId')

echo "wait image-available - ${imageId}"
aws ec2 wait image-available --image-ids ${imageId}

echo "ec2 terminate-instances - ${instanceId}"
aws ec2 terminate-instances --instance-ids ${instanceId}

GitHub にリポジトリを作成して、CircleCI で実際にビルドを行いました。

EC2 インスタンスを作成して、UserData を実行し AMI を作成するまでに 10 分ほどかかりました。

f:id:shiba-yan:20170109211445p:plain

AWS マネジメントコンソールで確認するとちゃんと AMI が作成されています。この AMI を使って Elastic Beanstalk を作成すると終わりです。

f:id:shiba-yan:20170109212619p:plain

まだ UserData の書き方は色々と工夫する必要があると思いますが、ひとまずは Elastic Beanstalk 向け Custom AMI の作成を CircleCI で自動化出来ました。

実際に Elastic Beanstalk を作成してみたところ、問題なく動作しましたがプロビジョニングの時間がデフォルトのイメージより少しかかっていました。もう少し調べたい部分です。