logo-title-final-registry-2−480

こんにちは、kyoです。

僕は元々エンジニアですが、これまでインフラ面に触れることは、あまりありませんでした。
リクルートジョブズに入社してからAWSやDockerなどのクラウド・インフラ技術に興味を持ち、色々と勉強してきました。
そしておそらく社内初のAWS認定資格取得や、微力ながらDockerのコントリビューター(主にzsh completion)になったという個人的に嬉しい成果も得ました。

今回はこの2つをテーマとして社内でAWS/Dockerの運用・推進状況を紹介していきます。
内容は『AWS上のDocker Registry』、『Registryの認証・フロントサーバ』、『Docker for AWS』の3つになります。

1. AWSでのDocker Registry構築

最初に社内のAWS環境を活用して、Dockerを色々試したい!と思いましたが、その矢先に出てきた問題はRegistry環境でした。
もちろんDocker Hubという公式の素晴らしいサービスはありますが、社内のセキュリティルール上使うことが難しいので、他の方法を探すしかありませんでした。
その際検討した候補と結果を以下にまとめますと…

候補 構築工数 運用工数 利用の便利さ セキュリティ コスト 選定結果
Docker Hub Enterprise
Amazon ECR
DockerRegistry on Elastic Beanstalk
DockerRegistry on EC2

構築、運用工数、利用の便利さ、セキュリティ、コストなどのあらゆる面から総合的に考慮した結果、DockerRegistry on EC2に決めました。

1.1. システム構成:

構成図は以下のとおり:
cloudcraft - Docker Registry
特に難しいことはなく、一般的なELB-Autoscaling構成になります。ストレージはS3を利用します。

1.2. 構築手順とポイント

  1. S3にイメージ格納用のバケットを作成
  2. ELBを作成:
    • Health Checkはtcpの5000番ポート(registryコンテナの動作ポート)
    • HTTPSの443ポートからHTTPの5000番ポートへトラフィックを転送
    • HTTPS証明書はAWSのCertificate Managerサービスを利用
       (最初に構築時ACMはまだ東京リージョンに来ていませんでしたが、5月に東京リージョンでも利用可能になりました。便利なサービスなのでぜひ利用してみてください。)
    • Security Groupは社内ネットワークに対してのみ443を開放するように設定
  3. Launch ConfigurationとAutoscaling Groupを作成:

    • 今回はあくまで社内で試験的に使うので、コストを抑えるためAutoscalingはmin 1, max 1のサイズ維持方式に設定
       (ダウンタイムは5分位なのでクリティカルな問題ではありません。)
    • コンテナが死んだ場合自動的に新しいEC2を立ち上げるようにAutoscalingのHealth CheckはELBタイプに設定
       (EC2タイプの場合、EC2自体が正常な限りAutoscalingが動作しません。)
    • Launch ConfigurationのSecurity GroupはELBからのトラフィックしか受け付けないように設定し、セキュリティを確保
    • EC2にS3書き込みきるRoleを作成(ポリシーにS3FullAccessなどを設定)
    • EC2のUser dataにdocker起動のコマンドを記入(Roleを設定したためawsのkeyとsecretは不要):
    #!/bin/sh
    yum update -y
    yum install -y docker
    service docker start
    docker run -d --name registry -p 5000:5000 \
    -e REGISTRY_STORAGE_S3_BUCKET=your_bucket \
    -e REGISTRY_STORAGE_S3_REGION=your_region \
    -e REGISTRY_STORAGE_S3_ROOTDIRECTORY=/ \
    -e REGISTRY_STORAGE=s3 \
    registry:2.0
    
  4. Route53のHostZoneで、ELBのDNS名をCNAMEレコードとして追加(例:docker.yourdomain)。

これでRegistryの構築が完了しました。今回は認証を入れていないので、社内ネットワークのどこからでも利用できます。
利用方法はDocker Hubとほぼ同じです。(ELBとACMによりHTTPSが利用可能、ポートの指定が不要です。)

Pull:

$ docker pull docker.yourdomain/hello-world

Push:

$ docker tag yourimage docker.yourdomain/yourimage #Registryのtagを設定
$ docker push docker.yourdomain/yourimage

利用したAWSのサービス及びその用途を簡単にまとめますと:

  • S3: イメージデータの永続化
  • EC2: Dockerのホストとして利用
  • AutoscalingGroup: システムの可用性を維持
  • ELB: 固定のEndpointとHTTPSの暗号化を提供
  • ACM: HTTPSで利用する証明書を提供
  • Route53: ELBのDNS名を自分のDomainにマッピング

2. Docker Registryの認証とUIを追加

上記で構築したRegistryは2ヶ月ほど安定運用しました。システム自体は特に問題ありませんでしたが、やはり他の要件が段々と出てきました。
主な要望として、「認証付きのPrivate Registry」と「Docker Hubみたいなイメージを確認、管理できるフロントUI」の二点です。

認証に関して、本家のドキュメントに書いてある通り、Docker Registry自体はhtpasswdベースのBasic認証を設定できますが、それだけではユーザの管理がかなり面倒なことになりかねません。他の認証サーバを立てるのが一般的だと思います。
認証サーバを自作するのは大変なので、cesantaのdocker_authなどのOSSを検討しましたが、今回はもう一つの要件として「UIが欲しい」とのことでしたので、色々調査した結果、Portusを利用することに決めました。

PortusはSUSEがrailsで開発したDocker Registry認証サーバ、FrontUIがあり、さらにDocker Hubとほぼ同レベルのユーザ/チーム管理、イメージ検索などが提供されています。

railsアプリなので、普通に設定してrails serverで起動すれば動きますが、やはりここは「Dockerでやりたい」という思いがありました。
Docker Hub上で検索すると色々出てくるのですが、一番PULLSが高いものを選びました。
また、イメージが突然変わることを回避するため、一回イメージをPullしてきたAMIを作成し、Launch ConfigurationでこのAMIを利用することにしました。

2.1. システム構成

今回のPortusと合わせて、Docker Registryの全体構成を以下のようにしました:
cloudcraft - Docker Port

2.2. 構築のポイント

portusをregistryと同居することは可能ですが、今回はあえて分けました。
(ただし後から分けた場合の連携が結構面倒であること判明したので、同居することをおすすめします。)
portus自体の構築はregistryとほぼ同じ構成です。
(サイズ維持のAutoscalingGroupを組んで、User dataにdocker起動のコマンドを記入、さらにELBを付けてDNS名を振る。)
あとはDBが必要なので、RDSでMariaDBを作成します。
User dataに書いたportus起動のコマンドは下記になります:

#!/bin/sh
docker run -e RAILS_ENV=production \
  -e PORTUS_PRODUCTION_HOST=your_rds_endpoint \
  -e PORTUS_PRODUCTION_USERNAME=db_username \
  -e PORTUS_PRODUCTION_PASSWORD=db_password \ 
  -e PORTUS_PRODUCTION_DATABASE=portus \
  -e PORTUS_MACHINE_FQDN=portus.yourdomain \
  -e PORTUS_SECRET_KEY_BASE=your_secret \
  -e PORTUS_KEY_PATH=/etc/docker/key.pem \
  -e PORTUS_PASSWORD=portus_password \
  -v /etc/docker/server.key:/etc/docker/key.pem \
  -d -p 3000:3000 --name portus portus puma -b tcp://0.0.0.0:3000 -w 3

/etc/docker/server.keyは後述の通信暗号化用のサーバ証明書の秘密鍵です。

2.3. 連携のポイント:

portusをregistryと連携するため、下記の対応が必要です:

  1. portusとregsitry間の通信暗号化のため、証明書の作成とインポート
  2. registryとportusはELB経由でアクセスするため、VPCのセキュリティ設定に引っかからず、VPC内の通信を行うためにInternal ELBを追加
  3. registry側のauthnotificationsオプション設定

2.3.1. SSL証明書作成

今回はEC2内部のアプリで使用するため、ACMは使えません。(ACMはELBとCloudFrontのみサポート)
ただし、証明書の目的は通信の暗号化だけなので、自己署名証明書でも問題ありません。

PortusのDNS名に合わせて証明書を作成し、サーバ証明書の秘密鍵をAMIに配置
(上記docker起動コマンドで使う。)
また、作成した証明書はRegistryに読み込ませるため、Registryを動かすEC2に配置
(こちらもAMI作成することをおすすめします。)

2.3.2. Internal ELB作成

これはVPCのセキュリティ設定によるものです。
(RegistryからELB経由でPortusをアクセスする場合、トラフィックは一旦VPCからインターネットに出て、またVPCに戻るルートになりますが、VPCとELBのセキュリティ制限によって社内ネットワークのIP以外アクセスできないようになっています。)
RegistryとPortusが同居している場合や、VPCのAccess Control ListとELBのSecurity Groupが制限していなければ、この対応が不要です。
Registryからの通知をVPC内でPortusに届くため、Portusの外部ELBと同じ設定のInternal ELBを作成します。PortusのAutoscaling Groupに作成したInternal ELBも追加。

2.3.3. Registryのオプション追加

Registryのconfig.ymlを下記のように変更:

version: 0.1
loglevel: debug
storage:
  s3:
    region: your_aws_region
    bucket: your_s3_bucket
    rootdirectory: /
auth:
  token:
    realm: https://portus.yourdomain/v2/token
    service: docker.yourdomain
    issuer: portus.yourdomain
    rootcertbundle: /etc/docker/key.pem
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3
notifications:
  endpoints:
    - name: portus
      url: https://your_internal_elb/v2/webhooks/events
      timeout: 500ms
      threshold: 5
      backoff: 1s

ここの注意点は:

  • authserviceはdocker registryのDNS名、issuerはportusのDNS名
  • rootcertbundleはオレオレ証明書のサーバ証明書、こちらはdocker registry起動時に読み込まないとsslエラーになる
  • notificationsurlは通知先なので、アクセスできるportusのURLを設定
     (僕の場合、Internal ELB経由でアクセスするためInternal ELBのDNSになる)

変更したconfig.ymlもRegistry起動のAMIに配置する。

2.3.4. Registryの起動コマンド修正

config.ymlとサーバ証明書が入ったAMIを作成したら(dockerを入れて起動するのも忘れずに)、RegistryのLaunch ConfigurationでそのAMIからEC2起動するように変更し、さらにUser dataを下記に書き換える:

#!/bin/sh
docker run -d -p 5000:5000 \
  -v /etc/docker/config.yml:/etc/docker/registry/config.yml \
  -v /etc/docker/server.crt:/etc/docker/key.pem \
  --name registry registry:2

2.4. Portusの利用

これでPortusの構築が完了!さっそく使ってみましょう。

2.4.1 UIと機能

最初に作成したユーザはAdminになります。
portus_login

メイン画面:
portus_main
UIがかなりシンプルで見やすい。
ユーザ毎に専用のnamespaceがあり、さらにteamを作成することができます。

ユーザは「Owner」、「Viewer(pullのみ)」、「Contributors(push可能)」の三種類の権限があります。
portus_user

自分のnamespaceをpublic/privateを切り替えます。
portus_repo

他にもrepositoryの検索、star付け、コメントなどの機能です。詳細はPortusのドキュメントを参照してください。

2.4.2. 認証

認証機能もDocker Hubと同じ感覚で使えます。
Public Repoをpullする場合は今まで同様にdocker pullだけ、Private Repoにアクセスする場合、loginが必要です。
(Portusで登録したアカウントを使います。)

$ docker login -u username -p password docker.yourdomain

成功するとLogin Succeededのメッセージが表示されます。
loginできたら、Private Repoでのpull/pushができます。コマンド自体はPublic Repoの時と変わりません。

$ docker pull hello-world #docker hubからpull
$ docker tag hello-world docker.yourdomain/username/hello-world #自分のnamespcaeのtagをつける
$ docker push docker.yourdomain/username/hello-world #自分のrepoにpush

PS:最近AWSのECR(EC2 Container Registry)も東京リージョンで使えるようになりました、構築はかなり簡単ですが、下記の理由で見送りました:

  • URLが長すぎ、アカウントIDも丸出しになっている。CNAMEレコードも付けられないので対処する手段がない。
  • VPC対応していない、社内運用上セキュリティに難あり。
  • 認証はIAMと統合されているため、IAMユーザでないと使えない。
  • IAMユーザでも、loginするにaws cliが必須。

ここまで追加利用したAWSのサービス及びその用途を簡単にまとめますと:

  • AMI: カスタマイズした環境でEC2を起動
  • RDS: RailsアプリのDB
  • Internal ELB: ELB間の通信をVPC内で行う

3. Docker for AWSで楽々Dockerデプロイ

AWSとDockerの話題になると、コンテナデプロイはECSではないか?と思う人が多いかもしれません。僕も実際ECSを試しましたが、色々な独自仕様はちょっと気になり、やはりDocker自身のエコシステムを使いたいという気持ちが強くなりました。よって、DockerCon16で発表されたDocker for AWSも最初からかなり注目していました。

3.1. Stack

結構前からPrivate Betaに登録したので、一ヶ月前にBeta版のStackが来ました(執筆時点ではBeta4)。
Docker for AWSはCloudFormationのstackで起動します、設定項目もかなり簡単です:
docker-for-aws

ManagerとWorkerの数、タイプを設定するだけ。あとはCloudFormationが全部やってくれます。

Stackの中身はこういう感じです:
docker-for-aws-stack

一見かなり複雑ですが、主に下記の内容があります:

  • Swarm ManagerのAutoscaling Group
  • Swarm NodeのAutoscaling Group
  • SSH用のELB
  • Node全体のELB
  • DynamoのTable(ManagerのIP格納用, kubernetesにおいてのetcdみたいな感じですかね)
  • SQS(Swarm間通信用だと思います)

3.2. 利用方法

CloudFormationでパラメータ設定して作成すると、約10分程度でStackが作成できました。
作成されたDocker for AWS環境は2つのELBを提供しています:

  • SSH ELB:ManagerNodeにSSHでログインするためのELB、配下は全ManagerNode
  • External ELB:実際サービスとして動かすコンテナにアクセスするためのELB、配下は全ManagerNodeと全WorkerNode

そのままのStackだと、セキュリティは全く設定されてないので、必要に応じてVPCのACLSecruit Groupを変更することをおすすめします。

コンテナをデプロイする時、下記に2つの方法があります:

  1. sshでELBからManager Nodeに入り、Node上でデプロイ
  2. sshのトンネリングを使ってdocker.sockをローカルにトンネリングし、ローカルで直接dockerコマンドによってデプロイ。

個人的には2番目のsshトンネリングが好きです、理由としてはローカルのzsh completionが効くのでdockerコマンドがかなり使いやすいからです。
ただし注意点として、最近docker 1.12のコマンドオプションが頻繁に変わるので、ローカルのdocker(僕の場合はDocker for Macを使っています)のバージョンとDocker for AWSのバージョンが不一致になると、コマンドのオプションが通らないことがあります。

例えばこの記事を書いてる時点で、Docker for Macの1.12 beta20とDocker for AWS beta4を使っています。Docker for AWSの方はdocker service create --with-registry-authのオプションがありますが、Docker for Macは古いオプションの--registry-authになります。

sshでトンネリングしてDocker for AWSを利用:

$ ssh -NL localhost:2374:/var/run/docker.sock docker@ssh-elb-dns -i keypair &
$ export DOCKER_HOST="localhost:2374"

3.3 Docker Swarm

Docker for AWSは基本的にDocker 1.12に統合されたSwarmを使う形になります。

Docker SwarmはDocker社開発したDocker Nativeのクラスタ・オーケストレーションツールです(他にはGoogleのkubenetesやApache Mesos+Marathonなどがあります)。今までは単独なツールですが、docker 1.12からDockerに統合され、さらに使いやすくなりました。

Docker1.12のSwarm詳細は公式ドキュメントを参照してください。ここでは例としてnginxのデプロイを紹介します。

3.3.1. Nginxサービスデプロイ

まずはデプロイコマンド:

$ docker service create --name nginx --with-registry-auth --mode global -p 80:80 --mount type=bind,source=/home/docker/log,target=/var/log/nginx --log-driver=awslogs --log-opt awslogs-region=ap-northeast-1 --log-opt awslogs-group=dockerlogs docker.mydomain/myname/nginx

詳細を説明します:

  • docker service create : docker swarmのサービスを作成
  • --name nginx: サービスの名前を指定
  • --with-registry-auth: Managerから各nodeにPrivate Registryの認証情報を転送。これを指定しないとWorkerからPrivateRegistryにアクセスできない。
      (docker loginでログインする必要がある。)
  • --mode global: サービスをグローバル化。swarmのmodeが二種類あり、デフォルトではreplica modeで、指定の数に応じてクラスタ全体でコンテナを作成。今回のglobal modeでは、各nodeにコンテナを一個作成するという意味。
  • -p 80:80: コンテナの80ポートをホストにExpose。Docker for AWSではホストにExposeしたポートは自動的にExternal ELBに登録される。
      (ただし自動登録のプロトコルはすべてTCPになる)
  • --mount type=bind,source=/home/docker/log,target=/var/log/nginx: docker run-vオプションと同等な機能(docker serviceでは-vが使えない)。ホストのファイルシステムをコンテナにマウントする。
    • typeはホストのファイルシステムを指すbindとVolumeを利用するvolumeの2つ
    • sourceはディレクトリ名・Volume名
    • targetはコンテナ内のパス
  • -log-driver=awslogs --log-opt awslogs-region=ap-northeast-1 --log-opt awslogs-group=dockerlogs: こちらはコンテナのログオプション。今回は複数ノードでnginxを起動するが、ログは集約したいので、awslogsのdriverを利用して、CloudWatchにログを集約させる。awslogsを使う前の事前準備として:
    • Docker for AWSのStackで作成されてEC2のRoleにCloudWatchのアクセス権限を付与。
        (Roleに権限付与しない場合はオプションでawsのkey,secret指定が必要。)
    • CloudWatchでLog Groupを作成。
        (Streamは作成しない、各コンテナのログはコンテナIDのStreamに転送する。Streamを指定する場合、複数コンテナが同時書き込みのため性能が低下。)
  • docker.mydomain/myname/nginx:1.9.15-alpine: PrivateRegistryにあるNginxのイメージを指定

3.3.2. サービス確認

起動したサービス/ノードを確認するには、下記のコマンドが使えます。

  • サービスの一覧:

    $ docker service ls
    ID            NAME     REPLICAS  IMAGE                                          COMMAND
    8zhjzflg29gn  nginx    global    docker.mydomain/myname/nginx
    
  • サービスの詳細:

    $ docker service inspect --pretty nginx
    ID:       8zhjzflg29gnme4ehrvo5ck6r
    Name:     nginx
    Mode:     Global
    Placement:
    UpdateConfig:
    Parallelism:    1
    On failure: pause
    ContainerSpec:
    Image:      docker.mydomain/myname/nginx
    Resources:
    Ports:
    Protocol = tcp
    TargetPort = 80
    PublishedPort = 80
    
  • サービスの動作詳細:

    $ docker service ps nginx
    ID                         NAME       IMAGE                         NODE                                     DESIRED STATE  CURRENT STATE          ERROR
    dsmivgnejbyq4d32z0p3oc2td  nginx      docker.mydomain/myname/nginx  ip-xxxx.ap-northeast-1.compute.internal  Running        Running 5 seconds ago
    eoz3ktzi802qjbao0p3269qs9   \_ nginx  docker.mydomain/myname/nginx  ip-yyyy.ap-northeast-1.compute.internal  Running        Running 5 seconds ago
    
  • ノード一覧:

    $ docker node ls
    ID                           HOSTNAME                                 STATUS  AVAILABILITY  MANAGER STATUS
    18ebvcfil6wtj0clbjt2wustd    ip-xxxx.ap-northeast-1.compute.internal  Ready   Active
    7c4js4bjmffhyovhv5liki0e2 *  ip-yyyy.ap-northeast-1.compute.internal  Ready   Active        Leader
    
  • ノードの詳細:

    $ docker node ps self
    ID                         NAME         IMAGE                             NODE                                     DESIRED STATE  CURRENT STATE          ERROR
    eoz3ktzi802qjbao0p3269qs9  nginx        docker.mydomain/myname/nginx      ip-xxxx.ap-northeast-1.compute.internal  Running        Running 9 minutes ago
    

3.3.3. サービスの更新

デプロイしたサービスの更新もdockerコマンドから実施できます。

$ docker service update nginx --update-parallelism 1 --update-delay 5s --with-registry-auth --image docker.mydomain/myname/nginx:1.1
  • --update-parallelism 1: 同時にアップデートするコンテナの数をして、今回は一個ずつ順次に更新
  • --update-delay 5s: 次のコンテナ更新までの待ち時間
  • --image: 新しいコンテナを起動するためのイメージ
    • タグの管理には要注意、全部latestにすると、イメージは更新されない可能性がある

3.3.4 サービスの停止

動作中にサービスを停止するには:

$ docker serivce rm nginx

3.3.5 サービスのスケーリング

サービスのスケーリングに関して、EC2レベルとコンテナレベルのスケーリングができます。

  • EC2レベル: CloudFormationのUpdateからWorkerNodeの数を増やせば、Autoscaling Groupが設定に応じてEC2を追加し、Swarmに自動登録する
  • コンテナレベル: replica modeの場合、dockerコマンドで特定のサービスのコンテナ数をスケーリングできる

    $ docker service scale nginx=4
    

以上でDocker for AWSのサービスデプロイ、確認、更新、削除の一連操作を確認できました。
すべてdockerコマンドから行うことができ、ECSよりかなりシンプルだと思います。
ただしBeta版なので、まだまだ問題があると思います。
僕が実際に遭遇したのは、ManagerNodeのスケールインする時、Dynamoに登録したPrimaryNodeが更新されず、Swarm全体が繋がらなくなったというケースです。
ここは今後の更新に期待することですね。

ここまで追加利用したAWSのサービス及びその用途を簡単にまとめますと:

  • CloudFormation: Docker for AWSのStackを起動
  • CloudWatch: Dockerコンテナのログ集約
  • DynamoDB: Docker for AWSで利用(PrimaryNodeのIP格納)
  • SQS: Docker for AWSで利用(Swarm間通信)

おわりに

今回は僕が社内でのAWS/Dockerの推進・利用状況を簡単に紹介しました。Docker 1.12の発表で、ProductionレベルでのDocker利用はさらに増えると思います。今後のDockerの動きにも注目したいですね。
次はAzureとDockerについて色々試してみたいです。