こんにちは、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. システム構成:
構成図は以下のとおり:
特に難しいことはなく、一般的なELB-Autoscaling構成になります。ストレージはS3を利用します。
1.2. 構築手順とポイント
- S3にイメージ格納用のバケットを作成
- ELBを作成:
Health Check
はtcpの5000番ポート(registryコンテナの動作ポート)- HTTPSの443ポートからHTTPの5000番ポートへトラフィックを転送
- HTTPS証明書はAWSのCertificate Managerサービスを利用
(最初に構築時ACMはまだ東京リージョンに来ていませんでしたが、5月に東京リージョンでも利用可能になりました。便利なサービスなのでぜひ利用してみてください。) Security Group
は社内ネットワークに対してのみ443を開放するように設定
-
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
- 今回はあくまで社内で試験的に使うので、コストを抑えるためAutoscalingはmin 1, max 1のサイズ維持方式に設定
- 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の全体構成を以下のようにしました:
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と連携するため、下記の対応が必要です:
- portusとregsitry間の通信暗号化のため、証明書の作成とインポート
- registryとportusはELB経由でアクセスするため、VPCのセキュリティ設定に引っかからず、VPC内の通信を行うためにInternal ELBを追加
- registry側の
auth
とnotifications
オプション設定
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
ここの注意点は:
auth
のservice
はdocker registryのDNS名、issuer
はportusのDNS名rootcertbundle
はオレオレ証明書のサーバ証明書、こちらはdocker registry起動時に読み込まないとsslエラーになるnotifications
のurl
は通知先なので、アクセスできる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と機能
メイン画面:
UIがかなりシンプルで見やすい。
ユーザ毎に専用のnamespaceがあり、さらにteamを作成することができます。
ユーザは「Owner」、「Viewer(pullのみ)」、「Contributors(push可能)」の三種類の権限があります。
自分のnamespaceをpublic/privateを切り替えます。
他にも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で起動します、設定項目もかなり簡単です:
ManagerとWorkerの数、タイプを設定するだけ。あとはCloudFormationが全部やってくれます。
一見かなり複雑ですが、主に下記の内容があります:
- 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の
ACL
とSecruit Group
を変更することをおすすめします。
コンテナをデプロイする時、下記に2つの方法があります:
- sshでELBからManager Nodeに入り、Node上でデプロイ
- 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 for AWSのStackで作成されてEC2のRoleにCloudWatchのアクセス権限を付与。
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について色々試してみたいです。