ECSのトラブルシューティングしたお話
はじめに
今回はTerrafromを使ってECSの構築をしていた際に躓いたことを紹介していきます。
メインは躓きポイントなので、コードについてはあまり詳しく書いていません。
全体のコードについてはこちらに置いてあるので、コードが見たい方はコチラへ↓
全体図
では最初に作成する全体図はこんな感じです。
全体としては、ECRからイメージを取得してタスクを起動しています。
シンプルなnginxのコンテナイメージです。
CloudWatch Logはあまり関係ありませんが、構成中に躓いた際のトラブルシューティング用に設定しています。
今回はECSをより理解するために、起動タイプは「EC2」を選択しています。
躓いたところ
では、ここから私が構築時に躓いた部分を紹介していきます。
大きく分けるとこんな感じです。
- タスクが起動しない
- ネットワークモード
- ホストのポート設定
公式ドキュメントを読み込めば躓かない部分かもしれませんが、100%網羅するのは不可能なので
トライ&エラーでトラブルシューティングをしていきます。
先に結論、その後にトライ&エラーの内容を書いていきます。
結論だけ欲しい!って方は最初の部分だけ読み進めてみてください。
タスクが起動しない
まずは結論から
タスクのplatformがx86_64
に設定されていました。
それに対して、ECRのコンテナイメージのplatformはarm64
になっていました。
その結果、実行基盤が合わず、起動できなかったようです。
コンテナイメージのplatformをx86_64で作成してpushし直すことで解決しました。
ロールの確認
ではここからは私が実際に試したことを書いていきます。
最初に設定していたタスク定義はこちらです。
resource "aws_ecs_task_definition" "task_definition" {
family = "${var.tag}-nginx"
cpu = "256"
memory = "512"
network_mode = "bridge"
requires_compatibilities = ["EC2"]
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
container_definitions = jsonencode([
{
"name" : "${var.tag}-task-definitions",
"image": "1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
"essential" : true,
"portMappings" : [
{
"containerPort" : 80
"protocol" : "tcp"
}
]
}
])
}
この状態でterraform apply
を行いました。
すると、サービスで最小タスク数を"1"に設定していたので、タスクを起動しようとしますが、エラーが出続けていました。
そして、CloudWatch Logsを設定していなかったのでエラー内容を確認できませんでした。
起動時のエラーなのでイメージ取得あたりが怪しいのかな?と思い
試しに使用しているイメージをパブリックなイメージに変えてみました。
"image": "1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
から
"image": "nginx:latest",
に変更すると、タスクが起動しました。
ということでECRからのイメージ取得に問題があると推測しました。
ここで最初に考えたのが、タスク定義ロールが設定できていないかな?と思いました。
ECRからイメージを取得するにはタスク定義ロールにAmazonECSTaskExecutionRolePolicy
というポリシーが必要です。
しかし、IAMロールの設定には特に問題はありませんでした。
Platform
行き詰まってしまったので、ログを回収することにしました。
タスク定義にCloudWatch Logsを設定して、起動時のエラーを確認します。
するとタスク起動時に以下のエラーが出ていました。
exec /docker-entrypoint.sh: exec format error
こいつや!!(やっぱりログを見るのが一番早いですね...)
調べてみると、イメージのプラットフォームと、実行するプラットフォームに互換性が無い場合に発生するエラーのようです。
結果としては、タスク定義で設定したplatformがx86_64で
作成したコンテナイメージがarm64でした。
ECSのコンソール画面を確認したところ、プラットフォームの部分が「-」となっています。
これは、terraformでplatformを明示していなかったので、デフォルト?のx86_64になっていたようです。
そこで、以下のコマンドでx86_64のイメージを作り直し、ECRに再アップロードしました。
$ docker build --platform linux/amd64 -t nginx .
少し気持ち悪いので、タスク定義もplatformをx86_64と明示しておきました。
runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}
これでタスクが立ち上がるようになりました。
platformもしっかりx86_64になっています。
それではコンテナが立ち上がったので、次は通信です。
ネットワークモード
まず結論です。
今回の構成ではネットワークモードが「awsvpc」では外からの接続ができませんでした。
「bridge」モードにすることで、外から接続できるようになりました。
では具体的に
私は最初、ネットワークモードを「awsvpc」に設定していました。
しかし、awsvpcモードでterraform apply
をすると以下のエラーになりました。
Error: creating ECS Service (nginx): InvalidParameterException: Assign public IP is not supported for this launch type.
起動タイプがEC2の場合はパブリックIPが割り当てられないよ!と怒られました。
知らなかった...
コンテナ用のENIへパブリックIPの割り当てられるのは起動タイプがFargate時のみだそうです。
これではホストのENIにパブリックIPを割り当てたとしても、コンテナに接続することができません。
図にするとこんな感じです。
そこで、ネットワークモードは「bridge」を選択しました。
これで外からコンテナに接続することができるようになった、はずだったのですが...
ホストのポート設定で躓いていたので、次に続きます。
ホストのポート
先に結論から
ホストのポートを動的に割り当てていたため、ポート番号が分からず通信できませんでした。
そこで明示的にホストのポートを割り当てることで解決しました。
では具体的に
ネットワークモードを変更した後に、ブラウザからEC2のパブリックアドレスにアクセスしました。
しかし、アクセスできません。とエラーが返ってきます。
ここでパッと思いつくのは
- ホスト(EC2)が起動していない
- コンテナが起動していない
- ネットワーク/セキュリティ関係の問題で接続できない
この辺りかなと推測しました。
そこで、まずはsshでホストに接続してみます。
$ ssh -i [ssh keyのpath] ec2-user@[EC2のパブリックIP]
問題なく接続できたので、docker psコマンドでコンテナが動いていることを確認します。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1234567890 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest "/docker-entrypoint.…" 42 minutes ago Up 42 minutes 0.0.0.0:36728->80/tcp nginx
1234567890 amazon/amazon-ecs-agent:latest "/agent" 18 hours ago Up 18 hours ecs-agent
コンテナは動作しているようです。
しかし、ここで気づきました。
ホストのポートが謎の番号になっている!!
0.0.0.0:36728->80/tcp
この部分ですね。
ホストのポートが36728になっています。
こちらの原因は
bridgeモードを選択したことで、ホストのポートが動的に割り当てられたことです。
そのため、タスク定義にてホストのポートを明示的に記載しました。
container_definitions = jsonencode([
{
"name" : "${var.tag}-task-definitions",
"image": "194641379830.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
"essential" : true,
"portMappings" : [
{
"containerPort" : 80
"hostPort" : 80 <----追記
"protocol" : "tcp"
}
]
}
])
}
これで無事にブラウザよりアクセスできることが確認できました。
まとめ
教訓としては、まずはエラーの詳細を手にいれるべし、ということです。
なんとなくこれでいけそう!みたいなのは浅い経験値では特に混乱を招くことがあると学びました。
再現性のあるやり方を一つずつ試して、何をやった、何をやってない、を明確にしていくことが大切だと学びました。
参考資料
Discussion