EC2/Laravel+Code4兄弟による継続的デプロイ環境のCloudFormationをAWS CDKに置き換えてみました

AWS CDKGAになったので前回記事で構築したEC2/Laravel+Code4兄弟による継続的デプロイ環境をAWS CDK化して試してみました。

今回のコード

下記、タグv0.0.0になります。

github.com

CloudFormation環境も残しておきたかったので前回のリポジトリとは別にしています。言語は普段使いで型の恩恵が受けられるTypeScriptを選択しました。

環境構成

前回の記事の「環境構成」と同じなので省略します。

コードのディレクトリ構成は下記です。

.
├── Dockerfile_ecr
├── appspec.yml
├── aws_deploy.sh
├── buildspec.yml
├── buildspec_ecr.yml
├── infra
│   ├── bin
│   │   └── laravel-ec2-cdk-sample.ts ... エントリポイント
│   ├── cdk.json
│   ├── lib
│   │   ├── build-env-stack.ts        ... BuildEnvスタック
│   │   ├── code-store-stack.ts       ... CodeStoreスタック
│   │   ├── deploy-pipeline-stack.ts  ... DeployPipelineスタック
│   │   ├── ec2-stack.ts              ... EC2スタック
│   │   └── network-stack.ts          ... Networkスタック
│   ├── package-lock.json
│   ├── package.json
│   ├── setup_ec2.yml
│   ├── templates
│   │   ├── laravel.env.j2
│   │   └── nginx.conf.j2
│   └── tsconfig.json
└── laravel

基本的には下記などを参照したり cdk synth して infra/cdk.out/ にできたCloudFormationテンプレートファイルを確認しながら置き換え作業をしていきました。

docs.aws.amazon.com

docs.aws.amazon.com

github.com

構築手順

README.mdの通りです

CodeCommitを構築

前回の記事の「CodeCommitを構築」です。

コード管理の場所を用意してGitHubから取り込みます。

git clone https://github.com/nihemak/laravel-ec2-cdk-sample.git
cd laravel-ec2-cdk-sample/infra
npm install
npm run build
./node_modules/aws-cdk/bin/cdk deploy CodeStore --require-approval never
git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/laravel-ec2-sample --all

Laravelプロジェクトのビルドに必要なECR環境を構築

前回の記事の「Laravelプロジェクトのビルドに必要なECR環境を構築」です。

Laravelプロジェクトのビルドで使用するEC2インスタンスと同じ環境のdockerイメージが登録されたECRリポジトリを用意します。

./node_modules/aws-cdk/bin/cdk deploy BuildEnv --require-approval never
CODEBUILD_ID=$(aws codebuild start-build --project-name laravel-ec2-sample-ecr --source-version master | tr -d "\n" | jq -r '.build.id')
echo "started.. id is ${CODEBUILD_ID}"
while true
do
  sleep 10s
  STATUS=$(aws codebuild batch-get-builds --ids "${CODEBUILD_ID}" | tr -d "\n" | jq -r '.builds[].buildStatus')
  echo "..status is ${STATUS}."
  if [ "${STATUS}" != "IN_PROGRESS" ]; then
    if [ "${STATUS}" != "SUCCEEDED" ]; then
      echo "faild."
    fi
    echo "done."
    break
  fi
done

VPC/サブネット環境とECインスタンスを構築

前回の記事の「VPC/サブネット環境とECインスタンスを構築」です。

まずEC2インスタンスへのSSHアクセスの為にキーペア/pemを作成します。

key_pair=$(aws ec2 create-key-pair --key-name test-laravel)
echo $key_pair | jq -r ".KeyMaterial" > test-laravel.pem
chmod 400 test-laravel.pem

そしてVPC/サブネット環境とLaravelプロジェクトを動かすECインスタンスを用意します。

./node_modules/aws-cdk/bin/cdk deploy Network --require-approval never
./node_modules/aws-cdk/bin/cdk deploy EC2 --require-approval never

デプロイのパイプラインを構築

前回の記事の「デプロイのパイプラインを構築」です。

CodePipelineおよびCodeBuildとCodeDeploy、CodeCommitを監視してCodePipelineを実行するCloudWatch Eventsを用意します。

./node_modules/aws-cdk/bin/cdk deploy DeployPipeline --require-approval never

使い方

README.mdの通りです。前回の記事の「使い方」と同じなので省略します。

まとめ

今回はAWS CDKを試してみました。

  • プログラミング言語で構成を定義できるので共通化などを使えるのは利点だと思います。反面、あまり凝りすぎると分かりづらくなってインフラ構成の仕様としての利点が薄れる気がします。そのため、構成定義ファイルと割り切ったある程度の冗長な書き方にして後から構成仕様がすぐわかるようにしておくことが大切だと感じました。*1
  • 構成がブラックボックスのままプロビジョニングするのは事故につながるので生成されるCloudFormationテンプレートファイルがどうなるか注意して使う必要があると感じます。そう考えると意外と導入の敷居は高いかもと思いました。*2

*1:この辺はユニットテストコードとかと同じと思いますが

*2:逆にCloudFormationテンプレートファイルに慣れているプログラマならすんなり使えるかも

EC2/Laravel環境とCode4兄弟による継続的デプロイ環境をCloudFormation/Ansibleでコード化してみました

AWS CLIでの構築手順だけだとインフラのメンテナンス大変そうだなと思ったのでAWS CLIでCode4兄弟によるEC2+nginx+Laravelの継続的デプロイ環境を構築するで作ったの環境をCloudFormationとAnsibleでコード化してみました。

今回のコード

下記、タグv0.0.0になります。

github.com

環境構成

以前の記事の構成とほとんど同じ構成です。

f:id:nihma:20190707124913p:plain

CloudFormation化にあたりスタックを下記の5つに分けました。

スタック 説明 リソース
CodeStore コード管理 AWS::CodeCommit::Repository
BuildEnv ビルド環境 AWS::ECR::Repository, AWS::IAM::Role, AWS::CodeBuild::Project
Network VPC, サブネット環境 AWS::EC2::VPC, AWS::EC2::InternetGateway, AWS::EC2::VPCGatewayAttachment, AWS::EC2::Subnet, AWS::EC2::RouteTable, AWS::EC2::Route, AWS::EC2::SubnetRouteTableAssociation
EC2 EC2環境 AWS::EC2::SecurityGroup, AWS::EC2::EIP, AWS::IAM::Role, AWS::IAM::Policy, AWS::IAM::InstanceProfile, AWS::EC2::Instance, AWS::EC2::EIPAssociation
DeployPipeline デプロイ環境 AWS::S3::Bucket, AWS::IAM::Role, AWS::CodeBuild::Project, AWS::CodeDeploy::Application, AWS::CodeDeploy::DeploymentGroup, AWS::CodePipeline::Pipeline, AWS::Events::Rule

EC2インスタンス内のセットアップはUserDataとCodeDeploy/Ansibleで行うようにしました。

UserDataで行うのはamazon-linux-extrasやyumによるパッケージインストールとCodeDeploy agentのインストールです。

infra/EC2.cfn.yml:L75-L85

  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      # ...(省略)...
      UserData: !Base64
        Fn::Sub: |
          #!/bin/bash
          yum -y update
          amazon-linux-extras install ansible2 nginx1.12 php7.3 -y
          yum install -y php-fpm php-mbstring php-xml php-bcmath
          yum install -y python2-pip ruby
          cd /home/ec2-user
          curl -O https://aws-codedeploy-${AWS::Region}.s3.amazonaws.com/latest/install
          chmod +x ./install
          ./install auto

上記以外のセットアップ処理はCodeDeployにてansible-playbookのローカル実行で行うようにしました。

aws_deploy.sh:L7

# Setup EC2
ansible-playbook -i localhost, -c local ${DESTINATION_PATH}/infra/setup_ec2.yml

コードのディレクトリ構成は下記です。

.
├── Dockerfile_ecr    ... ECR向けのdockerイメージのDockerfile
├── appspec.yml       ... CodeDeployデプロイ定義
├── aws_deploy.sh     ... CodeDeployデプロイ処理
├── buildspec.yml     ... CodeBuildビルド定義
├── buildspec_ecr.yml ... ECR向けのCodeBuildビルド定義
├── infra             ... CloudFormationやAnsibleのテンプレート
│   └── templates     ... Ansibleのテンプレート
└── laravel           ... Laravelプロジェクト

構築手順

README.mdの通りです。

CodeCommitを構築

前回の記事の「LaravelプロジェクトをCodeCommit管理にする」です。

コード管理の場所を用意してGitHubから取り込みます。

git clone https://github.com/nihemak/laravel-ec2-sample.git
cd laravel-ec2-sample
aws cloudformation validate-template \
    --template-body file://infra/CodeStore.cfn.yml
aws cloudformation create-stack \
    --stack-name laravel-ec2-sample-CodeStore \
    --template-body file://infra/CodeStore.cfn.yml
git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/laravel-ec2-sample --all

Laravelプロジェクトのビルドに必要なECR環境を構築

前回の記事の「CodeBuild環境を整える」です。

Laravelプロジェクトのビルドで使用するEC2インスタンスと同じ環境のdockerイメージが登録されたECRリポジトリを用意します。

aws cloudformation validate-template \
    --template-body file://infra/BuildEnv.cfn.yml
aws cloudformation create-stack \
    --stack-name laravel-ec2-sample-BuildEnv \
    --capabilities CAPABILITY_NAMED_IAM \
    --parameters \
      ParameterKey=CodeCommitStackName,ParameterValue=laravel-ec2-sample-CodeStore \
    --template-body file://infra/BuildEnv.cfn.yml
CODEBUILD_ID=$(aws codebuild start-build --project-name laravel-ec2-sample-ecr --source-version master | tr -d "\n" | jq -r '.build.id')
echo "started.. id is ${CODEBUILD_ID}"
while true
do
  sleep 10s
  STATUS=$(aws codebuild batch-get-builds --ids "${CODEBUILD_ID}" | tr -d "\n" | jq -r '.builds[].buildStatus')
  echo "..status is ${STATUS}."
  if [ "${STATUS}" != "IN_PROGRESS" ]; then
    if [ "${STATUS}" != "SUCCEEDED" ]; then
      echo "faild."
    fi
    echo "done."
    break
  fi
done

VPC/サブネット環境とECインスタンスを構築

前回の記事の「最初のEC2+nginx+Laravelな環境を作成する」です。

まずEC2インスタンスへのSSHアクセスの為にキーペア/pemを作成します。

key_pair=$(aws ec2 create-key-pair --key-name test-laravel)
echo $key_pair | jq -r ".KeyMaterial" > test-laravel.pem
chmod 400 test-laravel.pem

そしてVPC/サブネット環境とLaravelプロジェクトを動かすECインスタンスを用意します。

aws cloudformation validate-template \
    --template-body file://infra/Network.cfn.yml
aws cloudformation create-stack \
    --stack-name laravel-ec2-sample-Network \
    --capabilities CAPABILITY_NAMED_IAM \
    --template-body file://infra/Network.cfn.yml
aws cloudformation validate-template \
    --template-body file://infra/EC2.cfn.yml
aws cloudformation create-stack \
    --stack-name laravel-ec2-sample-EC2 \
    --capabilities CAPABILITY_NAMED_IAM \
    --parameters \
      ParameterKey=NetworkStackName,ParameterValue=laravel-ec2-sample-Network \
    --template-body file://infra/EC2.cfn.yml

デプロイのパイプラインを構築

前回の記事の「CodeDeploy環境を整える」「CodeBuild環境を整える」「CodePipeline環境を整える」「CodeBuildをキャッシュ対応する」「CloudWatch EventsでCodePipelineが自動実行されるよう設定する」です。

CodePipelineおよびCodeBuildとCodeDeploy、CodeCommitを監視してCodePipelineを実行するCloudWatch Eventsを用意します。

aws cloudformation validate-template \
    --template-body file://infra/DeployPipeline.cfn.yml
aws cloudformation create-stack \
    --stack-name laravel-ec2-sample-DeployPipeline \
    --capabilities CAPABILITY_NAMED_IAM \
    --parameters \
      ParameterKey=CodeCommitStackName,ParameterValue=laravel-ec2-sample-CodeStore \
    --template-body file://infra/DeployPipeline.cfn.yml

作成が終わると自動でCodePipelineが実行されEC2インスタンスにLaravelプロジェクトがデプロイされます。

使い方

README.mdの通りです。

SSHアクセス

まずEC2のCloudFormationスタックからIPアドレスを取得します。

ec2_ip=$(\
  aws cloudformation describe-stacks --stack-name laravel-ec2-sample-EC2 \
   | jq -r '.Stacks[].Outputs[] | select(.OutputKey == "EC2IP").OutputValue')
echo ${ec2_ip}

pemとIPアドレスでEC2インスタンスSSHできます。

ssh -i test-laravel.pem ec2-user@${ec2_ip}

ブラウザアクセス

ブラウザからhttps://${ec2_ip}にアクセスできます。

ただ今回はSSL自己証明書にしたので初回は怒られます。

f:id:nihma:20190707174513p:plain

xxxxにアクセスする(安全ではありません)をクリックするとトップページを見ることができます。

f:id:nihma:20190707174914p:plain

自動デプロイ

masterへのpushを行うとCodePipelineが自動実行されてデプロイされます。

$ vi laravel/resources/views/welcome.blade.php
$ git diff
diff --git a/laravel/resources/views/welcome.blade.php b/laravel/resources/views/welcome.blade.php
index 044b874..7d7190b 100644
--- a/laravel/resources/views/welcome.blade.php
+++ b/laravel/resources/views/welcome.blade.php
@@ -81,7 +81,7 @@

             <div class="content">
                 <div class="title m-b-md">
-                    Laravel
+                    Hello World!
                 </div>

                 <div class="links">
$ git add -A
$ git commit -m "Test master push"
$ git push ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/laravel-ec2-sample --all

しばらくしてブラウザからhttps://${ec2_ip}にアクセスするとトップページの文字列がLaravelからHello World!に変わります。

f:id:nihma:20190707175916p:plain

まとめ

今回はCloudFormationとAnsibleを使ってEC2で動くLaravel環境とCode4兄弟による継続的デプロイ環境をコード管理できるようにしてみました。

  • このままだとデプロイのたびにサービスが止まってしまうのでBlue/Greenデプロイくらいは対応した方が良さそう
  • UserDataとAnsibleの部分をPacker+AnsibleにしてAMIの構築を別のフェーズで行う構成の方が役割分担的に良さそう
  • 素直にECS/Fargate環境にした方が良さそう*1

*1:とはいえ色々な事情で気軽にコンテナを使えない現場もあるので...

コンピュータシステムの理論と実装の1〜5章のハードウェアを実装しました(ネタバレ注意)

たまには低レベルなこともしたくて*1コンピュータシステムの理論と実装(以下、nand2tetris本)を始めてみました。nand2tetris本NANDゲートのみ*2からCPU/OSなどを実装していく素敵な書籍です。今回は1〜5章のハードウェア部分を実装してみたので忘れっぽい自分のためのメモです。自力で実装に挑戦してみたい人にはネタバレになると思うので注意です。

今回のコード

下記、タグv0.0.0になります。

github.com

下記で動かせます。

git clone -b v0.0.0 https://github.com/nihemak/nand2tetris.git
cd nand2tetris
# download nand2tetris environment
./setup.sh
# test all
./test.sh

1章 ブール論理

Not

真理値表は下記です。

in - out
0 - 1
1 - 0

これはNand(in, in)です。関連するNandの真理値表は下記です。

a b - out
0 0 - 1
1 1 - 0

コードは01/Not.hdlです。

And

真理値表は下記です。

a b - out
0 0 - 0
0 1 - 0
1 0 - 0
1 1 - 1

これはNot(Nand(a, b))です。

Nand(a, b) = Not(And(a, b))の両辺にNotを適用してNotを打ち消した左辺と考えることができます。

コードは01/And.hdlです。

Or

真理値表は下記です。

a b - out
0 0 - 0
0 1 - 1
1 0 - 1
1 1 - 1

これはNot(And(Not(a), Not(b)))です。

ドモルガンの法則であるNot(Or(a, b)) = And(Not(a), Not(b))の両辺にNotを適用した右辺と考えることができます。

コードは01/Or.hdlです。

Xor

真理値表は下記です。

a b - out
0 0 - 0
0 1 - 1
1 0 - 1
1 1 - 0

これはOr(And(a, Not(b)), And(Not(a), b))です。

次の2つのOrを行い(1, 0)(0, 1)1となるようにすると考えることができます。

  • (1, 0)のみが1となるAnd(a, Not(b))
  • (0, 1)のみが1となるAnd(Not(a), b)

(1, 0)のみが1となるAnd(a, Not(b))の真理値表は下記です。

a Not(b) - And(a, Not(b))
0 1 - 0
0 0 - 0
1 1 - 1
1 0 - 0

(0, 1)のみが1となるAnd(Not(a), b)の真理値表は下記です。

Not(a) b - And(Not(a), b)
1 0 - 0
1 1 - 1
0 0 - 0
1 1 - 0

Or(And(a, Not(b)), And(Not(a), b))の真理値表は下記です。

And(a, Not(b)) And(Not(a), b) - out
0 0 - 0
0 1 - 1
1 0 - 1
0 0 - 0

コードは01/Xor.hdlです。

Mux

真理値表は下記です。

a b sel - out
0 0 0 - 0
0 1 0 - 0
1 0 0 - 1
1 1 0 - 1
0 0 1 - 0
0 1 1 - 1
1 0 1 - 0
1 1 1 - 1

これはAnd(Or(a, sel), Or(b, Not(sel)))です。

sel = 0の場合はa, sel = 1の場合はbにすれば良いと考えることができます。

sel - out
0 - a
1 - b

これには次の3つが使えます。

  • Or(x, 0) = x
  • Or(x, 1) = 1
  • And(x, 1) = 1

下記のようにselによって取り出す列を選ぶことができます。

  • sel = 0の場合、Or(a, sel) = aおよびOr(b, Not(sel)) = 1となるのでAnd(a, 1) = aになる
  • sel = 1の場合、Or(a, sel) = 1およびOr(b, Not(sel)) = bとなるのでAnd(1, b) = bになる

コードは01/Mux.hdlです。

DMux

真理値表は下記です。

in sel - a b
0 0 - 0 0
1 0 - 1 0
0 1 - 0 0
1 1 - 0 1

これは下記です。

  • a = And(in, Not(sel))
  • b = And(in, sel)

sel = 0の場合はa = in, b = 0, sel = 1の場合はa = 0, b = inにすれば良いと考えることができます。

sel - a b
0 - in 0
1 - 0 in

これには次の2つが使えます。

  • And(x, 0) = 0
  • And(x, 1) = 1

下記のようにselによって取り出す列を選ぶことができます。

  • sel = 0の場合、a = And(in, 1), b = And(in, 0)となるのでa = in, b = 0になる
  • sel = 1の場合、a = And(in, 0), b = And(in, 1)となるのでa = 0, b = inになる

コードは01/DMux.hdlです。

Not16

これは次のように全ての要素にNotを適用するだけです。

  • out[0] = Not(in[0])
  • out[1] = Not(in[1])
  • ...
  • out[14] = Not(in[14])
  • out[15] = Not(in[15])

コードは01/Not16.hdlです。

And16

これは次のように全ての要素にAndを適用するだけです。

  • out[0] = And(a[0], b[0])
  • out[1] = And(a[1], b[1])
  • ...
  • out[14] = And(a[14], b[14])
  • out[15] = And(a[15], b[15])

コードは01/And16.hdlです。

Or16

これは次のように全ての要素にOrを適用するだけです。

  • out[0] = Or(a[0], b[0])
  • out[1] = Or(a[1], b[1])
  • ...
  • out[14] = Or(a[14], b[14])
  • out[15] = Or(a[15], b[15])

コードは01/Or16.hdlです。

Mux16

これは次のように全ての要素にMuxを適用するだけです。

  • out[0] = Mux(a[0], b[0], sel)
  • out[1] = Mux(a[1], b[1], sel)
  • ...
  • out[14] = Mux(a[14], b[14], sel)
  • out[15] = Mux(a[15], b[15], sel)

コードは01/Mux16.hdlです。

Or8Way

これは次のように要素すべてのOrを行うだけです。

Or(Or(in[0], in[1]), Or(in[2], in[3])), Or(Or(in[4], in[5]), Or(in[6], in[7]))

コードは01/Or8Way.hdlです。

Mux4Way16

真理値表は下記です。

sel[1] sel[0] - out
0 0 - a
0 1 - b
1 0 - c
1 1 - d

これはMux16(Mux16(a, b, sel[0]), Mux16(c, d, sel[0]), sel[1])です。

sel[0]を使ってMux16a, bから1つ、c, dから1つを選択、それらの結果からsel[1]を使って1つをMux16で選択すると求めることができます。

sel[0]を使ってMux16a, bから1つを選択する真理値表は下記です。

sel[0] - Mux16(a, b, sel[0])
0 - a
1 - b
0 - a
1 - b

sel[0]を使ってMux16c, dから1つを選択する真理値表は下記です。

sel[0] - Mux16(c, d, sel[0])
0 - c
1 - d
0 - c
1 - d

上記の2つの結果からsel[1]を使って1つをMux16で選択する真理値表は下記です。

sel[1] - Mux16(Mux16(a, b, sel[0]), Mux16(c, d, sel[0]), sel[1])
0 - Mux16(a, b, sel[0]) = a
0 - Mux16(a, b, sel[0]) = b
1 - Mux16(c, d, sel[0]) = c
1 - Mux16(c, d, sel[0]) = d

コードは01/Mux4Way16.hdlです。

Mux8Way16

真理値表は下記です。

sel[2] sel[1] sel[0] - out
0 0 0 - a
0 0 1 - b
0 1 0 - c
0 1 1 - d
1 0 0 - e
1 0 1 - f
1 1 0 - g
1 1 1 - h

これはMux16(Mux4Way16(a, b, c, d, sel[0], sel[1]), Mux4Way16(e, f, g, h, sel[0], sel[1]), sel[2])です。

考え方はMux8Way16と同じです。まずsel[0..1]を使ってMux4Way16a, b, c, de, f, g, hから1つずつ選択、それらの結果からsel[2]を使って1つをMux16で選択すると求めることができます。

sel[0..1]を使ってMux4Way16a, b, c, dから1つ選択する真理値表は下記です。

sel[1] sel[0] - Mux4Way16(a, b, c, d, sel[0], sel[1])
0 0 - a
0 1 - b
1 0 - c
1 1 - d
0 0 - a
0 1 - b
1 0 - c
1 1 - d

sel[0..1]を使ってMux4Way16e, f, g, hから1つ選択する真理値表は下記です。

sel[1] sel[0] - Mux4Way16(e, f, g, h, sel[0], sel[1])
0 0 - e
0 1 - f
1 0 - g
1 1 - h
0 0 - e
0 1 - f
1 0 - g
1 1 - h

上記の2つの結果からsel[2]を使って1つをMux16で選択する真理値表は下記です。

sel[2] - Mux16(Mux4Way16(a, b, c, d, sel[0], sel[1]), Mux4Way16(e, f, g, h, sel[0], sel[1]), sel[2])
0 - Mux4Way16(a, b, c, d, sel[0], sel[1]) = a
0 - Mux4Way16(a, b, c, d, sel[0], sel[1]) = b
0 - Mux4Way16(a, b, c, d, sel[0], sel[1]) = c
0 - Mux4Way16(a, b, c, d, sel[0], sel[1]) = d
1 - Mux4Way16(e, f, g, h, sel[0], sel[1]) = e
1 - Mux4Way16(e, f, g, h, sel[0], sel[1]) = f
1 - Mux4Way16(e, f, g, h, sel[0], sel[1]) = g
1 - Mux4Way16(e, f, g, h, sel[0], sel[1]) = h

コードは01/Mux8Way16.hdlです。

DMux4Way

真理値表は下記です。

sel[1] sel[0] - a b c d
0 0 - in 0 0 0
0 1 - 0 in 0 0
1 0 - 0 0 in 0
1 1 - 0 0 0 in

これは下記です。

  • (w0, w1) = DMux(in, sel[0])
  • (a, c) = DMux(w0, sel[1])
  • (b, d) = DMux(w1, sel[1])

sel[0]を使ってDMuxinを2つに分離します。そして、前者をsel[1]を使ってDMuxで2つに分離するとa, c、後者をsel[1]を使ってDMuxで2つに分離するとb, dを求めることができます。

sel[0]を使ってDMuxinを2つに分離する真理値表は下記です。

sel[0] - w0 w1
0 - in 0
1 - 0 in
0 - in 0
1 - 0 in

前者をsel[1]を使ってDMuxで2つに分離する真理値表は下記です。

sel[1] - a c
0 - w0 = in 0
0 - w0 = 0 0
1 - 0 w0 = in
1 - 0 w0 = 0

後者をsel[1]を使ってDMuxで2つに分離する真理値表は下記です。

sel[1] - b d
0 - w1 = 0 0
0 - w1 = in 0
1 - 0 w1 = 0
1 - 0 w1 = in

これらを組み合わせると正しい真理値表になります。

sel[1] sel[0] - a b c d
0 0 - w0 = in w1 = 0 w0 = 0 w1 = 0
0 1 - w0 = 0 w1 = in w0 = 0 w1 = 0
1 0 - w0 = 0 w1 = 0 w0 = in w1 = 0
1 1 - w0 = 0 w1 = 0 w0 = 0 w1 = in

コードは01/DMux4Way.hdlです。

DMux8Way

真理値表は下記です。

sel[2] sel[1] sel[0] - a b c d e f g h
0 0 0 - in 0 0 0 0 0 0 0
0 0 1 - 0 in 0 0 0 0 0 0
0 1 0 - 0 0 in 0 0 0 0 0
0 1 1 - 0 0 0 in 0 0 0 0
1 0 0 - 0 0 0 0 in 0 0 0
1 0 1 - 0 0 0 0 0 in 0 0
1 1 0 - 0 0 0 0 0 0 in 0
1 1 1 - 0 0 0 0 0 0 0 in

これは下記です。

  • (w0, w1, w2, w3) = DMux4Way(in, sel[0..1])
  • (a, e) = DMux(w0, sel[2])
  • (b, f) = DMux(w1, sel[2])
  • (c, g) = DMux(w2, sel[2])
  • (d, h) = DMux(w3, sel[2])

考え方はDMux4Wayと同じです。まずsel[0..1]を使ってDMux4Wayinを4つに分離します。そして、それぞれをsel[2]を使ってDMuxで2つに分離するとa, eb, fc, gd, hを求めることができます。

sel[0..1]を使ってDMux4Wayinを4つに分離する真理値表は下記です。

sel[1] sel[0] - w0 w1 w2 w3
0 0 - in 0 0 0
0 1 - 0 in 0 0
1 0 - 0 0 in 0
1 1 - 0 0 0 in
0 0 - in 0 0 0
0 1 - 0 in 0 0
1 0 - 0 0 in 0
1 1 - 0 0 0 in

sel[2]を使ってDMuxa, eに分離する真理値表は下記です。

sel[2] - a e
0 - w0 = in 0
0 - w0 = 0 0
0 - w0 = 0 0
0 - w0 = 0 0
1 - 0 w0 = in
1 - 0 w0 = 0
1 - 0 w0 = 0
1 - 0 w0 = 0

sel[2]を使ってDMuxb, fに分離する真理値表は下記です。

sel[2] - b f
0 - w1 = 0 0
0 - w1 = in 0
0 - w1 = 0 0
0 - w1 = 0 0
1 - 0 w1 = 0
1 - 0 w1 = in
1 - 0 w1 = 0
1 - 0 w1 = 0

sel[2]を使ってDMuxc, gに分離する真理値表は下記です。

sel[2] - c g
0 - w2 = 0 0
0 - w2 = 0 0
0 - w2 = in 0
0 - w2 = 0 0
1 - 0 w2 = 0
1 - 0 w2 = 0
1 - 0 w2 = in
1 - 0 w2 = 0

sel[2]を使ってDMuxd, hに分離する真理値表は下記です。

sel[2] - d h
0 - w3 = 0 0
0 - w3 = 0 0
0 - w3 = 0 0
0 - w3 = in 0
1 - 0 w3 = 0
1 - 0 w3 = 0
1 - 0 w3 = 0
1 - 0 w3 = in

これらを組み合わせると正しい真理値表になります。

sel[2,1,0] - a b c d e f g h
000 - w0 = in w1 = 0 w2 = 0 w3 = 0 w0 = 0 w1 = 0 w2 = 0 w3 = 0
001 - w0 = 0 w1 = in w2 = 0 w3 = 0 w0 = 0 w1 = 0 w2 = 0 w3 = 0
010 - w0 = 0 w1 = 0 w2 = in w3 = 0 w0 = 0 w1 = 0 w2 = 0 w3 = 0
011 - w0 = 0 w1 = 0 w2 = 0 w3 = in w0 = 0 w1 = 0 w2 = 0 w3 = 0
100 - w0 = 0 w1 = 0 w2 = 0 w3 = 0 w0 = in w1 = 0 w2 = 0 w3 = 0
101 - w0 = 0 w1 = 0 w2 = 0 w3 = 0 w0 = 0 w1 = in w2 = 0 w3 = 0
110 - w0 = 0 w1 = 0 w2 = 0 w3 = 0 w0 = 0 w1 = 0 w2 = in w3 = 0
111 - w0 = 0 w1 = 0 w2 = 0 w3 = 0 w0 = 0 w1 = 0 w2 = 0 w3 = in

コードは01/DMux8Way.hdlです。

2章 ブール算術

HalfAdder

真理値表は下記です。

a b - carry sum
0 0 - 0 0
0 1 - 0 1
1 0 - 0 1
1 1 - 1 0

これは下記です。

  • sum = Xor(a, b)
  • carry = And(a, b)

コードは02/HalfAdder.hdlです。

FullAdder

真理値表は下記です。

a b c - carry sum
0 0 0 - 0 0
0 0 1 - 0 1
0 1 0 - 0 1
0 1 1 - 1 0
1 0 0 - 0 1
1 0 1 - 1 0
1 1 0 - 1 0
1 1 1 - 0 1

これは下記です。

  • (sum0, carry0) = HalfAdder(a, b)
  • (sum, carry1) = HalfAdder(sum, c)
  • carry = Or(carry0, carry1)

suma, b, cすべてのHalfAddersumcarryは「a, bHalfAddercarry」と「a, bHalfAddersumcHalfAddercarry」のOrです。

  • sum = Sum(Sum(a, b), c) = Xor(Xor(a, b), c)
  • carry = Or(Carry(a, b), Carry(Sum(a, b), c)) = Or(And(a, b), And(Xor(a, b), c))

コードは02/FullAdder.hdlです。

Add16

表にすると下記のようになります。

a b carry - out
a[0] b[0] - - Sum(a[0], b[0])
a[1] b[1] Carry(a[0], b[0]) - Sum(a[1], b[1])
a[2] b[2] Carry(a[1], b[1]) - Sum(a[2], b[2])
a[3] b[3] Carry(a[2], b[2]) - Sum(a[3], b[3])
a[4] b[4] Carry(a[3], b[3]) - Sum(a[4], b[4])
a[5] b[5] Carry(a[4], b[4]) - Sum(a[5], b[5])
a[6] b[6] Carry(a[5], b[5]) - Sum(a[6], b[6])
a[7] b[7] Carry(a[6], b[6]) - Sum(a[7], b[7])
a[8] b[8] Carry(a[7], b[7]) - Sum(a[8], b[8])
a[9] b[9] Carry(a[8], b[8]) - Sum(a[9], b[9])
a[10] b[10] Carry(a[9], b[9]) - Sum(a[10], b[10])
a[11] b[11] Carry(a[10], b[10]) - Sum(a[11], b[11])
a[12] b[12] Carry(a[11], b[11]) - Sum(a[12], b[12])
a[13] b[13] Carry(a[12], b[12]) - Sum(a[13], b[13])
a[14] b[14] Carry(a[13], b[13]) - Sum(a[14], b[14])
a[15] b[15] Carry(a[14], b[14]) - Sum(a[15], b[15])

これは1個のHalfAdderと15個のFullAdderを組み合わせれば良いです。

ゲートの構成は下記です。

f:id:nihma:20190427094246p:plain

コードは02/Add16.hdlです。

Inc16

これはAdd16(in, %B0000000000000001)です。

コードは02/Inc16.hdlです。

ALU

これは場合分けして考えます。

まずzxnxzynyを考慮した値を求めます。

  • x1 = Add16(x, Mux16(x, %B0000000000000000, zx))
  • x2 = Mux16(x1, Not(x1), nx)
  • y1 = Add16(y, Mux16(y, %B0000000000000000, zy))
  • y2 = Mux16(y1, Not(y1), ny)

そしてfnoを考慮してoutを求めます。

  • xy = Mux16(Add16(x2, y2), And16(x2, y2), f)
  • out = Mux16(xy, Not(xy), no)

あとはoutからzrngを求めることができます。

  • zr = Not(Or(Or8Way(out[0..7]), Or8Way(out[8..15]))))
  • ng = Or(out[15], 0)

コードは02/ALU.hdlです。

3章 順序回路

Bit

これはそのままです。

ゲートの構成は下記です。

f:id:nihma:20190427140009p:plain

コードは03/a/Bit.hdlです。

Register

これはBitを16個ならべるだけです。

  • out[0] = Bit(in[0], load)
  • out[1] = Bit(in[1], load)
  • ...
  • out[14] = Bit(in[14], load)
  • out[15] = Bit(in[15], load)

コードは03/a/Register.hdlです。

RAM8

これは8個のRegisterを並べます。

下記の3ステップで求めることができます。

  • addressRegisterのみload、それ以外は書き込みをしないようにfalseRegisterに指定するためにDMux8Wayで各loadを求める
  • Registerinを適用してそれぞれのoutを求める
  • addressoutMux8Way16で求める

ゲートの構成は下記です。

f:id:nihma:20190427142402p:plain

コードは03/a/RAM8.hdlです。

RAM64

これはRAM8と同じ考え方で下記を変更すればできます。

  • DMux8Wayselにはaddress[3..5]を指定する
  • RegisterRAM8にしてaddressにはaddress[0..2]を指定する
  • Mux8Way16selにはaddress[3..5]を指定する

ゲートの構成は下記です。

f:id:nihma:20190427162013p:plain

コードは03/a/RAM64.hdlです。

RAM512

これはRAM64と同じ考え方で下記を変更すればできます。

  • DMux8Wayselにはaddress[6..8]を指定する
  • RAM8RAM64にしてaddressにはaddress[0..5]を指定する
  • Mux8Way16selにはaddress[6..8]を指定する

ゲートの構成は下記です。

f:id:nihma:20190427162147p:plain

コードは03/b/RAM512.hdlです。

RAM4K

これはRAM512と同じ考え方で下記を変更すればできます。

  • DMux8Wayselにはaddress[9..11]を指定する
  • RAM64RAM512にしてaddressにはaddress[0..8]を指定する
  • Mux8Way16selにはaddress[9..11]を指定する

コードは03/b/RAM4K.hdlです。

RAM16K

これはRAM4Kと同じ考え方で下記を変更すればできます。

  • DMux8WayDMux4Wayに変えselにはaddress[12..13]を指定する
  • RAMの数を4個に変えRAM512RAM4Kにしてaddressにはaddress[0..11]を指定する
  • Mux8Way16Mux4Way16に変えselにはaddress[12..13]を指定する

ゲートの構成は下記です。

f:id:nihma:20190427163622p:plain

コードは03/b/RAM16K.hdlです。

PC

これは下記のように考えます。

  • 前回結果に依存するのでRegisterを使う
  • Registerに記録するのはMux4Way16resetおよびloadを使い%B0000000000000000in前回結果または前回結果+1から決定する
  • 前回結果または前回結果+1Mux16incを使い決定する

結果の決定の順序はreset > load > incの優先度によります。

Mux16incを使い決定する前回結果または前回結果+1の真理値表は下記です。

sel = inc - out
0 - a = 前回結果
1 - b = 前回結果+1 = Add16(前回結果, %B0000000000000001)

Mux4Way16resetおよびloadを使い%B0000000000000000in前回結果または前回結果+1から決定する真理値表は下記です。

sel[1] = reset sel[0] = load - out
0 0 - a = 前回結果または前回結果+1 = Mux16(前回結果, Add16(前回結果, %B0000000000000001), inc)
0 1 - b = in
1 0 - c = %B0000000000000000
1 1 - d = %B0000000000000000

ゲートの構成は下記です。

f:id:nihma:20190427182518p:plain

コードは03/a/PC.hdlです。

4章 機械語

Mult

ロジックは下記です。

f:id:nihma:20190427191304p:plain

コードは04/mult/mult.asmです。

Fill

ロジックは下記です。*3

f:id:nihma:20190429140142p:plain

コードは04/fill/Fill.asmです。

5章 コンピュータアーキテクチャ

Memory

メモリマップは下記です。

用途 - 開始アドレス 終了アドレス
RAM - 0 = %B000000000000000 16383 = 2^14 - 1 = %B011111111111111
Screen - 16384 = %B100000000000000 24575 = %B101111111111111
Keyboard - 24576 = %B110000000000000 24576 = %B110000000000000

メモリマップの通りaddress[14]RAMかどうか、address[13]ScreenKeyboardを判別することができる仕様の構成になっています。

下記のステップで考えることができます。

  • DMuxaddress[14]を使ってloadRAMScreenのどちらに指定するか決定する
  • Mux4Way16address[13..14]を使ってRAMScreenKeyboardからどれを結果にするか決定する

DMuxaddress[14]を使ってloadRAMScreenのどちらに指定するか決定する真理値表は下記です。

address[14] - RAMload Screenload
0 - load 0
1 - 0 load

Mux4Way16address[13..14]を使ってRAMScreenKeyboardからどれを結果にするか決定する真理値表は下記です。

address[14] address[13] - out
0 0 - RAMの結果
0 1 - RAMの結果
1 0 - Screenの結果
1 1 - Keyboardの結果

ゲートの構成は下記です。

f:id:nihma:20190428010042p:plain

コードは05/Memory.hdlです。

CPU

これはinstructionに指定される機械語の仕様から考えると分かりやすいです。

基数 - A命令 C命令
15 - 0 1
14 - v 1
13 - v 1
12 - v a: ALUのy(0: A, 1: M)
11 - v c1: ALUのzx
10 - v c2: ALUのnx
9 - v c3: ALUのzy
8 - v c4: ALUのny
7 - v c5: ALUのf
6 - v c6: ALUのno
5 - v d1: ARegisterのload
4 - v d2: DRegisterのload
3 - v d3: writeM
2 - v j1: PCのload(ただしALUのoutが負の場合、すなわちngが1の場合)
1 - v j2: PCのload(ただしALUのoutが0の場合、すなわちzrが1の場合)
0 - v j3: PCのload(ただしALUのoutが正の場合)

その他、補足は下記です。

  • ALUのoutが正かどうかはALUのngとzrからNot(Or(ng, zr))で求めることができる
  • A命令の場合もARegisterのloadを1にする

ゲートの構成は下記です。

f:id:nihma:20190428125847p:plain

コードは05/CPU.hdlです。

Computer

これは仕様にしたがってROM32KCPUMemoryを配線します。

ゲートの構成は下記です。

f:id:nihma:20190428133935p:plain

コードは05/Computer.hdlです。

まとめ

実装よりもまとめる方がしんどかったですが理解を深められてデバッグにもなりました。

*1:いつも低レベルですが

*2:正確にはD型フリップフロップやROM、ScreenやKeybordなども基本要素です

*3:高速化などは考慮してません

忙しいソフトウェアエンジニアにお勧めする友人の作り方

とある仕事関連のイベントでLTをさせていただきました。

ミッションだった下記を達成できたのでよかったです。

  • LTのトップバッターとして場を和ませる*1
  • 勤め先の制度としてエンジニアの自己研鑽のための補助制度を検討事項にしてもらう

*1:最後のスライドを「静聴」にしていましたが実際は「清聴」でした

AlphaZero風オセロにFlaskでWebUIを追加しECS/Fargateで動かしてみた

前々回前回AlphaZero風オセロ強化学習環境を作っていましたが、今回はFlaskでWeb化したゲーム部分を追加してECS/Fargateで動かしてみました。*1

今回のコード

下記、タグv0.0.3になります。

github.com

FlaskによるWebUI

ブラウザでアクセスしてオセロゲームしている様子のイメージは下記です。

f:id:nihma:20190309234827g:plain

実行方法

docker build & runするとFlaskのWebサーバがポート5000で立ち上がります。

README.md

docker build -f Dockerfile_app -t heta-reversi-app:latest .
docker run -d -p 5000:5000 heta-reversi-app

ブラウザでhttp://localhost:5000/にアクセスするとゲームが始まります。オセロ盤の赤丸が石の置ける場所ですのでクリックしてください。

デフォルトの対戦相手のアルゴリズムはランダムです。画面下部のリンクをクリックすると対戦相手のアルゴリズムを変更してゲームを最初から開始できます。*2

f:id:nihma:20190310131219p:plain

実装概要

関連するソース一式は下記です。

.
├── Dockerfile_app
├── Reversi.py
├── app.py
├── data
│   └── model.dat
├── static
│   └── css
│       └── reversi.css
└── templates
    └── index.html

Docker環境はchainer/chainer:v5.1.0-python3 + Flaskの構成です。

Dockerfile_app

FROM chainer/chainer:v5.1.0-python3

COPY . /app
WORKDIR /app

RUN pip3 install --upgrade pip
RUN pip3 install Flask==1.0.2

ENTRYPOINT ["python3"]
CMD ["app.py"]

エントリポイントはapp.pyです。 Base64エンコードしたオセロ盤と次の石の位置を含むjsonをGetパラメータで引き渡す方法にしました。*3

app.py:L17-L19

@app.route('/')
def random():
    return next('/', choice_random, 'random')

app.py:L35-L56

def next(url, choice, algorithm):
    board = Reversi.get_init_board()
    query = request.args.get('query')
    if query:
        params = json.loads(base64.urlsafe_b64decode(query).decode())
        player = Reversi.get_player(np.array(params['board']))
        board = Reversi.put(player, params['num'])
        while True:
            player = Reversi.get_player(board, False)
            if Reversi.is_putable(player):
                choice_data = choice(player)
                board = Reversi.put(player, choice_data['position_num'])
                player = Reversi.get_player(board)
                if Reversi.is_putable(player):
                    break
            else:
                break

    board, is_black, putable_position_nums = Reversi.get_player(board)
    black_num, white_num = Reversi.get_stone_num(board)
    return render_template('index.html',
                           url=url, algorithm=algorithm, is_end_board=Reversi.is_end_board(board), black_num=black_num, white_num=white_num, board=board, is_black=is_black, putable_position_nums=putable_position_nums)

htmlテンプレートはJinja2でtemplates/index.htmlです。

ECS/Fargateで動かす

全体構成は下記です。

f:id:nihma:20190310114836p:plain

ECRにイメージを登録するCodeBuildとイメージが動くECS/Fargateがあります。*4

構築の手順

手順はdocs/setup_app.mdにまとめました。

VPC前々回前回で作成したAWS Batch環境のVPCを使うようにしました。

ECRに登録するイメージを作成する際に含めるAPV-MCTSのモデルファイルはS3にある一番タイムスタンプが最近のファイルにしました。

buildspec_app.yml:L8-13

      - cd data
      - aws s3 sync s3://${MODEL_BUCKET}/data/ ./
      - MODEL_FILE=$(ls -lt model_*.dat | head -n 1 | awk '{print $9}')
      - echo $MODEL_FILE
      - mv $MODEL_FILE model.dat
      - rm model_*.dat

ECS/Fargateのオセロにアクセスする

コンソールの下記からPublic IPが確認できます。

ECS>クラスター>タスク>詳細>Network>Public IP

Public IPがわかればブラウザからhttp://Public IP:5000/でアクセスすることができます。*5

*1:当初はノウハウのあるServerless FrameworkでAPI化してSPAにする構想でしたがChainerなどの外部ライブラリがでかすぎてLambdaの250MB制限を回避できなさそうだったためECSにピボットしました

*2:ランダム以外は重くて実用に耐えられなさそうですが...

*3:手抜きでバリデーションはありませんが

*4:ECS/Fargateへのイメージのデプロイは未対応です

*5:インフラはだいぶ手抜き気味ですmm

AWS Batch環境でのAPV-MCTSのセルフプレイによる強化学習をGPU化してみた

前回の記事で作成したAPV-MCTSのセルフプレイによる強化学習のAWS Batch環境GPU化して動かしてみました。

今回のコード

下記、タグv0.0.2になります。

github.com

環境の概要

全体構成は下記です。

f:id:nihma:20190209234629p:plain

前回の構成から下記の変更を行いました。

  • nvidia-docker2に対応したAMIの作成を行うCodeBuildを追加
  • セルフプレイの新モデル作成時のトレーニングをGPUに対応
  • AWS Batchのコンピューティング環境で使うAMIとインスタンスを変更

GPU化の実装

具体的な実装内容です。ちなみに環境作成手順はbashからマークダウン形式に変更しdocs/setup_batch.mdに移動しました。

nvidia-docker2に対応したAMIの作成を行うCodeBuildを追加

Packerを使ってDeep Learning AMI(ami-08a7740ff4d3fd90f)をベースにnvidia-docker2に対応したAMIを作成するCodeBuildを追加しました。

CodeBuildでPackerを使ってのAMIの作成方法は下記を参考にしました。

aws.amazon.com

nvidia-docker2に対応したAMIの作成方法は下記を参考にしました。

docs.aws.amazon.com

ただ、Deep Learning AMI/usr/local/cuda/version.txtが複数行になっていたのでconfigure-gpu.shGet CUDA versioncathead -n 1に変更しました。

以下、コードです。

CodeBuildの作成手順は下記です。Packerで必要になるAMI作成場所のサブネットID*1などを環境変数で渡すように指定しています。

setup_batch.md

cat <<EOF > Source.json
{
  "type": "CODECOMMIT",
  "location": "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/heta-reversi",
  "buildspec": "buildspec_ami.yml"
}
EOF
cat <<EOF > Artifacts.json
{
  "type": "NO_ARTIFACTS"
}
EOF
cat <<EOF > Environment.json
{
  "type": "LINUX_CONTAINER",
  "image": "aws/codebuild/docker:18.09.0",
  "computeType": "BUILD_GENERAL1_SMALL",
  "environmentVariables": [
    {
      "name": "AWS_REGION",
      "value": "ap-northeast-1",
      "type": "PLAINTEXT"
    },
    {
      "name": "AWS_SUBNET_ID",
      "value": "${SUBNET_ID}",
      "type": "PLAINTEXT"
    }
  ]
}
EOF
aws codebuild create-project --name test-batch-ami \
                               --source file://Source.json \
                               --artifacts file://Artifacts.json \
                               --environment file://Environment.json \
                               --service-role ${ROLE_AMI_BUILD_ARN}
CODEBUILD_ID=$(aws codebuild start-build --project-name test-batch-ami --source-version ${BRANCH} | tr -d "\n" | jq -r '.build.id')
echo "started.. id is ${CODEBUILD_ID}"
while true
do
  sleep 10s
  STATUS=$(aws codebuild batch-get-builds --ids "${CODEBUILD_ID}" | tr -d "\n" | jq -r '.builds[].buildStatus')
  echo "..status is ${STATUS}."
  if [ "${STATUS}" != "IN_PROGRESS" ]; then
    if [ "${STATUS}" != "SUCCEEDED" ]; then
      echo "faild."
    fi
    echo "done."
    break
  fi
done
AMI_IDS=$(aws ec2 describe-images \
    --owner self \
    --filters "Name=tag:USE,Values=heta-reversi" | jq -r ".Images[].ImageId")
AMI_ID="${AMI_IDS[0]}"

CodeBuildで実施するAMI作成処理は下記です。今回はAMIが1つしかない前提でのAMI ID取得*2になっているためAMI作成前に過去に作成済みのAMIを削除するようにしてあります。*3

buildspec_ami.yml

version: 0.2

phases:
  pre_build:
    commands:
      - curl -qL -o packer.zip https://releases.hashicorp.com/packer/1.3.3/packer_1.3.3_linux_amd64.zip && unzip packer.zip
      - ./packer validate amazon-linux_packer-template.json
  build:
    commands:
      - ./delete_amis.sh
      - ./packer build amazon-linux_packer-template.json
  post_build:
    commands:
      - echo "done $(date)"

過去のAMIを削除する処理は下記です。今回の作成AMIにはタグUSEheta-reversiを付与するようにしてあるため削除の目印にしてあります。

delete_amis.sh

#!/bin/bash

aws ec2 describe-images \
    --owner self \
    --filters "Name=tag:USE,Values=heta-reversi" | jq -r ".Images[].ImageId" | while read -r AMI_ID
do
  aws ec2 deregister-image --image-id ${AMI_ID}
done

AMIを作成するPackerのTemplateは下記です。インスタンスタイプはGPUインスタンスp2.xlargeを使うようにしました。

amazon-linux_packer-template.json

{
    "variables": {
        "aws_region": "{{env `AWS_REGION`}}",
        "aws_subnet_id": "{{env `AWS_SUBNET_ID`}}",
        "aws_ami_name": "amazon-linux_heta-reversi"
    },

    "builders": [{
        "type": "amazon-ebs",
        "region": "{{user `aws_region`}}",
        "instance_type": "p2.xlarge",
        "subnet_id": "{{user `aws_subnet_id`}}",
        "ssh_username": "ec2-user",
        "ami_name": "{{user `aws_ami_name`}}",
        "ami_description": "heta-reversi's Amazon Linux",
        "source_ami": "ami-08a7740ff4d3fd90f",
        "ssh_pty": true,
        "tags": {
            "USE": "heta-reversi"
        }
    }],

    "provisioners": [
        {
            "type": "file",
            "source": "configure-gpu.sh",
            "destination": "/home/ec2-user/configure-gpu.sh"
        },
        {
            "type": "shell",
            "inline": [
                "bash ./configure-gpu.sh",
                "sudo rm -rf /var/lib/ecs/data/ecs_agent_data.json"
            ]
        }
    ]
}

PackerのTemplateのprovisionersで実行するnvidia-docker2の設定処理は下記です。dockerのdefault-runtimeにnvidiaを指定してnvidia-docker2が有効になるようにしています。

configure-gpu.sh

#!/bin/bash

# Install ecs-init, start docker, and install nvidia-docker 2
sudo yum install -y ecs-init
sudo service docker start
DOCKER_VERSION=$(docker -v | awk '{ print $3 }' | cut -f1 -d"-")
DISTRIBUTION=$(. /etc/os-release;echo ${ID$VERSION_ID})
curl -s -L https://nvidia.github.io/nvidia-docker/${DISTRIBUTION}/nvidia-docker.repo | \
  sudo tee /etc/yum.repos.d/nvidia-docker.repo
PACKAGES=$(sudo yum search -y --showduplicates nvidia-docker2 nvidia-container-runtime | grep ${DOCKER_VERSION} | awk '{ print $1 }')
sudo yum install -y ${PACKAGES}
sudo pkill -SIGHUP dockerd

# Get CUDA version
CUDA_VERSION=$(head -n 1 /usr/local/cuda/version.txt | awk '{ print $3 }' | cut -f1-2 -d".")

# Run test container to verify installation
sudo docker run --privileged --runtime=nvidia --rm nvidia/cuda:${CUDA_VERSION}-base nvidia-smi

# Update Docker daemon.json to user nvidia-container-runtime by default
sudo tee /etc/docker/daemon.json <<EOF
{
    "runtimes": {
        "nvidia": {
            "path": "/usr/bin/nvidia-container-runtime",
            "runtimeArgs": []
        }
    },
    "default-runtime": "nvidia"
}
EOF

sudo service docker restart

セルフプレイの新モデル作成時のトレーニングをGPUに対応

現在のモデルをベースにセルフプレイで生成した棋譜データを使って学習している部分をGPUで行うようにしました。変更を行なったらCodeBuildを実行してECRのdocker imageを更新します。

以下、コードです。

まずGPUを使う箇所を局所化するためにgpu_deviceをグローバル定義しました。*4この値を目印にGPU処理にするか判断します。

main.py: L26

gpu_device = -1

学習を行う_create_new_modelGPU化します。学習の実施前後でモデルをto_gpuGPUモードにしてto_cpuでCPUモードに戻しています。

main.py: L549-L573

    def _create_new_model(self, steps_list, epoch_num = None, batch_size = 2048):
        epoch_num = default_params['dual_net_trainer_create_new_model_epoch_num'] if epoch_num is None else epoch_num

        model = DualNet()
        model.load(self.model_filename)

        if gpu_device >= 0:
            cuda.get_device(gpu_device).use()
            model.to_gpu(gpu_device)

        optimizer = chainer.optimizers.Adam()
        optimizer.setup(model)
        for i in range(epoch_num):
            x_train, y_train_policy, y_train_value = self._get_train_batch(steps_list, batch_size)
            y_policy, y_value = model(x_train)
            model.cleargrads()
            loss = F.mean_squared_error(y_policy, y_train_policy) + F.mean_squared_error(y_value, y_train_value)
            loss.backward()
            optimizer.update()
            print("[new nodel] epoch: {} / {}, loss: {}".format(i + 1, epoch_num, loss))

        if gpu_device >= 0:
            model.to_cpu()

        return model

棋譜データから学習データを取り出す_get_train_batchです。GPUモードの場合はnumpyでは無くcuda.cupyを使う必要があります。

main.py: L532-L547

    def _get_train_batch(self, steps_list, batch_size):
        batch_x, batch_y_policy, batch_y_value = [], [], []
        for _ in range(batch_size):
            x, y_policy, y_value = self._get_train_random(steps_list)
            batch_x.append(x)
            batch_y_policy.append(y_policy)
            batch_y_value.append(y_value)

        xp = np
        if gpu_device >= 0:
            xp = cuda.cupy

        x_train        = Variable(xp.array(batch_x)).reshape(-1, 4, 8, 8)
        y_train_policy = Variable(xp.array(batch_y_policy)).reshape(-1, 64)
        y_train_value  = Variable(xp.array(batch_y_value)).reshape(-1, 1)
        return x_train, y_train_policy, y_train_value

最後にcreate-model-batchで動かす場合にGPUモードにしています。

main.py: L729-L745

    elif len(args) > 2 and args[1] == 'create-model-batch':
### ...(省略)...
        gpu_device = 0
### ...(省略)...

AWS Batchのコンピューティング環境で使うAMIとインスタンスを変更

AWS Batchで今回のAMIをGPUインスタンスを使うようにしました。

以下、コードです。

コンピューティング環境の定義のimageIdに今回のAMIをinstanceTypesGPUインスタンスを指定します。今回はp2.xlargeにしました。

setup_batch.md

cat << EOF > compute-environment.spec.json
{
    "computeEnvironmentName": "test-compute-environment",
    "type": "MANAGED",
    "state": "ENABLED",
    "computeResources": {
        "type": "EC2",
        "minvCpus": 0,
        "maxvCpus": 4,
        "desiredvCpus": 0,
        "instanceTypes": [
          "p2.xlarge"
        ],
        "imageId": "${AMI_ID}",
        "subnets": ["${SUBNET_ID}"],
        "securityGroupIds": ["${DEFAULT_SECURITY_GROUP_ID}"],
        "instanceRole": "${INSTANCE_ROLE_ARN}"
    },
    "serviceRole": "${ROLE_SERVICE_ARN}"
}
EOF
COMPUTE_ENV=$(aws batch create-compute-environment --cli-input-json file://compute-environment.spec.json)

動かした結果

結論は前回と同じで4時間かかり学習時間が短縮することはありませんでした。

GPUが有効に動いていないことが原因ではないかと今回のAMI/インスタンス/docker imageでMNISTのサンプルを動かしてみましたがきちんと動作しているようです。GPUは有効だと思われます。

ChainerでGPUを使ってみよう - TIM Labs

chainer/examples/mnist at v5 · chainer/chainer · GitHub

バグがある可能性や今回のケースではあまりGPUの効果がない可能性、バッチサイズを増やしたりパラメータを変更したら効果がある可能性、など疑っていますが調査中です... 🤔

まとめ

今回はAPV-MCTSのセルフプレイによる強化学習のAWS Batch環境GPU化してみました。残念ながら学習時間は前回と同じで短縮できませんでしたがPackerの使い方を知ることができてGPU環境も作ることができました。引き続き調査と流石にそろそろコードが限界なのでリファクタリングをしたいです。

*1:今回はAWS Batchが動くサブネットと同じにしました

*2:${AMI_IDS[0]}

*3:過去のAMIを残すようにする場合はAMIの名前が被らないようにする対応も必要と思います

*4:今回はお試し前提、リファクタあとでやる前提でグローバル変数にしました。。

AWS Batch環境を作ってAPV-MCTSのセルフプレイによる強化学習を動かしてみた

以前の記事で作成したAlphaZeroもどきオセロのAPV-MCTSのセルフプレイによる強化学習AWS Batchで動かしてみました。

今回のコード

下記、タグv0.0.1になります。

github.com

環境の概要

下記の2つの環境があります。

  • CodeBuildheta-reversiのdocker imageを作ってECRに登録する
  • AWS Batch:EC2インスタンスを作成しECRのdocker imageを起動しセルフプレイを実行して結果をS3バケットに格納する

全体構成は下記です。

f:id:nihma:20190126205303p:plain

環境構築の手順

setup_batch.shに記載しました。

大まかに下記の3つの構築を行います。

  • VPC環境
  • ECR関連
  • AWS Batch関連

VPC環境

AWS BatchがEC2インスタンスを作成するためのVPC環境を構築します。

  • VPC
  • Internet Gateway ... AWS Batchから利用するためSubnetをパブリックにするために必要
  • Subnet ... AWS Batchから利用するためEC2インスタンスへのIP自動割り当て設定が必要
  • Route Table
  • Security Group

コードです。

setup_batch.sh: L13-L63

## Create VPC
VPC=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16)
VPC_ID=$(echo ${VPC} | jq -r ".Vpc.VpcId")
aws ec2 create-tags \
    --resources ${VPC_ID} \
    --tags Key=Name,Value=test-batch

## Create Internet Gateway
INTERNET_GATEWAY=$(aws ec2 create-internet-gateway)
INTERNET_GATEWAY_ID=$( \
    echo ${INTERNET_GATEWAY} | jq -r ".InternetGateway.InternetGatewayId")
aws ec2 create-tags \
    --resources ${INTERNET_GATEWAY_ID} \
    --tags Key=Name,Value=test-batch
aws ec2 attach-internet-gateway \
    --internet-gateway-id ${INTERNET_GATEWAY_ID} \
    --vpc-id ${VPC_ID}

## Create Subnet
SUBNET=$( \
    aws ec2 create-subnet \
        --vpc-id ${VPC_ID} \
        --cidr-block 10.0.0.0/24 \
        --availability-zone ap-northeast-1a)
SUBNET_ID=$(echo ${SUBNET} | jq -r ".Subnet.SubnetId")
aws ec2 create-tags \
    --resources ${SUBNET_ID} \
    --tags Key=Name,Value=test-batch
aws ec2 modify-subnet-attribute --subnet-id ${SUBNET_ID} --map-public-ip-on-launch

## Create Route Table
ROUTE_TABLE_ID=$( \
    aws ec2 describe-route-tables \
        --filters Name=vpc-id,Values=${VPC_ID} \
             | jq -r ".RouteTables[].RouteTableId")
aws ec2 create-tags \
    --resources ${ROUTE_TABLE_ID} \
    --tags Key=Name,Value=test-batch
aws ec2 create-route \
    --route-table-id ${ROUTE_TABLE_ID} \
    --destination-cidr-block 0.0.0.0/0 \
    --gateway-id ${INTERNET_GATEWAY_ID}
aws ec2 associate-route-table \
    --route-table-id ${ROUTE_TABLE_ID} \
    --subnet-id ${SUBNET_ID}

## Security Group
DEFAULT_SECURITY_GROUP_ID=$( \
    aws ec2 describe-security-groups \
        --filters Name=group-name,Values=default Name=vpc-id,Values=${VPC_ID} \
            | jq -r '.SecurityGroups[].GroupId')

ECR関連

下記を構築しCodeBuildを実行してECRにdocker imageを登録します。

  • S3 ... セルフプレイ結果を格納するバケット
  • ECR repository
  • IAM ecr build role
  • CodeBuild ecr

コードです。

setup_batch.sh: L65-L155

## Create S3
aws s3 mb s3://test-batch-bucket-name --region ap-northeast-1

## Create ECR repository
ECR_REPO_NAME="test-batch"
ECR_REPO=$(aws ecr create-repository --repository-name ${ECR_REPO_NAME})
ECR_REPO_URL=$(echo ${ECR_REPO} | jq -r ".repository.repositoryUri")

## Create IAM ecr build role
cat <<EOF > Trust-Policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "codebuild.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
ROLE_ECR_BUILD=$(aws iam create-role --role-name test-batch-build-ecr \
                                     --assume-role-policy-document file://Trust-Policy.json)
aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
                           --role-name test-batch-build-ecr
ROLE_ECR_BUILD_ARN=$(echo ${ROLE_ECR_BUILD} | jq -r ".Role.Arn")

## Create CodeBuild ecr
cat <<EOF > Source.json
{
  "type": "CODECOMMIT",
  "location": "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/heta-reversi",
  "buildspec": "buildspec_ecr.yml"
}
EOF
cat <<EOF > Artifacts.json
{
  "type": "NO_ARTIFACTS"
}
EOF
cat <<EOF > Environment.json
{
  "type": "LINUX_CONTAINER",
  "image": "aws/codebuild/docker:18.09.0",
  "computeType": "BUILD_GENERAL1_SMALL",
  "environmentVariables": [
    {
      "name": "REGION",
      "value": "ap-northeast-1",
      "type": "PLAINTEXT"
    },
    {
      "name": "IMAGE_REPO_NAME",
      "value": "example",
      "type": "PLAINTEXT"
    },
    {
      "name": "IMAGE_TAG",
      "value": "latest",
      "type": "PLAINTEXT"
    },
    {
      "name": "ECR_REPO_URL",
      "value": "${ECR_REPO_URL}",
      "type": "PLAINTEXT"
    }
  ]
}
EOF
aws codebuild create-project --name test-batch-ecr \
                               --source file://Source.json \
                               --artifacts file://Artifacts.json \
                               --environment file://Environment.json \
                               --service-role ${ROLE_ECR_BUILD_ARN}
CODEBUILD_ID=$(aws codebuild start-build --project-name test-batch-ecr --source-version master | tr -d "\n" | jq -r '.build.id')
echo "started.. id is ${CODEBUILD_ID}"
while true
do
  sleep 10s
  STATUS=$(aws codebuild batch-get-builds --ids "${CODEBUILD_ID}" | tr -d "\n" | jq -r '.builds[].buildStatus')
  echo "..status is ${STATUS}."
  if [ "${STATUS}" != "IN_PROGRESS" ]; then
    if [ "${STATUS}" != "SUCCEEDED" ]; then
      echo "faild."
    fi
    echo "done."
    break
  fi
done

AWS Batch関連

AWS Batch環境です。

  • Instance role
  • IAM ecr service role
  • Batch compute environment ... 今回はcpuを0〜4にしてみました
  • Batch job queue
  • IAM job role
  • job definition ... 今回はcpuを4、memoryを2000にしてみました

setup_batch.sh: L157-L264

## Create Instance role
cat <<EOF > Trust-Policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
aws iam create-role --role-name test-batch-instance \
                    --assume-role-policy-document file://Trust-Policy.json
aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role \
                           --role-name test-batch-instance
INSTANCE_ROLE=$(aws iam create-instance-profile --instance-profile-name test-batch-instance)
INSTANCE_ROLE_ARN=$(echo ${INSTANCE_ROLE} | jq -r ".InstanceProfile.Arn")
aws iam add-role-to-instance-profile --role-name test-batch-instance --instance-profile-name test-batch-instance

## Create IAM ecr service role
cat <<EOF > Trust-Policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "batch.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
ROLE_SERVICE=$(aws iam create-role --role-name test-batch-service \
                                   --assume-role-policy-document file://Trust-Policy.json)
aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole \
                           --role-name test-batch-service
ROLE_SERVICE_ARN=$(echo ${ROLE_SERVICE} |jq -r ".Role.Arn")

## Create Batch compute environment
cat << EOF > compute-environment.spec.json
{
    "computeEnvironmentName": "test-compute-environment",
    "type": "MANAGED",
    "state": "ENABLED",
    "computeResources": {
        "type": "EC2",
        "minvCpus": 0,
        "maxvCpus": 4,
        "desiredvCpus": 0,
        "instanceTypes": ["optimal"],
        "subnets": ["${SUBNET_ID}"],
        "securityGroupIds": ["${DEFAULT_SECURITY_GROUP_ID}"],
        "instanceRole": "${INSTANCE_ROLE_ARN}"
    },
    "serviceRole": "${ROLE_SERVICE_ARN}"
}
EOF
COMPUTE_ENV=$(aws batch create-compute-environment --cli-input-json file://compute-environment.spec.json)
COMPUTE_ENV_ARN=$(echo ${COMPUTE_ENV} | jq -r '.computeEnvironmentArn')

## Create Batch job queue
JOB_QUEUE=$(aws batch create-job-queue \
  --job-queue-name test-job-queue \
  --priority 1 \
  --compute-environment-order order=1,computeEnvironment=${COMPUTE_ENV_ARN})
JOB_QUEUE_ARN=$(echo ${JOB_QUEUE} | jq -r '.jobQueueArn')

## Create IAM job role
cat <<EOF > Trust-Policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
ROLE_JOB=$(aws iam create-role --role-name test-batch-job \
                               --assume-role-policy-document file://Trust-Policy.json)
aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
                           --role-name test-batch-job
ROLE_JOB_ARN=$(echo ${ROLE_JOB} | jq -r ".Role.Arn")

## Create job definition
cat << EOF > job-definition.spec.json
{
  "image": "${ECR_REPO_URL}",
  "vcpus": 4,
  "memory": 2000,
  "jobRoleArn": "${ROLE_JOB_ARN}"
}
EOF
JOB_DEF=$(aws batch register-job-definition \
  --job-definition-name test-job-definition \
  --type container \
  --container-properties file://job-definition.spec.json)
JOB_DEF_ARN=$(echo ${JOB_DEF} | jq -r '.jobDefinitionArn')

セルフプレイ方法

こちらもsetup_batch.shに記載しました。

JOBを登録してステータスがSUCCEEDEDになったらS3バケットから結果ファイルを取り出します。

setup_batch.sh: L266-L278

## Submit job
JOB=$(aws batch submit-job \
    --job-name "test-job" \
    --job-queue "${JOB_QUEUE_ARN}" \
    --job-definition "${JOB_DEF_ARN}")
JOB_ID=$(echo ${JOB} | jq -r ".jobId")

## Show job status
aws batch describe-jobs --jobs ${JOB_ID} | jq -r ".jobs[].status"

## Get model file
aws s3 ls test-batch-bucket-name/data/
aws s3 sync s3://test-batch-bucket-name/data .

まとめ

今回はAPV-MCTSのセルフプレイによる強化学習を動かすAWS Batch環境を作ってみました。

ただ、マシンパワーが足りず学習が終わりそうにないため今回は下記のように暫定でパラメータをかなり小さな値にしました。*1これでもc4.xlargeで4時間かかりました。

main.py: L16-L24

# FIXME: Stop global variables by modularizing
default_params = {
    'choice_asynchronous_policy_and_value_monte_carlo_tree_search_try_num': 1500,
    'dual_net_trainer_self_play_try_num':                                   2500,
    'dual_net_trainer_create_new_model_epoch_num':                          100,
    'dual_net_trainer_evaluation_try_num':                                  400,
    'dual_net_trainer_evaluation_win_num':                                  220,
    'dual_net_trainer_try_num':                                             100
}

main.py: L713-L722

    elif len(args) > 2 and args[1] == 'create-model-batch':
        bucket_name = args[2]

        # FIXME: Stop global variables by modularizing
        default_params['choice_asynchronous_policy_and_value_monte_carlo_tree_search_try_num'] = 150
        default_params['dual_net_trainer_self_play_try_num']                                   = 25
        default_params['dual_net_trainer_create_new_model_epoch_num']                          = 10
        default_params['dual_net_trainer_evaluation_try_num']                                  = 40
        default_params['dual_net_trainer_evaluation_win_num']                                  = 22
        default_params['dual_net_trainer_try_num']                                             = 1

元のパラメータにて現実的な時間で学習できるようにするためには、nvidia-docker2とGPUインスタンスGPU計算化したり、複数インスタンスを使った分散化したり、など改善の余地があると思います。

*1:リファクタリングしないと...