Azure で Docker Cluster を構成するさまざまな方法

ここでは、Azure を使ってマルチ インスタンスや Cluster を構成するさまざまな方法をまとめたい。(Swarm, Mesos, Kubernetes 自体の構成方法の話ではなく、Azure に構成する際の考え方。)
いろいろと使える情報 (サンプルの構成ファイルなど) もあるので、それらの紹介 (リンク) もあわせて以下に記載する。

Azure + Docker の超基本

クラスタの話をする前に、まず、Azure で Docker を使う場合の超基本的な話を整理しておく。(知らない人のために)

Azure 上で Docker を使う場合、ざっくり下記の 2 つが使える。
(この他に、Docker 社が提供している Docker Cloud、Mesosphere が提供している DCOS でも、Azure を正式サポートしているが、これらについては本投稿の対象外とする。)

  • 普通に Azure Virtual Machine (IaaS, VM) を立ち上げて Container を立てる方法
  • Azure Container Service (ACS) と呼ばれるコンテナのホスティングを管理するサービス型の Azure リソースを使って Container 作成と管理をおこなう方法。
    やや乱暴な説明だが、AWS, Google を使っている人は、Amazon EC2 Container Service (ECS), Google Container Engine (GCE) と同じ位置づけのいわゆる「サービス」型のホスティング環境と思えば良い。(内部的な話は後述)

前者の VM (Azure Virtual Machine) を使うケースについて、Azure Virtual Machine (IaaS 環境) に docker container を構成するには、直球で Virtual Machine に docker のダウンロード・インストールをおこなっても良いが、Azure の場合は、Virtual Machine Extension と呼ばれる「VM に対する追加の構成要素 (リソースの 1 つ)」を使って docker のインストールと構成が可能だ。(よって、Azure 上で Docker を扱う場合、この “Extension” という言葉は良く出てくることになる。)
この構成をおこなうには、大きく下記の 3 通りの方法がある。

まずもっとも一般的な方法として、Azure CLI (Command-line Interface) のコマンド ユーティリティ (Azure 専用のコマンド ユーティリティ) を使う方法がある。(Azure CLI の azure vm docker create コマンドですぐに構成できる。)

Azure CLI を使って Docker VM Extension を構成する方法 :
https://azure.microsoft.com/en-us/documentation/articles/virtual-machines-docker-with-xplat-cli/

VM + Docker の 2 番目の方法として、ARM (Azure Resource Manager) の Resource template と呼ばれる展開用の json ファイルを使って、Virtual Machine Extension を (json で) 記述する方法がある。
VM Extension にはさまざまな種類があるが、その 1 つとして Docker Extension と呼ばれるものがあるので、これを使えば簡単に構成できるようになっている。(なお、sh を実行するための VM Extension も用意されているので、この sh を使って Docker を構成する方法もある。)
展開をコード化して自動化したい場合にはすごく使える方法なので、上記 (コマンド ユーティリティ) で展開がうまくいったら、さいごに ARM の template としてコード化してみると良いだろう。(なお、このあと紹介する template を見る前に、最低限、ここで紹介している How-to 程度は理解できるようにしておいてほしい。)

Docker を構成した resource template の例 :
(下記の azuredeploy.json が開始点だ。以降も同様。。。)
https://github.com/Azure/azure-quickstart-templates/tree/master/docker-simple-on-ubuntu

抜粋すると以下のような感じだ。(ハイライト部分が DockerExtension)

. . .

{
  "apiVersion": "2015-06-15",
  "type": "Microsoft.Compute/virtualMachines",
  "name": "vmtest01",
  "location": "East US",
  . . . (途中省略)
},
{
  "type": "Microsoft.Compute/virtualMachines/extensions",
  "name": "testvm01/dockerex01",
  "apiVersion": "2015-06-15",
  "location": "East US",
  "dependsOn": [
    "Microsoft.Compute/virtualMachines/vmtest01"
  ],
  "properties": {
    "publisher": "Microsoft.Azure.Extensions",
    "type": "DockerExtension",
    "typeHandlerVersion": "1.0",
    "autoUpgradeMinorVersion": true,
    "settings": {}
  }
}

. . .

VM + Docker のおまけとして、docker 標準の client でも構成可能だ。
Azure の Docker サポートは MS が勝手に対応しているのではなく、Azure と Docker の双方が対応している形で、docker driver の 1 つとして azure がサポートされているからだ。上述の ARM のような “Azure かぶれ” な方法ではなく、オープンなツールやフレームワークなどから呼び出す場合は、この方法が使えるだろう。
(この具体的方法はここでは省略するが、下記ブログを参照してほしい。)

Docker client のみを使って Azure 上の Docker container を作成する例 :
https://blogs.msdn.microsoft.com/tsmatsuz/2015/10/08/azure-docker-machine-on-linux-from-pc-mac/

また、Azure Container Service (ACS) については、本投稿でテーマとしている「Cluster」の話と大きく関連しているので後述する。

Compose

まずは、IaaS v2 (Azure Virtual Machine) を使った構成を中心に整理しよう。

最初に、クラスタ (Cluster) ではなく、単なるマルチインスタンス (複数台) 構成の話だが、Azure では docker compose も柔軟に扱えるようになっている。

Azure の ARM (Azure Resource Manager) template の場合は、上述の Docker Extension で Compose の記述がサポートされていて、下記の通り ARM template の json に混ぜて構成が可能だ。(なお、コマンドを使った場合は「Azure VM Docker Compose Quickstart」で記載しているように普通に Compose を構成するだけ。)

Docker Compose を使って WordPress と MySQL を構成する template の例 :
https://github.com/Azure/azure-quickstart-templates/tree/master/docker-wordpress-mysql

抜粋すると以下のような感じだ。

. . .
{
  "apiVersion": "2015-06-15",
  "type": "Microsoft.Compute/virtualMachines",
  "name": "testvm01",
  "location": "East US",
  . . .
},
{
  "type": "Microsoft.Compute/virtualMachines/extensions",
  "name": "testvm01/dockerex01",
  "apiVersion": "2015-06-15",
  "location": "East US",
  "dependsOn": [
    "Microsoft.Compute/virtualMachines/rtestvm01"
  ],
  "properties": {
    "publisher": "Microsoft.Azure.Extensions",
    "type": "DockerExtension",
    "typeHandlerVersion": "1.0",
    "autoUpgradeMinorVersion": true,
    . . .
    "settings": {
      "compose": {
        "db": {
          "image": "mysql",
          "ports": [
            "3306:3306"
          ],
          "volumes": [
            "/var/lib/mysql:/var/lib/mysql"
          ],
          "environment": [
            "MYSQL_ROOT_PASSWORD"
          ]
        },
        "wordpress": {
          "image": "wordpress",
          "ports": [
            "80:80"
          ],
          "links": [
            "db:mysql"
          ]
        }
      }
    }
  }
}
. . .

Docker Compose の scale コマンドで、起動する container の数も柔軟に制御できる。

分散クラスタ

Production を考える場合、単に複数のコンテナが起動していれば良いというわけではない。
複数の物理マシンに点在する複数のコンテナを柔軟に管理するために、Swarm、Mesos などのオーケストレーション エンジンを使うのが一般的だ。

Azure VM (IaaS v2) を使って Docker + Swarm, Mesos, Marathon 等で分散クラスタを構成する場合は、専用の Extension はないので、上述の Compose を使うか、sh の実行をおこなうことになる。(コマンドの場合の例は「Azure Virtual Machine : How to use docker with swarm」参照。)

例えば、下記は、ARM (Azure Resource Manager) template で、上述した Compose を使って Swarm をインストールしている例だ。

Docker Compose と使って Swarm を構成する template の例 :
https://github.com/Azure/azure-quickstart-templates/tree/master/docker-swarm-cluster

Azure VM Extension には Linux の sh を実行する Extension (CustomScriptForLinux Extension) もあるので、Compose を使わずにすべて sh だけで構成する方法もある。
例えば、下記は mesos, zookeeper 等も含む構成を sh で記述しているが (適宜パラメータ化をおこなっている)、こうした sh を CustomScriptForLinux Extension を使って実行すれば良い。(下記では docker 自体も sh でインストールしている。)

#!/bin/bash

###########################################################
# Configure Mesos One Box
#
# This installs the following components
# - zookeepr
# - mesos master
# - marathon
# - mesos agent
###########################################################

set -x

echo "starting mesos cluster configuration"
date
ps ax

SWARM_VERSION="1.0.0-rc2"

#############
# Parameters
#############

MASTERCOUNT=$1
MASTERMODE=$2
MASTERPREFIX=$3
SWARMENABLED=$4
MARATHONENABLED=$5
CHRONOSENABLED=$6
ACCOUNTNAME=$7
set +x
ACCOUNTKEY=$8
set -x
AZUREUSER=$9
SSHKEY=${10}
HOMEDIR="/home/$AZUREUSER"
VMNAME=`hostname`
VMNUMBER=`echo $VMNAME | sed 's/.*[^0-9]\([0-9]\+\)*$/\1/'`
VMPREFIX=`echo $VMNAME | sed 's/\(.*[^0-9]\)*[0-9]\+$/\1/'`

echo "Master Count: $MASTERCOUNT"
echo "Master Mode: $MASTERMODE"
echo "Master Prefix: $MASTERPREFIX"
echo "vmname: $VMNAME"
echo "VMNUMBER: $VMNUMBER, VMPREFIX: $VMPREFIX"
echo "SWARMENABLED: $SWARMENABLED, MARATHONENABLED: $MARATHONENABLED, CHRONOSENABLED: $CHRONOSENABLED"
echo "ACCOUNTNAME: $ACCOUNTNAME"

###################
# setup ssh access
###################

SSHDIR=$HOMEDIR/.ssh
AUTHFILE=$SSHDIR/authorized_keys
if [ `echo $SSHKEY | sed 's/^\(ssh-rsa \).*/\1/'` == "ssh-rsa" ] ; then
  if [ ! -d $SSHDIR ] ; then
    sudo -i -u $AZUREUSER mkdir $SSHDIR
    sudo -i -u $AZUREUSER chmod 700 $SSHDIR
  fi

  if [ ! -e $AUTHFILE ] ; then
    sudo -i -u $AZUREUSER touch $AUTHFILE
    sudo -i -u $AZUREUSER chmod 600 $AUTHFILE
  fi
  echo $SSHKEY | sudo -i -u $AZUREUSER tee -a $AUTHFILE
else
  echo "no valid key data"
fi

###################
# Common Functions
###################

ensureAzureNetwork()
{
  # ensure the host name is resolvable
  hostResolveHealthy=1
  for i in {1..120}; do
    host $VMNAME
    if [ $? -eq 0 ]
    then
      # hostname has been found continue
      hostResolveHealthy=0
      echo "the host name resolves"
      break
    fi
    sleep 1
  done
  if [ $hostResolveHealthy -ne 0 ]
  then
    echo "host name does not resolve, aborting install"
    exit 1
  fi

  # ensure the network works
  networkHealthy=1
  for i in {1..12}; do
    wget -O/dev/null http://bing.com
    if [ $? -eq 0 ]
    then
      # hostname has been found continue
      networkHealthy=0
      echo "the network is healthy"
      break
    fi
    sleep 10
  done
  if [ $networkHealthy -ne 0 ]
  then
    echo "the network is not healthy, aborting install"
    ifconfig
    ip a
    exit 2
  fi
  # ensure the host ip can resolve
  networkHealthy=1
  for i in {1..120}; do
    hostname -i
    if [ $? -eq 0 ]
    then
      # hostname has been found continue
      networkHealthy=0
      echo "the network is healthy"
      break
    fi
    sleep 1
  done
  if [ $networkHealthy -ne 0 ]
  then
    echo "the network is not healthy, cannot resolve ip address, aborting install"
    ifconfig
    ip a
    exit 2
  fi
}
ensureAzureNetwork
HOSTADDR=`hostname -i`

ismaster ()
{
  if [ "$MASTERPREFIX" == "$VMPREFIX" ]
  then
    return 0
  else
    return 1
  fi
}
if ismaster ; then
  echo "this node is a master"
fi

isagent()
{
  if ismaster ; then
    if [ "$MASTERMODE" == "masters-are-agents" ]
    then
      return 0
    else
      return 1
    fi
  else
    return 0
  fi
}
if isagent ; then
  echo "this node is an agent"
fi

zkhosts()
{
  zkhosts=""
  for i in `seq 1 $MASTERCOUNT` ;
  do
    if [ "$i" -gt "1" ]
    then
      zkhosts="${zkhosts},"
    fi

    IPADDR=`getent hosts ${MASTERPREFIX}${i} | awk '{ print $1 }'`
    zkhosts="${zkhosts}${IPADDR}:2181"
    # due to mesos team experience ip addresses are chosen over dns names
    #zkhosts="${zkhosts}${MASTERPREFIX}${i}:2181"
  done
  echo $zkhosts
}

zkconfig()
{
  postfix="$1"
  zkhosts=$(zkhosts)
  zkconfigstr="zk://${zkhosts}/${postfix}"
  echo $zkconfigstr
}

######################
# resolve self in DNS
######################

echo "$HOSTADDR $VMNAME" | sudo tee -a /etc/hosts

################
# Install Docker
################

echo "Installing and configuring docker and swarm"

time wget -qO- https://get.docker.com | sh

# Start Docker and listen on :2375 (no auth, but in vnet)
echo 'DOCKER_OPTS="-H unix:///var/run/docker.sock -H 0.0.0.0:2375"' | sudo tee /etc/default/docker
# the following insecure registry is for OMS
echo 'DOCKER_OPTS="$DOCKER_OPTS --insecure-registry 137.135.93.9"' | sudo tee -a /etc/default/docker
sudo service docker restart

ensureDocker()
{
  # ensure that docker is healthy
  dockerHealthy=1
  for i in {1..3}; do
    sudo docker info
    if [ $? -eq 0 ]
    then
      # hostname has been found continue
      dockerHealthy=0
      echo "Docker is healthy"
      sudo docker ps -a
      break
    fi
    sleep 10
  done
  if [ $dockerHealthy -ne 0 ]
  then
    echo "Docker is not healthy"
  fi
}
ensureDocker

############
# setup OMS
############

if [ $ACCOUNTNAME != "none" ]
then
  set +x
  EPSTRING="DefaultEndpointsProtocol=https;AccountName=${ACCOUNTNAME};AccountKey=${ACCOUNTKEY}"
  docker run --restart=always -d 137.135.93.9/msdockeragentv3 http://${VMNAME}:2375 "${EPSTRING}"
  set -x
fi

##################
# Install Mesos
##################

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv E56151BF
DISTRO=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
CODENAME=$(lsb_release -cs)
echo "deb http://repos.mesosphere.io/${DISTRO} ${CODENAME} main" | sudo tee /etc/apt/sources.list.d/mesosphere.list
time sudo add-apt-repository -y ppa:openjdk-r/ppa
time sudo apt-get -y update
time sudo apt-get -y install openjdk-8-jre-headless
if ismaster ; then
  time sudo apt-get -y --force-yes install mesosphere
else
  time sudo apt-get -y --force-yes install mesos
fi

#########################
# Configure ZooKeeper
#########################

zkmesosconfig=$(zkconfig "mesos")
echo $zkmesosconfig | sudo tee /etc/mesos/zk

if ismaster ; then
  echo $VMNUMBER | sudo tee /etc/zookeeper/conf/myid
  for i in `seq 1 $MASTERCOUNT` ;
  do
    IPADDR=`getent hosts ${MASTERPREFIX}${i} | awk '{ print $1 }'`
    echo "server.${i}=${IPADDR}:2888:3888" | sudo tee -a /etc/zookeeper/conf/zoo.cfg
    # due to mesos team experience ip addresses are chosen over dns names
    #echo "server.${i}=${MASTERPREFIX}${i}:2888:3888" | sudo tee -a /etc/zookeeper/conf/zoo.cfg
  done
fi

#########################################
# Configure Mesos Master and Frameworks
#########################################
if ismaster ; then
  quorum=`expr $MASTERCOUNT / 2 + 1`
  echo $quorum | sudo tee /etc/mesos-master/quorum
  hostname -i | sudo tee /etc/mesos-master/ip
  hostname | sudo tee /etc/mesos-master/hostname
  echo 'Mesos Cluster on Microsoft Azure' | sudo tee /etc/mesos-master/cluster
fi

if ismaster  && [ "$MARATHONENABLED" == "true" ] ; then
  # setup marathon
  sudo mkdir -p /etc/marathon/conf
  sudo cp /etc/mesos-master/hostname /etc/marathon/conf
  sudo cp /etc/mesos/zk /etc/marathon/conf/master
  zkmarathonconfig=$(zkconfig "marathon")
  echo $zkmarathonconfig | sudo tee /etc/marathon/conf/zk
  # enable marathon to failover tasks to other nodes immediately
  echo 0 | sudo tee /etc/marathon/conf/failover_timeout
  #echo false | sudo tee /etc/marathon/conf/checkpoint
fi

#########################################
# Configure Mesos Master and Frameworks
#########################################
if ismaster ; then
  # Download and install mesos-dns
  sudo mkdir -p /usr/local/mesos-dns
  sudo wget https://github.com/mesosphere/mesos-dns/releases/download/v0.2.0/mesos-dns-v0.2.0-linux-amd64.tgz
  sudo tar zxvf mesos-dns-v0.2.0-linux-amd64.tgz
  sudo mv mesos-dns-v0.2.0-linux-amd64 /usr/local/mesos-dns/mesos-dns
  RESOLVER=`cat /etc/resolv.conf | grep nameserver | tail -n 1 | awk '{print $2}'`

  echo "
{
  \"zk\": \"zk://127.0.0.1:2181/mesos\",
  \"refreshSeconds\": 1,
  \"ttl\": 0,
  \"domain\": \"mesos\",
  \"port\": 53,
  \"timeout\": 1,
  \"listener\": \"0.0.0.0\",
  \"email\": \"root.mesos-dns.mesos\",
  \"resolvers\": [\"$RESOLVER\"]
}
" > mesos-dns.json
  sudo mv mesos-dns.json /usr/local/mesos-dns/mesos-dns.json

  echo "
description \"mesos dns\"
# Start just after the System-V jobs (rc) to ensure networking and zookeeper
# are started. This is as simple as possible to ensure compatibility with
# Ubuntu, Debian, CentOS, and RHEL distros. See:
# http://upstart.ubuntu.com/cookbook/#standard-idioms
start on stopped rc RUNLEVEL=[2345]
respawn
exec /usr/local/mesos-dns/mesos-dns -config /usr/local/mesos-dns/mesos-dns.json" > mesos-dns.conf
  sudo mv mesos-dns.conf /etc/init
  sudo service mesos-dns start
fi


#########################
# Configure Mesos Agent
#########################
if isagent ; then
  # Add docker containerizer
  echo "docker,mesos" | sudo tee /etc/mesos-slave/containerizers
  # Add resources configuration
  if ismaster ; then
    echo "ports:[1-21,23-4399,4401-5049,5052-8079,8081-32000]" | sudo tee /etc/mesos-slave/resources
  else
    echo "ports:[1-21,23-5050,5052-32000]" | sudo tee /etc/mesos-slave/resources
  fi
  hostname -i | sudo tee /etc/mesos-slave/ip
  hostname | sudo tee /etc/mesos-slave/hostname

  # Add mesos-dns IP addresses at the top of resolv.conf
  RESOLV_TMP=resolv.conf.temp
  rm -f $RESOLV_TMP
  for i in `seq $MASTERCOUNT` ; do
      echo nameserver `getent hosts ${MASTERPREFIX}${i} | awk '{ print $1 }'` >> $RESOLV_TMP
  done

  cat /etc/resolv.conf >> $RESOLV_TMP
  mv $RESOLV_TMP /etc/resolv.conf
fi

##############################################
# configure init rules restart all processes
##############################################

echo "(re)starting mesos and framework processes"
if ismaster ; then
  sudo service zookeeper restart
  sudo service mesos-master start
  if [ "$MARATHONENABLED" == "true" ] ; then
    sudo service marathon start
  fi
  if [ "$CHRONOSENABLED" == "true" ] ; then
    sudo service chronos start
  fi
else
  echo manual | sudo tee /etc/init/zookeeper.override
  sudo service zookeeper stop
  echo manual | sudo tee /etc/init/mesos-master.override
  sudo service mesos-master stop
fi

if isagent ; then
  echo "starting mesos-slave"
  sudo service mesos-slave start
  echo "completed starting mesos-slave with code $?"
else
  echo manual | sudo tee /etc/init/mesos-slave.override
  sudo service mesos-slave stop
fi

echo "processes after restarting mesos"
ps ax

# Run swarm manager container on port 2376 (no auth)
if ismaster && [ "$SWARMENABLED" == "true" ] ; then
  echo "starting docker swarm:$SWARM_VERSION"
  echo "sleep to give master time to come up"
  sleep 10
  echo sudo docker run -d -e SWARM_MESOS_USER=root \
      --restart=always \
      -p 2376:2375 -p 3375:3375 swarm:$SWARM_VERSION manage \
      -c mesos-experimental \
      --cluster-opt mesos.address=0.0.0.0 \
      --cluster-opt mesos.port=3375 $zkmesosconfig
  sudo docker run -d -e SWARM_MESOS_USER=root \
      --restart=always \
      -p 2376:2375 -p 3375:3375 swarm:$SWARM_VERSION manage \
      -c mesos-experimental \
      --cluster-opt mesos.address=0.0.0.0 \
      --cluster-opt mesos.port=3375 $zkmesosconfig
  sudo docker ps
  echo "completed starting docker swarm"
fi
echo "processes at end of script"
ps ax
echo "Finished installing and configuring docker and swarm"
date
echo "completed mesos cluster configuration"

Kubernetes

上記では技術計算等で扱う大規模分散構成のようなクラスタを想定したが、例えば、同一構成の展開・管理では Kubernetes クラスタと組み合わせた Scaling も組み合わせたくなるだろう。(通常、Azure Virtual Machine で Web や AP の負荷分散をおこなう場合には Azure 標準の Load Balancer リソースが使えるが、ここに Docker が入ってきた場合には話が変わってくる。)
この場合も上記と同じ話で、compose か sh で書いて展開することになる。

なお、残念ながら Azure Quickstart Templates にこのサンプルはあがっていないが、下記の Kubernetes の Github に Azure の場合の構成方法について書かれているので、これを参考にスクリプト化などしてほしい。

[Kubernetes on Azure] Start Guide :
https://github.com/kubernetes/kubernetes/blob/release-1.1/docs/getting-started-guides/coreos/azure/README.md

Azure Container Service (ACS)

※ 以降、2016/02/18 追記

冒頭で述べた Azure Container Service (ACS) を使うと、自身で明示的に VM を立てずに Service のクラスタを立ち上げて管理できる。Swarm、Apache Mesos、さらに Marathon などに対応しているので、上述の分散クラスタを扱う場合にもって来いなサービスだ。(むしろ、クラスタを立てずに使うようなシンプルな使い方はできない。)

現在、下記の ARM template を使ってクラスタ (サービス インスタンス) を配置 (作成) するが、内部では結果的に、Virtual Machine Scale Sets (VM Scale Sets) や Load Balancer のリソース が起動している。(利用者にとって単一のサービス インスタンスとして管理できるだけで、実際には VM などの IaaS 環境が使われているということ。ECS 同様、GCE のような Full Managed なサービスではない。)
Amazon EC2 Container Service (ECS)、Google Container Engine (GCE) 同様、今後は Auto Scale に対応しようということだろう。

Mesos による ACS (Azure Container Service) クラスタの配置
https://github.com/Azure/azure-quickstart-templates/tree/master/acs-mesos

Swarm による ACS (Azure Container Service) クラスタの配置
https://github.com/Azure/azure-quickstart-templates/tree/master/acs-swarm

クラスタを作成した後は、Mesos UI などの標準のツールで管理できるという点は相当 魅力的だ。いったん配置してしまえば、Apache Mesos、Swarm の利用者にとって説明の必要はないだろう。(ただし、プログラムから管理する場合は、ACS が提供する REST API も使用可能。)

広告