✏️ 👤

Docker-in-Docker でお手軽 Amazon ECS Anywhere お試し環境を手に入れる

『手軽に作って壊してができる ECS Anywhere お試し環境が欲しい』、あるいは『ECS Anywhere で遊んでみたい気持ちはあるけどそれだけのために Raspberry Pi を買う1気にはならない』、という方向けの記事です.

TL;DR

x86_64 なラップトップが手元にあるなら…

  1. VirtualBox で VM を作ればサクッと試せる
  2. ただし VM はそこそこ重い

M1 Mac なみなさまは…

  1. VirtualBox は残念ながら M1 Mac 未サポート
    というか ARM 未サポート
  2. お金を出せば Parallels で ARM な VM を作れる2ので、それも可 💸
    VMware Fusion は残念ながら記事執筆時点で ARM 未サポート

というわけで、本記事では Docker-in-Docker を利用して (M1 Mac でも) ECS Anywhere する手順をまとめていきます 💪

そもそも ECS Anywhere ってなんですか?という方のために最初に簡単に説明しますが、そんなのいいからとにかく遊びたいという方は「実践 Amazon ECS Anywhere + dind on M1 Mac」セクションに進んでください. 実際に遊ぶ前にデモは見ておきたい、という方は「デモ - Amazon ECS Anywhere + dind on M1 Mac」セクションにどうぞ.

目次

  1. Amazon ECS Anywhere とは
  2. 『ラップトップで試したいけど、そんなわたしは M1 Mac』という壁
  3. Docker-in-Docker (dind)
  4. デモ - Amazon ECS Anywhere + dind on M1 Mac
  5. 実践 Amazon ECS Anywhere + dind on M1 Mac
  6. まとめ

Amazon ECS Anywhere とは

Amazon ECS Anywhere は2021年の5月にリリースされた Amazon ECS の機能の一つで、これを利用するとクラウド上の ECS クラスタに AWS 外の(例えばオンプレミスデータセンターの)物理・仮想マシンが参加できるようになります. Amazon ECS の RunTaskCreateService といった API を叩けば、クラスタに参加させたクラウドの外のマシン群の上でコンテナを実行できるという優れものです.

ECS クラスタに参加させるマシンは、その物理的な位置から近い AWS リージョンの ECS クラスタに繋ぐことが AWS 公式には推奨3されていますが、例えば ap-northeast-1 (東京) リージョンに作った ECS クラスタに世界中のデータセンターからマシン繋ぐことで厨二病心を悪化させる超グローバルなクラスタを構築することも、もちろん理論上可能です.

そんな夢広がるクラスタを作れる Amazon ECS Anywhere ですが、本質的に解決したいことはそういったエンジョイ系クラスタの実現ではありません. 新規システムを AWS 上で作ることに慣れてしまった僕個人としては、何か動かしたいなら全部 AWS 上で動かせばいいのでは?などと考えがちなのですが、世の中にはオンプレミスで動かさなければならないさまざまな事情やワークロードがあります.

例えば(すんごい金額を)投資済みのオンプレミスデータセンターを今後も活用していく必要があったり、法律や業界のレギュレーションによって借り物ではなく所有するハードウェアの上でデータを処理する必要があったり、あるいは個人情報のような類のデータは必ずオンプレミスデータセンターで処理することがルールとして定められている会社や組織といったものも少なからずあるでしょう.

もし Amazon ECS Anywhere が存在しなかったら、上記のようなケースでオンプレミスにてコンテナを実行したい場合には Kubernetes (やその派生プロダクト)に代表されるコンテナオーケストレーションソフトウェアを利用することが一般にはよく知られている方法かもしれません. 実行するコンテナの数が少なければ docker run だけで事足りるかもしれませんが、多くの場合なんらかのコンテナオーケストレーションツールを実行し、それらを管理運用していくことになるでしょう.

しかし、オンプレミスでコンテナを実行はしたいけど、じゃあ一緒に Kubernetes を実行して管理運用していきたいかと言うとそれは人それぞれ、というか会社それぞれです. すでに Kubernetes をベースとした標準化を推し進めてきた組織にとってはオンプレミスでの Kubernetes 実行とその管理運用は必然かもしれませんが、Kubernetes を実行したいんじゃなくてコンテナを実行したいんですけど私たちは、という方々にとってはこれは激しく重労働、巨大なコストです.

そこで Amazon ECS Anywhere です.

Amazon ECS Anywhere では、コンテナオーケストレータそのものはこれまでの ECS 同様にクラウド側に配置され、引き続き AWS が管理運用するフルマネージドなコントロールプレーンであり続けます. これはつまり、コンテナ実行環境であるオンプレミス側でのコンテナオーケストレータ実行・管理・運用が不要であることを意味します.

“Amazon ECS Anywhere” という名前がついているので、まるで何か新しいサービスがリリースされたかのように見えますが、実際には EC2 と Fargate というこれまでの使えた ECS のノードタイプ(コンテナの実行環境)に加えて、“External” というノードタイプが増えたというのが Amazon ECS Anywhere を端的に説明した表現と言えるでしょう.

『ラップトップで試したいけど、そんなわたしは M1 Mac』という壁

というわけでそんな Amazon ECS Anywhere を試してみたいわけですが、クラスタに参加させるノードが手元にない、という方もいらっしゃるはずです.

Raspberry Pi や Intel NUC のようなおうちコンテナ勢に人気のアイテムもありますが、これらもそこそこお金がかかります. 特に Intel NUC は僕も欲しいけど高くて手が出せません.

Docker Desktop がこなれてくるまでみんなが愛用していた VirtualBox は ARM 未サポートなので M1 Mac では使えないし、M1 Mac で ARM VM を動かせる Parallels も ECS Anywhere の検証目的で買うにはちょっとお高い.

AWS 上に Amazon Lightsail4 で建てた VM を使ってもいいけど、違うんですそういうのじゃないんです.

そこで Docker-in-Docker です.

Docker-in-Docker (dind)

実行中の Docker コンテナの中で dockerdockerd コマンドなどを利用できるようにし、コンテナの中で docker run を実行してコンテナ内コンテナを立ち上げるテクニックを一般に Docker-in-Docker、略して dind と呼んだりします.
その様子から「Docker マトリョーシカ」などと呼んだこともありましたが、残念ながら6年が経った今でもこの呼び名が流行る様子はありません.

余談ですが、手元で触る dind の代表例5は最近だと kind とかでしょうか. Kubernetes コントローラ類を実装しているような人やあるいは OSS のコントローラをちょっとだけ手元で動かしてみたい、といったケースで使ったことがあるかもしれません.

そしてこの dind の仕組みを手元のラップトップ(e.g. M1 Mac)上で使うことで、VM を使わずに ECS Anywhere で遊べるようになります. 最高です.

  1. ラップトップ(e.g. M1 Mac)上で dind 可能なコンテナを立ち上げ、それを ECS Anywhere ノードとして登録 (以下、「dind ノードコンテナ」と呼びます)
  2. ECS の API を呼んで ECS タスクを実行すると、dind ノードコンテナの中で ECS タスク(コンテナ)が起動する

最高ですね.

デモ - Amazon ECS Anywhere + dind on M1 Mac

以下の動画は先日(2021/7/16)行ったライブ配信で ECS Anywhere を紹介した際にお見せした、ECS Anywhere なコンテナを dind on M1 Mac で動かすライブデモです. ECS クラスタの作成から ECS Anywhere ノードの登録、タスク定義の登録にタスクの実行まで数分程度で完了します‪🤗
デモは動画の58分30秒くらいからで、サムネイルをクリックするとその時間あたりから再生が開始されます.

実際に動かさなくてもデモ動画で十分、という場合にはこちらを見ていただくだけでも十分かもしれません.


さて、雰囲気を掴んだところで実際に動かす手順に進みましょう.

実践 Amazon ECS Anywhere + dind on M1 Mac

セクションタイトルには便宜上 “M1 Mac” と入れてますが、Linux コンテナが動く Docker 環境なら M1 Mac 以外でも普通に動きます.

1.「ノード」となるコンテナイメージの作成

まずは以下の Dockerfile を手元の任意のディレクトリに保存します.
以下は Ubuntu ベースのものです. Amazon Linux 2 ベースのものを使いたい場合はこちらをどうぞ.

ビルドします.

$ docker build -t ecs-anywhere-node-img:latest .

2. dind ノードコンテナの実行

あとから停止や再起動がしやすいよう、ここでは名前付きコンテナ ecs-anywhere-node として実行します. Expose しているポート番号 8080 から 8090 番は後段のステップで ECS を使ってノードコンテナ内でコンテナを実行した際に HTTP リクエストを送れるように設定していますが、これは何番でも大丈夫ですし、レンジで expose する必要性も特にはありません.

$ docker run -d --privileged \
    --name ecs-anywhere-node \
    -p 8080-8090:8080-8090 ecs-anywhere-node-img:latest

さて、コマンドから一目瞭然ですが、Docker-in-Docker を実現する方法として privileged フラグを利用していることに気がつきますね. privileged フラグの危険性についてはここでは論じませんが、少なくとも『どこでもサクサク利用して良いオプションの類ではない』ことだけは頭の片隅に置いておきましょう.
最近だと SysboxRootless Containers といったものもありますので、このあたりも一緒に覚えておくとどこかで役に立つ日が来るかもしれません.

3. dind ノードコンテナ を ECS Anywhere ノードとして登録

API/CLI でもノード登録はやれますが、今回は見た目に分かりやすい Amazon ECS マネジメントコンソールを利用します. AWS アカウントやリージョンがテストに利用して良いものかどうかの確認を忘れないようにしましょう.

ちなみに前述しましたが、リージョンはどこでも大丈夫です. 今回は東京リージョンを利用します.

3-1. ECS クラスタを作成する

まずはテスト用の ECS クラスタを作成します. 既存の ECS クラスタを利用したい場合はそれでも構いません. 画面左側の「新しい ECS エクスペリエンス」がオフになっていることを確認し、「クラスターの作成」を押下します.

新規クラスタ作成

新規クラスタ作成

今回作成する ECS クラスタには手順2で起動したノードコンテナだけを登録するので、「ネットワーキングのみ」を選択して「次のステップ」を押下します.

クラスタのネットワーク構成初期値

クラスタのネットワーク構成初期値

ここではクラスタ名を “ecsAnywhere-test” にしてみました. お好きな名前を入力してください. VPC は不要なのでチェックを外し、ECS タスクやコンテナのメトリクスを CloudWatch で確認したい場合は Container Insights の有効化にチェックを入れます. 最後に「作成」を押下しましょう.

クラスタ名の設定など

クラスタ名の設定など

無事クラスタが作成できたら「クラスターの表示」を押下して作成した ECS クラスタの画面に移動します.

作成完了

作成完了

3-2. dind ノードコンテナを ECS クラスタに登録するスクリプトを取得する

作成したばかりのクラスタなので「登録済みコンテナインスタンス」は “0” となっているはずです. 「ECS インスタンス」タブを開き、「External インスタンスの登録」を押下します.

External インスタンスの登録

External インスタンスの登録

料金についての注意書きが気になりますが、利用する AWS アカウントではじめて ECS Anywhere インスタンスを登録する場合は最初の半年間は毎月 2,200 インスタンス時間(24時間立ち上げっぱなしのノード3台分くらい)が無料枠になりますので、その範囲を超えないようにすれば特に心配しなくても大丈夫でしょう5. 画面の各項目は必要があれば変更して構いませんが、この記事ではそのまま進めますので「次のステップ」を押下します.

External インスタンスの台数や割り当てる IAM ロールなど

External インスタンスの台数や割り当てる IAM ロールなど

画面にコマンドが表示されますので、「コピー」を押下してローカルのテキストエディタなどに貼り付けておきましょう. 「完了」を押下してインスタンス登録手順の画面を閉じます.

External インスタンス登録コマンドのコピー

External インスタンス登録コマンドのコピー

あとはコピったコマンドを dind ノードコンテナの中で実行するだけです.

3-3. dind ノードコンテナを ECS クラスタに登録する

手順2ですでに実行済みの dind ノードコンテナの中に入ります. (4eebf... のところは実行した環境ごとに異なる値になりますが、気にする必要はありません)

$ docker exec -it ecs-anywhere-node /bin/bash
root@4eebfcedbeef:/

手順3-2で取得したコマンド(curl --proto "https" -o ... みたいなやつ)を貼り付けて実行します.

root@4eebfcedbeef:/ curl --proto "https" -o "/tmp/ecs-anywhere-install.sh" "https://amazon-ecs-agent.s3.amazonaws.com/ecs-anywhere-install-latest.sh" ... # 省略

Docker エンジンや ECS Agent のセットアップなどをやってくれている様子がうかがえると思います. 以下のような表示が確認できたら ECS マネジメントコンソールに戻りましょう.


## 省略

##########################
# Trying to wait for ECS agent to start ...

Ping ECS Agent registered successfully! Container instance arn: "arn:aws:ecs:ap-northeast-1:123456789012:container-instance/ecsAnywhere-test/1a4b8d11e48c46cb93178d11d6f21b99"

You can check your ECS cluster here https://console.aws.amazon.com/ecs/home?region=ap-northeast-1#/clusters/ecsAnywhere-test

# ok
##########################


##########################
This script installed three open source packages that all use Apache License 2.0.
You can view their license information here:
  - ECS Agent https://github.com/aws/amazon-ecs-agent/blob/master/LICENSE
  - SSM Agent https://github.com/aws/amazon-ssm-agent/blob/master/LICENSE
  - Docker engine https://github.com/moby/moby/blob/master/LICENSE
##########################

ECS マネジメントコンソールを再読み込みすると「登録済みコンテナインスタンス」に “1”、そして「ECS インスタンス」タブ下のテーブルに登録されたインスタンスが載っていることが確認できるはずです.

External インスタンス登録コマンドのコピー

External インスタンス登録コマンドのコピー

4. dind ノードコンテナ内で ECS タスクを実行

4-1. ECS タスク実行 IAM ロールを作成する

dind ノードコンテナ内で実行する ECS タスクのアプリケーションログを CloudWatch Logs に格納するための IAM ロールを作成します.

# dind ノードコンテナの中ではなく、Mac などの Terminal で作業します🙋‍♂️
$ cat > task-execution-assume-role.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "ecs-tasks.amazonaws.com" },
      "Action": "sts:AssumeRole"
  }]
}
EOF

$ aws iam create-role \
    --role-name ecsAnywhereTaskExecutionRole \
    --assume-role-policy-document file://task-execution-assume-role.json

$ aws iam attach-role-policy \
    --role-name ecsAnywhereTaskExecutionRole \
    --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

# あとで使うので環境変数に入れておく
$ export TASK_EXECUTION_ROLE_ARN=$(aws iam get-role --role-name ecsAnywhereTaskExecutionRole --query 'Role.Arn' --output text)

4-2. ECS タスク定義を作成する

今回は HTTP リクエストに対して “Hello, World!” を返す public.ecr.aws/toricls/go-hello-world:latest を実行します. のちほど実施する動作確認用にホスト(M1 Mac)側のポート番号 8080 への HTTP リクエストがコンテナ内のアプリケーション待ち受けポート番号である 80 に流れるように設定をします.

$ export CONTAINER_DEFINITION=$(cat <<EOF
{
    "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
            "awslogs-group": "/ecs/hello-ecs-anywhere",
            "awslogs-region": "ap-northeast-1",
            "awslogs-stream-prefix": "ecs"
        }
    },
    "portMappings": [{
        "hostPort": 8080, "protocol": "tcp", "containerPort": 80
    }],
    "environment": [{
        "name": "MESSAGE",
        "value": "こんにちは Amazon ECS Anywhere です!🚀"
    }],
    "image": "public.ecr.aws/toricls/go-hello-world:latest",
    "essential": true,
    "name": "hello-ecs-anywhere"
}
EOF
)

# ECS タスク定義の登録
$ aws ecs register-task-definition \
    --region ap-northeast-1 \
    --family hello-ecs-anywhere \
    --execution-role-arn "${TASK_EXECUTION_ROLE_ARN}" \
    --container-definitions "${CONTAINER_DEFINITION}" \
    --network-mode bridge --cpu 256 --memory 256 \
    --requires-compatibilities EXTERNAL

# CloudWatch Logs のロググループも作成しておく
$ aws logs create-log-group \
    --region ap-northeast-1 \
    --log-group-name /ecs/hello-ecs-anywhere

4-3. dind コンテナ内でタスクを実行する

ECS マネジメントコンソールからタスクを実行してみましょう.「タスク」タブを開き、「新しいタスクの実行」を押下します.

「起動タイプ」に EXTERNAL、「タスク定義」に先ほど作成した hello-ecs-anywhere を選び、画面下部の「タスクの実行」を押下します.

今回利用している public.ecr.aws/toricls/go-hello-world:latest イメージはサイズも数 MB と小さいので長くても10数秒程度でタスクの起動が確認できるはずです. 再読み込みボタンを押下してみて PENDING のところが RUNNING になったら起動完了です.

5. 動作確認

ここまでのステップで、手元のラップトップで実行している dind ノードコンテナの中で public.ecr.aws/toricls/go-hello-world:latest コンテナが起動しているはずなので、動作確認してみましょう.

ブラウザで http://localhost:8080 を開きます.

Yay!✌️

6. ストレッチでこんなのも挑戦してみてはいかがでしょうか

  1. ECS タスク定義内の環境変数 MESSAGE を書き換えて再デプロイしてみる
  2. CloudWatch Logs の /ecs/hello-ecs-anywhere ロググループに保存されているアプリケーションログを見てみる
  3. dind ノードコンテナをもう1個増やし、ECS サービスで複数個のタスクを実行してみる.
    手順2と同じステップで増やせますが、その際は --name オプションと -p オプションが1個目の dind ノードコンテナと被らないように注意しましょう.
  4. 自前のコンテナイメージなど、他のコンテナイメージをデプロイしてみる

7. お片付け

遊び終わったら片付けもしておきましょう.

7-1. AWS 上のリソースを削除する

まずは External インスタンスを登録解除します.「ECS インスタンス」タブから対象のコンテナインスタンス名を選択(クリック)しましょう.

画面右上の「登録解除」を押下します

「AWS Systems Manager から登録解除」にチェックを入れ、フィールドに “deregister” と入力した上で「登録解除」を押下します.

ECS クラスタも削除します. 画面右上の「クラスターの削除」を押下しましょう.

フィールドに “delete me” と入力し、「削除」を押下します. 削除後 ECS クラスタの一覧画面に戻ると削除したはずの “ecsAnywhere-test” クラスタが残っていてびっくりするかもしれませんが、放っておくとそのうち消えます.

次に ECS タスク実行 IAM ロールを削除しましょう. 次のコマンドを実行してください.

$ aws iam detach-role-policy \
    --role-name ecsAnywhereTaskExecutionRole \
    --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

$ aws iam delete-role \
    --role-name ecsAnywhereTaskExecutionRole

External インスタンス登録時に自動的に作成される “ecsExternalInstanceRole"、CloudWatch Logs の /ecs/hello-ecs-anywhere ロググループも不要であれば削除してかまいません.

7-2. お手元のラップトップをお掃除する

# ストレッチタスクで2個目以降の dind ノードコンテナを実行している場合はそちらも stop と rm を実施しておきましょう
$ docker stop ecs-anywhere-node && docker rm ecs-anywhere-node
$ docker rmi ecs-anywhere-node-img:latest

以上です!

まとめ

ECS Anywhere でもっと遊びたい!という方、次は Raspberry Pi を使ったおうち ECS クラスタ構築なんていかがでしょうか?

他にももっと面白い ECS Anywhere の使い方があるよ!という方、ぜひ @toricls までご連絡ください!お待ちしております!


  1. Amazon VPC と接続可能なおうち Amazon ECS Anywhere クラスターの構築 | Amazon Web Services ブログ ↩︎

  2. [祝GA!] ECS Anywhere を「M1 Mac」の仮想マシンを使って動かしてみた | DevelopersIO ↩︎

  3. “Q: どの AWS リージョンでオンプレミスコンピューティングを登録すればよいですか?” - “A: オンプレミスのコンピューティングに地理的に最も近い AWS リージョンに登録することをお勧めします。” - Amazon ECS Anywhere FAQ ↩︎

  4. AWS 上で $3.5~/mo で使える VPS - Amazon Lightsail ↩︎

  5. 正確には kind は dockershim は使っていないはずなので Containerd-in-Docker などと表現すべきかもですが、細かいところはおいておきましょう. ↩︎