DockerやECR, ECS, Fargateなど、コンテナ周りのAWS知識を効率的にキャッチアップしたい人のために

# 概要
私自身がコンテナや、コンテナ関連のAWSサービスについてはほぼ分からない状態だったのですが、そこからできる限り効率的に知識をキャッチアップしたくて学習したときの道のりです。

同じように困っていらっしゃる方のお役に立てばと思い、記事にしてみました。この道のりの通りに進んでいただければ、時間を無駄にすることなく、多少なりともスムーズに知識をキャッチアップできると思います。

# 主要な概念や全体像を理解するまでの道のり
いきなり詳細に踏み込んでも、つまりいきなりFargateなどのAWSサービスを使っても、すぐに迷子になることは目に見えていましたので、まずは全体感や重要な概念、用語を理解しようと思いました。

そこで色々と調べていると、次の記事を見つけました。ものすごく分かりやすかったです。

 * 「それコンテナにする意味あんの？」迷える子羊に捧げるコンテナ環境徹底比較　（https://dev.classmethod.jp/cloud/aws/cmdevio2019-container/
）
  * この記事で紹介されていた以下リンクも、後から読もうと思っています。
     * Best practices for writing Dockerfiles（https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
）
     * [AWS Black Belt Online Seminar] Docker on AWS レポート [12-factor App]（https://dev.classmethod.jp/cloud/aws/black-belt-docker-on-aws-2017/ ）

これを読んでよく分かったのは、以下の点です。

 * レジストリとは、Docker Hubのようなもので、Dockerイメージを管理するところ。AWSだとECRがこれにあたる。
 * それとは別次元で、以下がある。
   * コントロールプレーン
     * コンテナ数の管理など、コンテナの管理をするもの。データプレーンを起動する役割したりする。ECSがこれにあたる。
   * データプレーン
     * 実際にコンテナが動く場所で、EC2やFargateがこれにあたる。


ただ、上記の記事を読んでも、ECSのクラスター、サービス、タスク、コンテナの概念はよく分かりませんでした。うーむ、難しい・・。

また、それらとデータプレーン（EC2, Fargate）との違いについても、ほぼ理解できませんでした。

こういったトピックについては、以下の記事がとても分かりやすいです。

 * Amazon EC2 Container Service(ECS)の概念整理（https://qiita.com/NewGyu/items/9597ed2eda763bd504d7 ）

### DockerコンテナをFargateで動かすことの素晴らしさ

こうやって調査をしていきますと、Fargateというサービスの何が素晴らしいのか、という点が見えてきました。

DockerコンテナをFargateで動かすということは

 * PaaS（Elastic Beanstalkなど）よりも自由度が高くなり
 * IaaS（EC2など）よりも生産性が高くなる（DockerのインストールやOSセキュリティパッチの適用などから解放される）

ということです。一言でいうと「良い塩梅でめっちゃいい」ということですね。

EC2（つまりIaaS）でコンテナを動かすことはできます。しかし、それにはEC２インスタンスを構築し、Dockerという基盤をインストールする必要があります。そしてOSセキュリティパッチなどの面倒を見続けなければなりません。それは、ものすごく大変なことです。

そのあたりの大変さをAWSが引き受けてくれる、というのがFargateの素晴らしいところです。私たちは、Dockerイメージさえつくれば良いのです。

こういった点から、色々な制約はもちろんあるものの、マネージドの恩恵を最大限に受けるようにした方が得策だと感じてきました。

### Dockerfileを使う「docker build」と、docker-compose.ymlを使う「docker-compose」の関係
Dockerについてまだ慣れていない私は、この二つの関係がよく分からず混乱していました。

この点については、以下の記事で分かりやすく解説されています。

 * Dockerfileとdocker-compose（https://qiita.com/koka/items/3d3d4ee5680f92a0ad89 ）

平たく言いますと、「docker-composeは、docker buildを包含するもの」、ということです。

docker-composeではDockerイメージのビルドや、他のDockerコンテナとの連携などを制御できます。



# 実際に動かしてみるまでの道のり

主要な概念や全体像を理解できましたので、今度は実際にECR, ECS, Fargateを使って、Dockerイメージを動かしてみることにしました。

私の場合、自作したアプリを動かしてみたかった関係で、Spring Bootが動くコンテナをつくってみようと思いました。

その際、以下の記事を参考にさせていただきました。

 * Docker上でSpring Bootを動かしてみる（https://qiita.com/tkani/items/ed56229330f00a333d5e ）

こちらの記事を参考に、以下のDockerfileを、Mavenプロジェクト直下に作成しました。

```dockerfile:Dockerfile
FROM openjdk:8-jdk-alpine

COPY target/my-app-0.0.1-SNAPSHOT.jar my-app.jar

ENTRYPOINT ["java","-jar","/my-app.jar"]
```

そして、Dockerイメージをつくります。

```terminal
$ docker build ./ -t my-app
```
tオプションは作成したイメージにどんな名前をつけるか、を指定するものです。

ところが、このイメージの名前というものが意外に分かりづらいのです。ここに指定したものは、何を意味するのか、何に影響してくるのかが、分かりません。

tオプションについては、以下の公式リファレンスで解説されています。

 * http://docs.docker.jp/engine/reference/commandline/build.html#t
 * http://docs.docker.jp/engine/reference/commandline/tag.html

しかし、これらを読んでもイマイチよく分かりません。他の記事を調べても「なるほど！そういうことか！」という思いに至ることができませんでした。

公式リファレンスや先人の解説記事の内容をつなぎ合わせると、イメージ名は以下のものだと分かってきました。

### 「イメージ名」とは？

イメージ名というのは、全世界のなかでイメージを一意に特定するためのIDみたいなものです。
では、どうやって一意に特定するのかというと

 * どのレジストリで管理されているか？
    * レジストリ、というのはDockerHubのようにDockerのイメージを管理するところです。
    * 平たく言うと、DockerHubなのか？ECRなのか？全く別のホストなのか？、ということです。
    * ですから、ここにはホスト名やIPアドレス、およびポート名が記載されることになります。
 * そのレジストリの中の、なんというリポジトリなのか？
    * リポジトリというのは、レジストリの中にあるもので、Githubのリポジトリのようなものです。
    * DockerHubではユーザー名によって名前空間が区切られていますから、ここが[ユーザー名]/[リポジトリ名]という構造になります。一方、そのようにユーザー名で区切られていない場合は、シンプルに[リポジトリ名]（スラッシュで区切られない）という感じです。
 * そのリポジトリの中の、どのタグか。
    * これもGithubのタグと同じようなものです。1.0とかバージョン番号にするのが典型ですね。
    * `docker build -t`のtオプションでタグ名を省略すると、latestがデフォルトで付きます。

イメージ名は以上の３要素から構成され、具体的には以下の構造です。

`レジストリ名（多くの場合ホスト名）` / `リポジトリ名（DockerHubの場合は、「ユーザー名/リポジトリ名」）` : `タグ名`

では、先ほどのように

```terminal
$ docker build ./ -t my-app
```

と指定した場合はどうなるのでしょうか？

ホスト名が省略されていますので、公式リファレンスの

>ホストの指定が無ければ、デフォルトで Docker の公開レジストリのある registry-1.docker.io を使います。

の記述に基づいて、`registry-1.docker.io`であると見なされます（つまりDockerHubだと見なされます）。

タグ名が省略されていますので、`latest`がデフォルトで指定されます。

つまり、以下で指定したのと同じ意味になる、ということだと私は解釈しています。

`registry-1.docker.io/my-app:latest`

### 作成したDockerイメージを、ECRに登録します

ローカルPCで先ほどつくったDockerイメージを、ECRに登録していきます。

この手順については公式ガイドで解説されています。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/docker-basics.html#use-ecr

以下、この公式ガイドの手順どおりに実行したことについて、解説してきます。

ECRでは、レジストリはユーザー単位に作成されます。ここはDockerHubとは異なりますね。DockerHubは全体で一つのレジストリで、その中の名前空間をユーザー名で分割してる感じです。

ですので、どのレジストリにアクセスするかは、aws cliでログインしているユーザに紐づくレジストリとなります。このため、コマンドにはレジストリを指定する内容が出てきません。

まずは、レジストリという自分専用の部屋に、今回のDockerイメージを格納するための空間（リポジトリ）を作成します。

```terminal
$ aws ecr create-repository --repository-name my-app --region ap-northeast-1
```

すると、以下の結果がかえってきます。

```terminal
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:{アカウントID}:repository/my-app",
        "registryId": "{アカウントID}",
        "repositoryName": "my-app",
        "repositoryUri": "{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app",
        "createdAt": 1570824304.0
    }
}
```

次に、リポジトリーに登録したDockerイメージに、ECR用のイメージ名（tag）をつけます。

```terminal
$ docker tag my-app {アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app
```

ここでついに、イメージ名にホスト名を明示的に追加したわけです。

 * `{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com`というECR上の私専用の部屋（レジストリにある、
 * `my-app`というリポジトリに配置するイメージだよ

ということを、イメージ名が表しています。なお、タグについては省略しましたので、自動的に「latest」が付与されます。


次に、レジストリ（ECR）にログインする必要があるのですが、「ログインするためのコマンド」を取得するためのコマンドを叩かねばなりません。

```terminal
$ aws ecr get-login --no-include-email --region ap-northeast-1
```
すると、以下のコマンドを実行するよう、指示されます。

```terminal
docker login -u AWS -p {ものすごく長い文字列。12 時間有効な認証トークン、つまりパスワード。} https://{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com
```

このコマンドを実行してECRにログインします。（実際には、get-loginコマンドの結果を`eval`したら楽だと思います）

そして、さきほどつけたタグ名を指定して、ECRにイメージを登録します。

```terminal
$ docker push {アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app
```


```terminal
The push refers to repository [{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app]
ffb5690b4e55: Preparing 
ceaf9e1ebef5: Preparing 
9b9b7f3d56a0: Preparing 
f1b5933fe4b5: Preparing 
no basic auth credentials
```

このようにno basic auth credentialsが出てしまった場合は、以下を参考に対応しましょう。
https://qiita.com/NaokiIshimura/items/1886dbd04631c3f7d0e1

うまくいくと・・・

```terminal
The push refers to repository [{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app]
ffb5690b4e55: Pushed 
ceaf9e1ebef5: Pushed 
9b9b7f3d56a0: Pushed 
f1b5933fe4b5: Pushed 
latest: digest: sha256:{ハッシュ値}
size: 1160
```

のようになります。

管理コンソールから見ると・・・

<img width="933" alt="スクリーンショット 2019-10-12 5.30.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455418/3dade738-006f-3678-d821-9f8e8452a059.png">
<img width="934" alt="スクリーンショット 2019-10-12 5.31.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455418/7afbbfdf-d456-be67-f853-94a25bb40085.png">


しっかり登録されています！よかった！

### ECRに登録したDockerイメージを、ECSを使ってFargateの上で動かします。

次は、このECRに登録したdockerイメージを、Fargateの上で動かしてみましょう。Fargateで動くDockerコンテナの管理は、ECSで行います。

この手順については、公式ガイドで解説されています。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_GetStarted_Fargate.html

ガイドに記載のとおり、以下ウィザードを使ってECSおよびFargateをセットアップできます。
https://console.aws.amazon.com/ecs/home#/firstRun

私が分かりづらかった点を、以下に記載します。

#### ポートマッピング
ポートマッピングをSpring Boot（のtomcat）が待ち受けているポート番号に指定します。たとえば、Tomcatが5000番ポートをリッスンする設定にしているならば、5000を指定する必要があります。Fargateをつかう場合、前面で待ち受けるポートと、コンテナ側のポート番号は同じじゃないとダメみたいです。それはもはや「マッピング」ではないと思いますが・・。

#### セキュリティグループのインバウンドルール
ポートマッピングでFargateが待ち受けるポート番号を、Tomcatが待ち受けるポート番号と同じにしないといけないので、そのポート番号でFargateへ流入してくるトラフィックを許可しないといけません。

#### Fargateから他のAWSサービスを呼び出す場合は、権限設定が必要
Fargateに限らない話ですが、他のAWSサービスを呼び出す権限がないのにそのサービスを呼び出そうとすると、エラーが起こります。

今回のアプリの場合、Fargate（で動くSpring Bootアプリ）から、DynamoDBを呼び出していました。

ECSの管理コンソールからログが見れるのですが、そこでログを見て見ると・・・

```terminal
Caused by: com.amazonaws.SdkClientException: Unable to load AWS credentials from any provider in the chain: [EnvironmentVariableCredentialsProvider: Unable to load AWS credentials from environment variables (AWS_ACCESS_KEY_ID (or AWS_ACCESS_KEY) and AWS_SECRET_KEY (or AWS_SECRET_ACCESS_KEY)), SystemPropertiesCredentialsProvider: Unable to load AWS credentials from Java system properties (aws.accessKeyId and aws.secretKey), com.amazonaws.auth.profile.ProfileCredentialsProvider@46393853: profile file cannot be null, com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper@53f52e4e: Unable to load credentials from service endpoint]
```

というエラーが出ていました。

↓整形すると・・・

```terminal
Caused by: com.amazonaws.SdkClientException: 
Unable to load AWS credentials from any provider in the chain: 

[
EnvironmentVariableCredentialsProvider: 
 Unable to load AWS credentials from environment variables
 (AWS_ACCESS_KEY_ID (or AWS_ACCESS_KEY) and
  AWS_SECRET_KEY (or AWS_SECRET_ACCESS_KEY)), 

SystemPropertiesCredentialsProvider: 
 Unable to load AWS credentials from Java system properties
 (aws.accessKeyId and aws.secretKey),

com.amazonaws.auth.profile.ProfileCredentialsProvider@46393853:
 profile file cannot be null, 

com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper@53f52e4e: 
 Unable to load credentials from service endpoint
]
```

これを素直に読めば「どこかにcredentialを置け」、と指示されていることになります。

しかし、その前にそもそも、EC2やFargateからAWSのAPIを呼び出すときは、どのユーザーのAWS_ACCESS_KEY_IDとAWS_SECRET_KEYを指定すべきなのでしょうか？

普通に考えたら、サーバーや設定画面に、アクセスキーやシークレットキーを配置するなど、セキュリティ的に考えられませんから、実行しているFargate, EC2のロールに権限をつけることで対象のサービスを呼び出したいところです。

先ほどのエラーは、以下の流れを経て発生しています。

 * タスクを実行するロールに、対象のAWSサービス（今回はDynamoDB）にアクセスする権限が付与されていない（上記エラーメッセージの４つ目が該当します）。

 * しょうがなく、アクセスキーなどを探しにいったところ、見つからなかった（上記エラーメッセージの１つ目から３つ目が該当します）。



ということで、そもそもタスクを実行するロールに適切な権限さえついていれば、問題がないということです。

これを実現するには、ECSのタスク定義でタスクロールを適切に設定すれば良いです。

<img width="949" alt="スクリーンショット 2019-10-12 9.52.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455418/03c05c93-e88a-bec4-d714-d020f9cce5dc.png">


私の場合は、この「タスクロール」に何のロールも設定されていませんでした。ecsTaskExecutionRoleはデフォルトで用意されているもののようで、ひとまずこのロールを設定します。

あとは、対象のAWSサービスを呼び出す権限を、このロールに付与すればOKです。

ecsTaskExecutionRoleのリンクから、ロールの設定画面に飛べます。

<img width="953" alt="スクリーンショット 2019-10-12 9.54.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455418/18aeb1c9-f528-abad-4572-cd88abbf342f.png">


私の場合は、Fargateで動かすコンテナからDynamoDBを使いたいので、ここにAmazonDynamoDBFullAccessを付与してみました。（権限が強すぎるとは思いますが・・）

すると、先ほどのエラーも出ず、通常どおりに動作しました！

# しっかり学ぶ道のり
Dockerそのものについては、こちらがとてもオススメです。

自宅ではじめるDocker入門
https://www.kohgakusha.co.jp/books/detail/978-4-7775-2072-5

基本的な概念から、Dockerのインストール、基本的なコマンド、複数コンテナの構成まで、自宅でハンズオンで学べます。2019年2月の本なので、そこそこ新しいかなと思います。

# 終わりに
以上、私のキャッチアップの道のりをご紹介しました。

aws cliの導入手順などは端折ってしまいましたが、誰かのお役に立てたらと思います。
ご覧いただき、ありがとうございました。