Docker を使って Djangoチュートリアルの Polls アプリを AWS ECS へデプロイするサンプルのようなもの

# はじめに

こちらはDjangoのチュートリアル「[はじめての Django アプリ作成、その１](https://docs.djangoproject.com/ja/2.2/intro/tutorial01/)」のPolls(投票)アプリを Amazon Web Service(AWS) の Elastic Container Service(ECS) へデプロイするサンプル（のようなもの）です。

nginx + uwsgi + python + django + MySQL DB という構成で docker-compose を使っています。
また、デプロイには AWS CLI と ECS CLI を使い AWSコンソールは(ほとんど)使いません。（EC2インスタンス への ssh 接続でセキュリティグループを編集するときにだけコンソールを使用します）

前半はローカル環境上で docker-compose を使い Polls アプリを作成して動かすところまで。
後半で前半に作成した Polls アプリを ECS へデプロイします。

ソースは GitHub 上に公開しています。
（但しDjangoアプリの部分はこれから作るため含まれていません。）
https://github.com/Brokenumbrella/django-ecs-sample
この説明の中でも「１. 前準備」の中で上記から clone します。

### ここでは説明しないこと
- git や docker のインストール方法と使い方など。
- AWS アカウントの取得方法や AWS の仕組みなど。
- AWS CLI, ECS CLI のインストール方法や使い方など。

### 前提条件
- git をインストールしている。
- docker をインストールしている。
- AWS のアカウントを持っている（フルアクセス出来るものが良いと思います）。  
  もしAWSのアカウントが無くても前半のローカル環境で動かす所までは出来ます。
- AWS CLI をインストールしている。  
  インストール方法は後半でAWSサイトへのリンクを載せています。  
  そこでインストールするのでも構いません。  
  インストールしなくても前半のローカル環境で動かす所までは出来ます。
- ECS CLI をインストールしている。  
  インストール方法は後半でAWSサイトへのリンクを載せています。  
  そこでインストールするのでも構いません。  
  インストールしなくても前半のローカル環境で動かす所までは出来ます。
- インターネットに接続できる。
- 作るプロジェクトの名前は mysite です。  
  （変更したい場合は Grep で mysite の文字列を全て変更してください）

Python は docker コンテナで構築するため事前にインストールしておく必要はありません。
と言いたい所ですが、AWS CLI が Python 2.7 もしくは 3.4 以降を使います。
AWS CLI をインストールする際は入っていなければ Python のインストールが必要です。

### 動作確認済みの環境

- Ubuntu 18.04.3 LTS
  Docker version 19.03.5, build 633a0ea838
  docker-compose version 1.25.0, build 0a186604

- Windows 10 Enterprise 1903 build 18362.418
  Docker version 19.03.5, build 633a0ea
  docker-compose version 1.24.1, build 4667896b
  (Docker for Windows を使用)

**【注意】Windows ではコマンドの実行を Git-Bash 上で行って下さい。**
**PowerShell や コマンドプロンプト では id コマンドが使えず動きません。**

### 構成
- OS  : Alpine3
- 言語 : Python3(Python公式の DockerImage をベースにしています) 
- フレームワーク : Django2
- WSGIサーバー : uwsgi
- Webサーバ : nginx 
- データベース : mysql8

### 各インストールバージョン
- Alpine 3.1.0
- Python 3.7.4
- Django 2.2.10 以上 3.0 未満
- uwsgi  2.0.18 以上 3.0 未満
- nginx  1.17.8
- MySQL  8.0.19
- mysqlclient 1.4.6 以上 2.0 未満

動作確認は上記の一番低い（確認時のリリース）バージョンで行っています。
Python と Alpine は Python の公式リポジトリにあればどれでも使う事が出来ると思います。
nginx の別バージョンを使いたい場合は ./docker-compose.yml と ./web/Dockerfile-nginx を編集して下さい。
MySQL の別バージョンを使いたい場合は ./docker-compose.yml と ./web/Dockerfile-mysql を編集して下さい。
Django, uwsgi, mysqlclient の別バージョンを使いたい場合は ./web/requirements.txt を編集して下さい。
※Django は3.0がリリースされていますが、テストできておらず 2.2 を対象としています。
※Python 3.8.1 + apine 3.11 ではDjangoアプリ作成後の動作確認だけで、アプリの作成はチェック出来ていません。多分動くのでは無いかと・・・

### お約束
- 個人的なテストとして作成したものでなんら保証が出来るようなものではありません。
- 試される際は自己責任でお願いします。
- 私自身の理解が足りておらず「コピペで動いている」ところもあります。
- とはいえ、この情報が誰かの役に立てばいいなと思っています。

# １. 前準備

## 開発用のソースを GitHub から取得(clone)します

### 1. プロジェクト用のフォルダを作り移動します
例）home フォルダに myprojects というフォルダを作成してそこに取得する場合。

```sh:shell
$ mkdir ~/myprojects/  && cd ~/myprojects/
```
  このフォルダ下に django-ecs-sample というフォルダが作成されます。

### 2. GitHub からテストプロジェクトを clone します
```sh:shell
$ git clone https://github.com/brokenumbrella/django-ecs-sample.git
```
**Windowsで git の改行コードの自動変換がONの場合には注意が必要です**
git のインストール方法によっては、改行コードが CR+LF で取得されます。
もし web/run-my-app.sh ファイルを VSCode などのエディターで開いた際に改行コードが CRLF だった場合は、LF に変更して下さい。
linux では CRLF のシェルスクリプトファイルは実行できずエラーとなります。
（私はこれの解決に１日かかってしまいました）

### 3. clone したフォルダ及びファイルの構成

django-ecs-sample フォルダは下記の構成になっています。

```sh
+ db
  + conf
    - mysql_my.cnf                # MySQL8 で使う設定ファイル
+ nginx
  - uwsgi_params                  # uwsgi の設定ファイル
  + conf
    - default.conf                # nginx の設定ファイル
+ web
  - Dockerfile                    # docker-compose でビルドする際に使う Dockerfile
  - Dockerfile-mysql              # ECS へデプロイする際に使う MySQL用 Dockerfile
  - Dockerfile-nginx              # ECS へデプロイする際に使う nginx用 Dockerfile
  - Dockerfile-web                # ECS へデプロイする際に使う Web(Django)アプリ用 Dockerfile
  - requirements.txt              # Python のライブラリ定義ファイル
  - uwsgi.ini                     # uwsgi 用の設定ファイル（プロジェクトフォルダーへコピーします)
  - run-my-app.sh                 # ECS へデプロイする際に使う Webアプリ起動用シェルスクリプト
- .gitignore
- docker-compose.yml              # docker-compose 用ファイル
- docker-compose-ecs.yml          # ECS へデプロイする際の docker-compose ファイル
- docom.sh                        # docker-compose を簡単に利用する為のスクリプト
- esc-params.yml                  # ECS へデプロイする際のタスク定義ファイル
- readme.md                       # 簡単なドキュメント
+ log                             # ログ保存用のフォルダ
  + uwsgi
    - __init__.txt
+ static                          # staticファイル用のフォルダ
  - __init__.txt
+ src                             # プロジェクトを入れるソースフォルダ
  - __init__.txt
```
作成する Django アプリのソースファイルは ./src/ フォルダに置きます。
log, static, src はdocker-composeする際にマウントする際に必要になるため、Gitリポジトリ上では単なる空フォルダです。
gitで空フォルダを保持するために \_\_init\_\_.txt を入れています。

## dockerコンテナ内でのユーザーの追加と変更について

  dockerコンテナに（ホストマシンの）現在のユーザーを追加するため linux の id コマンドを使っています。
  ユーザー切り替えを行う事で docker 側でマウントしたフォルダに現在のユーザーと同じ権限でファイルやフォルダを作成出来ます。
  ユーザーを指定していない場合は root ユーザーでファイルが作られるため、ローカル環境で編集するには root への昇格が必要になります。
  そういった手間を減らすためと、docker は root ユーザー以外で運用する方が良いらしいのでこのようにしています。
  このため動作確認済みの環境以外では動かない可能性もあります。ご注意下さい。

### **docom.shについて**
  上記の id コマンドを使ったユーザーの設定などを簡略化するため `docom.sh` というスクリプトファイルを同封しました。
  docker-compose を起動する際にユーザーID等をセットするためのシェルスクリプトで下記のようになっています。

```sh:shell
$ cat docom.sh
DUID=$(id -u) DGID=$(id -g) MYSQL_PW=PWRoot1 docker-compose $1 $2 $3 $4 $5 $6 $7 $8 $9
```

  環境変数の DUID と DGID には id コマンドを使ってユーザーIDとグループID を入れています。
  （ちなみに名前ではなく 1000 といった番号です）
  また MYSQL_PW=PWRoot1 の部分は MySQL DB のルートユーザーパスワードになっています。
  実際に使う際は変更して頂くようお願いします。（このままでも動きますが）
  変更の際は web/mydb.cnf(プロジェクトフォルダへコピーしてればそちらも) と docker-compose-ecs.yml にも同じパスワードを設定して下さい。
  "PWRoot1" を Grep で一括変換する方が良いと思います。

  上記の設定を一時的に環境変数へセットし docker-compose を呼び出しています。
  今後はコード簡便化のため、このスクリプトを使って説明していきます。
  **このスクリプトは開発やテストを目的としており、セキュリティに注意すべき環境では他の方法を検討して下さい。（ここ大事）**

# ２．ローカル環境で Django Polls アプリを動かしてみよう

それでは本題に入りましょう。
docker compose を使いローカル環境で nginx, MySQL, Web(Djangoアプリ) の３つのコンテナを起動して動かし Polls アプリを作ります。

## 1. web 用の docker image を作成するためビルドします
まず Python+Django 環境を構築するために docker-compose build を行います。
下記コマンドを docom.sh ファイルのあるフォルダで実行します。

```sh:shell
$ ./docom.sh build web
```
初めてビルドする際には時間がかかります。
(ベースコンテナをダウンロードするためインターネットへの接続スピードにも影響されます)
下記のように `Successfully built` と出ればビルド成功です。

```sh:shell
Successfully built 036160743a81
Successfully tagged django-ecs-sample_web:latest
```
ここでビルドに使うファイルについて簡単に説明します。
####(1)Web コンテナをビルドするための Dockerfile です

```python:./web/Dockerfile
FROM python:3.7.4-alpine3.10
#  alpine では apk を使う（add でインストール）
# nginx, suprevisor, uwsgi のインストール（gcc,build-base,linux-headersはuwsgiインストール時に使うためインストールする）
# libffi-dev, mysql-dev, mysql-client, python3-dev は mysql を使うためにインストール
RUN apk update && apk add --no-cache \
    gcc \
    build-base \
    linux-headers \
    libffi-dev \
    mysql-dev \
    python3-dev && \
    pip3 install --upgrade pip

# requirements.txt から Django などの必要なライブラリをインストール
# 不要になった gcc などをアンインストール
COPY ./requirements.txt /code/
RUN pip3 install -r /code/requirements.txt && \
    apk del gcc build-base linux-headers libffi-dev python3-dev

# 8001番ポートを開放する（ことを宣言する）
EXPOSE 8001
# 作業用フォルダを /code/ にする
WORKDIR /code/

# ユーザーを作成してカレントユーザーにする（一般ユーザーで動かすため）
# docker-compose.yml の args に指定した uid と gid を使えるように宣言する
# ユーザー名、グループ名は id と一緒にしておく
ARG DUID
ARG DGID

# ユーザーの作成
RUN addgroup -S -g $DGID $DGID && \
    adduser -S -u $DUID -g $DGID $DUID

# ユーザーを切り替える
USER $DUID

# 実行コマンドは docker-compose.yml の方で指定するためここはシェルを指定しておく
CMD ["/bin/sh"]
```
- python:3.7.4-alpine3.10 イメージを元にしています。  
  DockerHubに公開されていれば、FROM python:3.7.4-alpine3.10 の部分を変更する事で別バージョンのPythonやalpineを使うことも出来ます。  
  但しバージョンによってはテストアプリが動かない可能性もあります。
- ライブラリのインストール時に必要な gcc 等をインストールしています。
- nginx との接続用に 8001 番ポートを開けています。
- /code/ を作業フォルダにしていますが、docker-compose.yml でローカル環境の ./src/ フォルダへマウントします。
  つまりこのコンテナ内で /code/ 下に置いたファイルはローカル環境の ./src/ 下に置かれます。
  (マウントについては dockerのドキュメント等をご参照下さい)
- mysql-client は Djangoアプリから MySQL を使うのに必要がないためインストールしていません。
- ユーザーIDとグループIDを環境変数から取得してユーザーを作成します。

####(2)インストールする Python ライブラリを指定するファイルです。

```python:./web/requirements.txt
django>=2.2.10, <3.0       # django 2.2.10 で動作確認済み、2.?.? の間は動くと仮定している
uwsgi>=2.0.18, <3.0       # uwsgi 2.0.18 で動作確認済み、2.?.? の間は動くと仮定している
mysqlclient>=1.4.6, <2.0  # mysqlclient 1.4.6 で動作確認済み、1.?.? の間は動くと仮定している
```
django, uwsgi, mysqlclient をインストールしています。
こちらで動作確認したのはそれぞれ >= で書かれているバージョンです。
もしアプリが起動しない場合、>= を == に変更し、,以降をコメントアウトする事でバージョンを固定してみて下さい。

```python:例）
django==2.2.10
```
####(3)docker compose で使用するYAMLファイルです。

```yaml:docker-compose.yml
version: '3'

services:
  db:                                         # MySQL DB 用の設定（この db という名前で web から DB HOST の指定ができる)
    image : mysql:8.0.19
    container_name: mysql.db
    volumes:                                  # マウントフォルダの指定
      - ./db/data:/var/lib/mysql              # データの永続化を行う
      - ./db/conf/:/etc/mysql/conf.d/         # 設定ファイルをここから読み込ませる
      - ./db/sqls:/docker-entrypoint-initdb.d # 初期データを与える場合はここから読み込ませる
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_PW}       # rootパスワードの設定
      - MYSQL_DATABASE=mysite                 # 作成するDatabase名
      - TZ=Asia/Tokyo                         # タイムゾーンを日本時間に変更

  web:                                        # Web(Django)アプリケーション 用の設定
    build:                                    # ビルド設定
      context: ./web                          # ./web/Dockerfile を用いてビルドする
      args:                                   # Dockerfile へ渡す環境変数の指定
        - DUID=${DUID}
        - DGID=${DGID}
    environment:                              # 実行時に設定する環境変数
      - MYSQL_HOST=${MYSQL_HOST}
    user: ${DUID}:${DGID}                     # 実行時のユーザー指定
    container_name: django.web
    command: uwsgi --ini /code/mysite/mysite/uwsgi.ini
    volumes:                                  # マウントフォルダの指定
      - ./src:/code                           # アプリケーションのソースフォルダ
      - ./static:/static                      # static ファイルフォルダ
      - ./log/uwsgi/:/var/log/uwsgi           # uwsgi の設定ファイルをここから読み込ませる
    expose:                                   # 開放するポート
      - "8001"
    links:                                    # db コンテナを先に立ち上げてから web を立ち上げる
      - db

  nginx :                                     # nginx 用の設定
    image: nginx:1.17.8
    container_name: nginx
    ports:                                    # 8080 ポートを 80 ポートへ
      - "8080:80"
    volumes:                                  # マウントフォルダの指定
      - ./nginx/conf:/etc/nginx/conf.d        # nginx 用の設定ファイルをここから読み込ませる
      - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params  # uwsgi のパラメータファイルをここから読ませる
      - ./static:/static                      # static ファイルフォルダ
      - ./log/nginx/:/var/log/nginx           # ログファイルをここへ保存する（永続化）
    depends_on:
      - web                                   # web コンテナの後から起動させる
```

## 2. Django project を作成します
上記 1. で作った docker 環境を使いプロジェクトを作成します。
django-admin startproject コマンドを使い mysite という名前で新規作成します。
（mysite 以外の名前にする場合は MySQL DB のテーブル名等も変更する必要があり、 grep 検索などで置換して下さい。）

下記コマンドを実行してプロジェクトを作成します。

```sh:shell
$ ./docom.sh run web django-admin startproject mysite
```
コンテナ内で /code/ フォルダに、ローカル環境では ./src/ フォルダにプロジェクトが作成されます。
もし ./src/mysite/ フォルダが作成されていない場合はMySQLサーバーの立ち上げで失敗している可能性があります。
`db/data/` フォルダのファイルとサブフォルダを全て削除してやり直してみて下さい。

## 3. `./web/uwsgi.ini` ファイルを `./src/mysite/mysite/` へコピーします

ローカル環境で下記コマンドを使ってコピーします。

```sh:shell
$ cp ./web/uwsgi.ini ./src/mysite/mysite/
```

これは uwsgi 用の設定ファイルです。
コピーする事でコンテナ内では `/code/mysite/mysite/uwsgi.ini` に配置される事になります。

```ini:./web/uwsgi.ini
[uwsgi]
# この prjname に django-admin startproject で作成したプロジェクト名を指定します。
prjname=mysite
basepath=/code/%(prjname)/
chdir=%(basepath)
module = %(prjname).wsgi:application
socket = :8001
wsgi-file = %(basepath)%(prjname)/wsgi.py
logto = /var/log/uwsgi/uwsgi.log
py-autoreload = 1
# usage:
#  このファイルは django-admin startproject の後に /code/prjname/prjname/ へコピーする。
```

## 4. nginx の設定ファイルについて

これは nginx 用の設定ファイルです。
このファイルは nginx コンテナを実行する際にマウントさせて読み込ませるためコピーなどは必要ありません。(こんなファイルだという説明だけです）

```def:./nginx/conf/default.conf
upstream django {
  ip_hash;
  server web:8001;
}
server {
  # the port your site will be served on
  listen      80;
  server_name localhost compute.amazonaws.com; # substitute your machine's IP address or FQDN
  charset     utf-8;
  client_max_body_size 75M;   # adjust to taste
  location /static {
    alias /static;
  }
  location / {
    include     /etc/nginx/uwsgi_params; # the uwsgi_params file you installed
    uwsgi_pass  django;
  }
}
```

- django との接続に web コンテナの 8001 ポートを使うように指定します。（ここは理解不足です）
- server_name では localhost と EC2 の ２つを指定しています。実際にはドメインやIPアドレスを指定するようにして下さい。
- location /static{ alias } で /static へのアクセスを nginx コンテナの /static フォルダにマッピングさせています。   
  ここは後ほど collectstatic で /static フォルダに集約させます。
- location /{} で /static 以外のアクセスを全て django に処理させるようにしています。

## 5. MySQL DB を使うように設定を行います。
Django はデフォルトで DataBase に sqlite3 を使うようになっています。
ここでは MySQL DB を使わせるように設定を変更します。
  
- ./src/mysite/mysite/settings.py を編集します。

```python:./src/mysite/mysite/settings.py
DATABASES = {
  'default': {
    'ENGINE': 'django.db.backends.sqlite3',
    'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
  }
}
```
上記の部分を下記のように書き換えます。

```python:./src/mysite/mysite/settings.py
DB_SETTING_FILE = os.path.join(BASE_DIR,'mydb.cnf')
DATABASES = {
  'default': {
    'ENGINE': 'django.db.backends.mysql',
    'OPTIONS':{
      'read_default_file':DB_SETTING_FILE,
    },
  }
}
```
DB ENGINE には django.db.backends.mysql を指定します。
MySQL 関連の設定は次に説明する mysql.cnf ファイルから読み込むように指定しています。
ついでに、Djangoで使う文字コードを日本語に、時間も日本時間へ変更しておきます。
LANGUAGE_CODE を 'ja' に、TIME_ZONE を 'Asia/Tokyo' に変更します。

```python:./src/mysite/mysite/settings.py
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'
```

- ./web/mydb.cnf を ./src/mysite/ へコピーします。  
Django から MySQL DB へアクセスするための設定をこのファイルに纏めています。
コードは下記のようになっています。

```ini:./web/mydb.cnf
[client]
  database = mysite
  user = root
  password = PWRoot1
  host = db
  port = 3306
  default-character-set = utf8mb4
```

- 'database' には docker-compose.yml の service: db: environment: MYSQL_DATABASE に指定したDataBase名を入れます。
- 'user' は root ユーザーでアクセスさせています。
- 'password' には docom.sh で指定した MYSQL_PW の値を指定して下さい。
- 'host' には docker-compose.yml で定義した service の名前 'db' を指定します。後は docker compose がよしなにやってくれます。
- 'port' は 一般的なMySQLで使用するポート番号 3306 を設定しています。
- 'default-character-set' で絵文字などに対応したutf8mb4 を指定しています。

## 6. docker-compose up でサーバーを立ち上げてアプリケーションを動かします

下記コマンドでサーバーが起動します。
（-d を指定してバックグラウンドでコンテナを実行させています。）

```sh:shell
$ ./docom.sh up -d
Starting mysql.db ... done
Starting django.web ... done
Starting nginx      ... done
```
上記のように３つのコンテナが起動したら、Webブラウザを立ち上げて http://localhost:8080 にアクセスします。
下記の Django のデモ画面が表示されれば問題なくサーバーが起動できています。
おめでとうございます！
![django-ecs-sample01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260738/06dcf0f8-0716-1dd3-45ef-11c86c04f47b.png)

## 7. docker-compose down サーバーを終了します
  下記コマンドを実行します。

```sh:shell
$ ./docom.sh down
Stopping nginx      ... done
Stopping django.web ... done
Stopping mysql.db   ... done
Removing nginx      ... done
Removing django.web ... done
Removing mysql.db   ... done
Removing network django-ecs-sample_default
```
上記のようにコンテナが停止・削除されます。
これ以降は `http://localhost:8080` へアクセスしてもエラーが返されます。

## 8. ログファイルはローカル環境の `./log/` フォルダ下に保存されています
  サーバーが起動しない等の場合は下記フォルダのログを見て原因を調べることが出来ます。

```sh:
./log/nginx/ : nginx が出力するログ
./log/uwsgi/ : uwsgi が出力するログ
```

## 9. Django アプリケーションを作ります
ここで作るアプリケーションはチュートリアルの `polls` です。
また以下２つの方法があります。どちらでも好きな方法で作る事が出来ます。

- **方法１：サーバーを起動させて docker exec によりコンテナ内に入って作業する**

&emsp; まずサーバーを起動していない場合は起動させます。

```sh:shell
$ ./docom.sh up -d
```

&emsp; サーバーが立ち上がったら docker exec で django.web に shell で接続します。
&emsp; (Windows10 の場合は winpty をつけて下さい)

```sh:shell
$ docker exec -it django.web sh
※windows10 の場合
$ winpty docker exec -it django.web sh
```
&emsp; `/code $` とプロンプトが出ればコンテナ内に入れています。
&emsp; misite プロジェクトフォルダ上で python manage.py startapp を実行します。

```sh:shell
/code $ cd mysite
/code $ python manage.py startapp polls
/code $ exit
```
&emsp; ※exit でコンテナから抜けます。

&emsp; 起動したサーバーを停止します。

```sh:shell
$ ./docom.sh down
```

- **方法２：docker-compose run web で作る場合**

アプリケーションの作成は manage.py のあるフォルダで行うためワークフォルダを指定する必要があります。
`docker-compose run -w //code/mysite/` で指定できるので、これを使います。
(ubuntu では /code/mysite/ でも動いたのですが、Windows10 では //code/mysite/ でなければ動きませんでした。)

```sh:shell
$ ./docom.sh run -w //code/mysite/ web python manage.py startapp polls
```

どちらの方法でもアプリケーションを作成できます。
ローカルに ./src/mysite/polls フォルダが出来ていればアプリケーションの作成は成功です。

## 10. アプリケーションへアクセスできるようにビューとurlsを指定します
  ここからは Django の Polls チュートリアルとほぼ同様のコーディング作業になります。

(1) ./src/mysite/polls/views.py を下記のように編集します。

```Python:./src/mysite/polls/views.py
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.") 
```

(2) ./src/mysite/polls/ フォルダに urls.py ファイルを作成し、下記コードを書きます。

```Python:./src/mysite/polls/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]
```

(3) 次に ./src/mysite/urls.py ファイルを下記のように書き換えます。

```Python:./src/mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]
```
このルーティングによって `http://localhost/polls/` へアクセスする事で 1 の inxex(request) が呼び出されレスポンスが返されるようになります。

## 11. ここまで出来た Polls アプリを動かしてみましょう
下記コマンドでサーバーを立ち上げます。

```sh:
$ ./docom.sh up -d
```
Webブラウザで http://localhost:8080/polls へアクセスします。
URL には /polls を付けて下さい。先ほどと同じ`localhost:8080`だけでは **Page not found(404)** エラーが出ます。
ブラウザ画面に

```
  Hello, world. You're at the polls index.
```
と表示されていればここまでは成功です。
サーバーを停止させます。

```sh:shell
$ ./docom.sh down
```

## 12. collectstatic でスタティックファイルを所定のフォルダへ集めます
AWS ECS などへ公開する場合は Javascript や css 、画像などの static ファイルを１箇所にまとめる必要があります。
また、今回 nginx で /static/ をホストしているためここで纏めておきます。
これらのファイルを纏めるために django では collectstatic が用意されています。
collectstatic を実行するには前準備をする必要がありますので、そこから説明します。

(1). はじめに ./src/mysite/mysite/settings.py の最後に STATIC_ROOT の設定を追記します。
  これを入れ忘れると collectstatic で下記のエラーが出ますので、これが出たらここに戻って確認して下さい。

```sh
django.core.exceptions.ImproperlyConfigured: You're using the staticfiles app without having set the STATIC_ROOT setting to a filesystem path.
```
では下記のように追記します。

```python:./src/mysite/mysite/settings.py
  |
  〜いろいろ〜
  |
  STATIC_URL = '/static/'
  STATIC_ROOT = STATIC_URL      # これを追加する
```

(2). collectstatic を行います。
サーバーが立ち上がっていなければ起動します。

```sh
$ ./docom.sh up -d
```
docker exec でコンテナに入ります。（ubuntu の場合）

```sh
$ docker exec -it django.web sh
```
(Windows10 の場合は winptyをつけて)

```sh
$ winpty docker exec -it django.web sh
```
`/code $` とプロンプトが出ればコンテナ内に入れています。

```sh
/code $ cd mysite
/code $ python manage.py collectstatic
/code $ exit
```
もしくは docker-compose run を使い以下の１行でも作成可能です。

```sh
$ ./docom.sh run -w //code/mysite/ web python manage.py collectstatic
```
./static フォルダにファイルが存在する場合は途中で下記のような問いが出ますので、yes を入力して下さい。

```sh
This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel:
```
./static フォルダに admin フォルダが追加されている事を確認できると思います。

## 13. Djangoチュートリアルに従い Polls アプリを作成します
  ここからは、下記公式サイトのチュートリアルのその２から７までを実際に行います。
  （「高度なチュートリアル」はやらなくても大丈夫です。）
  [はじめての Django アプリ作成、その2 モデルの作成](https://docs.djangoproject.com/ja/2.2/intro/tutorial02/#creating-models)

  チュートリアル内で `python manage.py 〜` という部分が出てきた際は「9.Django アプリケーションを作ります」と同じように docker exec もしくは docker-compose run を使います。
  例えば `python manage.py makemigrations polls` を行う場合下記の手順で実行してください。
  サーバーが立ち上がっていなければ起動します。

```sh
$ ./docom.sh up -d
```
ubuntu の場合

```sh
$ docker exec -it django.web sh
```
Windows10 の場合

```sh
$ winpty docker exec -it django.web sh
```
`/code $` とプロンプトが出ればコンテナ内に入れています。

```sh
/code $ cd mysite
/code $ python manage.py makemigrations polls
/code $ exit
```
**./docom.sh up -d はサーバーを起動してない場合に必要です。起動していれば docker exec ～ から実行してください。**
  またサーバーを起動しておけば、途中でもブラウザの表示更新(リロード)するだけ変更が適用されますので都度サーバーの起動、終了を行う必要はありません。
もしくは

```sh
$ ./docom.sh run -w //code/mysite/ web python manage.py makemigrations polls
```
でも行うことができます。
ただし残念ながら、superuser を作るコマンド `python mange.py createsuperuser` だけは docker-compose run では実行できません。ここではユーザー名などの入力を求められるからです。このため createsuperuser を行う際は docker exec 〜 を使って下さい。
それから、python shell を実行させている際にソースを修正しても起動している shell には反映されません。
shell を起動しなおす必要があります。
（起動しなおしたら必要であれば from datetime などの初期化処理からやり直します）

**ここを完了しなくても次の「２．AWS ECS へデプロイしよう」へ進みデプロイする事は出来ます。**
**"Hello, world. You're at the polls index."と出るだけですが・・・**

# ２．AWS ECS へデプロイしよう

## 1. AWS 側の準備

### (1) Python をインストールします
  AWS CLI をインストールするに先立ち、Python 2.7以降か 3.4 以降が必要になるためインストールします。
  既にインストール済みの場合は 2. へ進んでください。
  もし Python がインストールされているかわからない場合は、下記コマンドで確認出来ます。

```sh
$ python --version
```
Python 3 の場合は

```sh
$ python3 --version
```
バージョン番号が出ればインストールされています。（但し 2.7 or 3.4 未満の場合はアップデートが必要です）
ここでは　Python3 の最新版をインストールします。
[Python公式ページhttps://www.python.org/](https://www.python.org/)からダウンロード、インストールします。
Windows10 へインストールする場合はこちらも参考にさせていただきました。
[Python3のインストール](https://www.python.jp/install/windows/install_py3.html)
今回は最新安定板の3.8.1をインストールしました。
インストールが完了すると下記のようにして確認できます。

```sh
$ python --version
Python 3.8.1
```

### (2) AWS CLI のインストールと設定

AWS CLI の詳細は下記を参照してください。
[AWS コマンドラインインターフェイス](https://aws.amazon.com/jp/cli/)

インストールと設定は下記公式ページの手順で行ってください。
2020年2月現在では バージョン２は評価版での公開のため、バージョン１の方をインストールします。

[AWS CLI のインストール](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-install.html)

Windows10 では下記のページから MSI インストーラをダウンロードしてインストールしました。
また、Windows10では下記のドキュメントに従い PowerShell を使います。
[https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-windows.html#install-msi-on-windows](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-windows.html#install-msi-on-windows)

続けて設定を行います。
[AWS CLI の設定](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html)

これ以降の説明では「AWS CLI のかんたん設定」の `aws configure` を実行したとして進めます。
もし「複数のプロファイルの作成」で profile 名を指定した場合は `aws --profile profuser ecr 〜〜` とプロファイル名を指定してコマンドを実行してください。(profuser の部分には作成したプロファイル名を指定します)

### (3) ECS CLI のインストールと設定

ECS CLI の詳細は下記を参照してください。
[AWS ECS コマンドラインインターフェースの使用](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI.html)
インストールは下記公式ページの手順で行ってください。
現時点では Version2 はまだ評価版なので、Version1 をインストールします。
また、ここでは下記のドキュメントに従い Windows10 では PowerShell を使います。

[Amazon ECS CLI のインストール](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_CLI_installation.html)

ここも公式にお任せでOKかと思ったのですが、Windows でステップ２の「MD5 サムを使用した検証」をやる場合は注意が必要です。
（オプションなので飛ばす人はこの注意も必要ないです）
まずは手順通りに実行してみます。

```sh
ps c:\> Get-FileHash ecs-cli.exe -Algorithm MD5
Resolve-Path : パス 'C:\ecs-cli.exe' が存在しないため検出できません。
～云々～
```
のように、ecs-cli.exe ファイルが存在しないとエラーが出ました。
これは、ステップ１で `C:\Program Files\Amazon\ECSCLI` フォルダに ecs-cli.exe をダウンロードしているためです。
このためフルパスを指定して実行します。

```sh
PS C:\> Get-FileHash "C:\Program Files\Amazon\ECSCLI\ecs-cli.exe" -Algorithm MD5
  ```
これでハッシュ値がとれたと思います。
また、確認のためダウンロードした 'md5.txt' ファイルは確認が済めば不要なので削除して構いません。

## 2. docker image をビルドします

### (1) ./src/mysite/mysite/settings.pyを修正します
ALLOWED_HOST に何処でも動作するよう '*' を指定しておきます。
運用するサーバーが決まったらここには `hogehoge.com` や '100.100.100.100' などドメインやIPアドレスを指定します。
また、実運用の際には DEBUG = False にしましょう。

```Python:./src/mysite/mysite/settings.py
  〜いろいろ〜

  ALLOWED_HOSTS = ['*']       # ここ

  〜いろいろ〜
```

### (2) MySQL, nginx, web の設定やソースコードを含めた Dockerfile について説明します。
  前半のローカル環境では MySQL, nginx の設定ファイルを docker の volumes を使って指定しましたが、ECS上で動かすにはコンテナ内にそれらも含めます。
  また web の docker image には作成したアプリケーションのソースファイル(./src/ フォルダ以下)を含める必要もあります。
  そのため、デプロイ用に web, MySQL, nginx 用 の Dockerfile を作成し、それぞれにビルドします。
  各 Dockerfile は ./web/ フォルダに入っていますが、下記のようになっています。

```Dockerfile:./web/Dockerfile-mysql
FROM mysql:8.0.19

# 設定ファイルをコピーする
COPY ./db/conf/ /etc/mysql/conf.d/
```

```Dockerfile:./web/Dockerfile-nginx
FROM nginx:1.17.8

# 設定ファイルをコピーする
COPY ./nginx/conf /etc/nginx/conf.d
COPY ./nginx/uwsgi_params /etc/nginx/uwsgi_params
# static ファイルは nginx で返すためこのコンテナにコピーしておく
COPY ./static /static
```

```Dockerfile:./web/Dockerfile-web
FROM python:3.7.4-alpine3.10

# alpine では apk を使う（add でインストール）
# nginx, suprevisor, uwsgi のインストール（gcc,build-base,linux-headersはuwsgiインストール時に使うためインストールする）
# libffi-dev, mysql-dev, python3-dev は mysql を使うためにインストール
RUN apk update && apk add --no-cache \
  gcc \
  build-base \
  linux-headers \
  libffi-dev \
  mysql-dev \
  python3-dev && \
  pip3 install --upgrade pip

# requirements.txt から Django などの必要なライブラリをインストール
# 不要になった gcc などをアンインストール
COPY ./web/requirements.txt /code/
RUN pip3 install -r /code/requirements.txt && \
    apk del gcc build-base linux-headers libffi-dev python3-dev

# ソースファイルを /code/ フォルダへコピーする
COPY ./src/ /code/
# 実行時のスクリプトファイルをコピーする
COPY ./web/run-my-app.sh /code/

# 8001番ポートを開放する
EXPOSE 8001
# 作業用フォルダを /code/ にする
WORKDIR /code/

# ユーザーを作成してカレントユーザーにする（一般ユーザーで動かすため）
# docker-compose.yml の args に指定した uid と gid を使えるように宣言する
# ユーザー名、グループ名は id と一緒にしておく
ARG DUID
ARG DGID

# ユーザーの作成
RUN addgroup -S -g $DGID $DGID && \
    adduser -S -u $DUID -g $DGID $DUID

# uwsgi 用のログパスを追加
RUN mkdir /var/log/uwsgi/
RUN chown -R $DUID:$DGID /var/log && \
    chown -R $DUID:$DGID /code

USER $DUID
```

### (3) ３つのDockerfileをビルドして docker image を作ります

今回は docker コマンドの build を使います。
前半の docker-compose ではないため docom.sh は使いません、ご注意ください。
下記のコマンドでビルドを行います。

```sh
$ docker build -t django-ecs-sample-web -f ./web/Dockerfile-web --build-arg DUID=$(id -u) --build-arg DGID=$(id -g) .
$ docker build -t django-ecs-sample-mysql -f ./web/Dockerfile-mysql .
$ docker build -t django-ecs-sample-nginx -f ./web/Dockerfile-nginx .
```

- -t django-ecs-sample-web など、それぞれのイメージに対して今後利用しやすいように名前をつけています。
- -f でビルドに使う Dockerfile を指定しています。
- --build-arg DUID=$(id -u) --build-arg DGID=$(id -g) の部分はdocom.shで環境変数をdocker-composeへ渡していたのと同じです。
docker build では --build-arg オプションで環境変数を渡すところに注意が必要です。

## 3. docker-compose-ecs.yml について
  ecs へデプロイするために専用ファイルを用意しました。
  また各 image には仮の URL を指定しています。
  後ほど ECR にリポジトリを作成して、そのURLに書き換えます。

```yaml:docker-compose-ecs.yml
version: '3'

services:
  db:
    image : XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest
    volumes:
      - /db/data:/var/lib/mysql              # データの永続化を行う
    environment:
      - MYSQL_ROOT_PASSWORD=PWRoot1
      - MYSQL_DATABASE=mysite
      - TZ=Asia/Tokyo

  web:
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest
    command: sh /code/run-my-app.sh
    links:
      - db

  nginx :
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest
    ports:
      - "80:80"
    links:
      - web
```
この中で webコンテナの起動コマンドを下記のように変えています。
`web: command: sh /code/run-my-app.sh`
これは django の migrate で DB へのマイグレーション処理を行ってから uwsgi を起動するスクリプトです。
初回起動時に MySQL の準備が間に合わず migrate に失敗する事から成功するまで無限ループさせています。
無限ループは正直恐ろしいんですが、これに失敗するって事は MySQL が立ち上がらないということなので、いいかなと。
実際には他にもっとスマート（本来の）やり方があるんじゃないかと思います。
そもそも RDS などのマネージドサービス使えば前もって起動させておき、migrate なども別のタイミングで行うことが出来るはず、などなどのご意見もあると思いますが。
どなたかこの場合に他に何か良策があれば教えて頂けると助かります。
ともかく先へ進めるため下記のスクリプトを動かすようにしました。

```sh:./web/run-my-app.sh
#!/bin/sh

app=mysite
while :
do
  if python /code/$app/manage.py migrate; then
    break
  else
    sleep 1
  fi
done
uwsgi --ini /code/$app/$app/uwsgi.ini

exit 0
```

## 4. AWS ECR にリポジトリを作成し、dockerイメージをプッシュします

ECS へデプロイする際 docker image は ECR（Elastic Container Registry) か docker hub に置く必要があります。
今回は AWS で完結させたいので、先程ビルドした３つの docker image を AWS の ECRへプッシュします。

### (1) AWS ECR にプッシュ用のリポジトリを作成します

始めに下記コマンドで、web 用の django-ecs-sample-web リポジトリを作成します

```sh:shell
$ aws ecr create-repository --repository-name django-ecs-sample-web
```
作成に成功すれば下記のようにJSON形式で作成したリポジトリの情報が返されコンソール画面に表示されます。

```json:json
{ 
  "repository": {
    "repositoryArn": "arn:aws:ecr:ap-northeast-1:XXXXXXXXXXXX:repository/django-ecs-sample-web",
    "registryId": "XXXXXXXXXXXX",
    "repositoryName": "django-ecs-sample-web",
    "repositoryUri": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web",
    "createdAt": 1575769798.0,
    "imageTagMutability": "MUTABLE",
    "imageScanningConfiguration": {
      "scanOnPush": false
    }
  }
}
```
この時に返される"repositoryUri"の"XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web"を控えておきます。
もしくは、URL の構成は
[AWS ACCOUNT ID].dkr.ecr.[reagion].amazonaws.com/[repository name]
のようになっているので、アカウントID, リージョン名, リポジトリ名 から導き出すことも出来ます。（今回のリポジトリ名は django-ecs-sample-web です。）
これは push 用のタグ付けや docker-compose-ecs.yml でイメージの読み込み先として使うため後々必要になります。
続いて nginx、mysql 用のリポジトリも作成し、同様にrepositoryUriを控えます。

```sh:shll
$ aws ecr create-repository --repository-name django-ecs-sample-nginx
$ aws ecr create-repository --repository-name django-ecs-sample-mysql
```

### (2) ./docker-compose-ecs.yml ファイルを修正します
リポジトリが出来たので、./docker-compose-ecs.yml ファイルの image 項目をリポジトリ URI に書き換えます。
./docker-compose-ecs.yml を開くと下記の用に XXXXXXXXXXXX.dkr.ecr となっていますので、ここを(1)で控えた URI に書き換えてください。(もしくはデプロイするアカウントIDをXXXXXXXXXXXXに入れるので構いません。）

```yaml:./docker-compose-ecs.yml
  mysql:
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest

  web:
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest

  nginx:
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest
```

### (3) ECR へプッシュできるよう docker image にタグを付けます
下記のように docker tag コマンドを使用します。
XXXXXXXXXXXX の URI 部分は、リポジトリ作成時に控えた repositoryUri の値を指定して下さい。

```sh:shell
  $ docker tag django-ecs-sample-nginx:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest
  $ docker tag django-ecs-sample-web:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest
  $ docker tag django-ecs-sample-mysql:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest
  ```
タグが付いたかは下記のようにして確認出来ます。

```sh
  $ docker images
  REPOSITORY                                                                TAG       IMAGE ID        CREATED       SIZE
  XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx   latest    1dd8c2bc8f1d    1 days ago    127MB
  django-ecs-sample-nginx                                                     latest    1dd8c2bc8f1d    1 days ago    127MB
  XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web     latest    7633e0977266    1 days ago    462MB
  django-ecs-sample-web                                                       latest    7633e0977266    1 days ago    462MB
  XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql   latest    fdbaa71f3406    1 days ago    456MB
  django-ecs-sample-mysql                                                     latest    fdbaa71f3406    1 days ago    456MB
  ```

### (4) ECR リポジトリにへプッシュします
  下記のように ecs-cli push コマンドを使います。
  XXXXXXXXXXXX の URI 部分は、リポジトリ作成時に控えた repositoryUri の値を指定して下さい。

```sh:shell
$ ecs-cli push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest
```

```sh
INFO[0000] Pushing image       repository=XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx tag=latest
INFO[0015] Image pushed
```
のように表示されれば成功です。
また下記はインターネット接続を切った時に出たエラーメッセージです。
何らかのエラーが出るとこのように表示されるので適宜対応してください。

```
FATA[0000] Error executing 'push': unable to create repository: RequestError: send request failed
caused by: Post https://api.ecr.ap-northeast-1.amazonaws.com/: dial tcp: lookup api.ecr.ap-northeast-1.amazonaws.com: no such host
```

続けて nginx と mysql のコンテナも push します。

  ```sh
  $ ecs-cli push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest
  $ ecs-cli push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest
  ```

## 5. AWS ECS にクラスターを作成します

Amazon ECS クラスターとは、タスクまたはサービスの論理グループです。
EC2 を使用してタスクまたはサービスを実行している場合、クラスターはコンテナインスタンスのグループ化でもあります。
とのことですが、私は単純にインスタンス、サービス、タスクの容れ物だと思って使っています。
詳細は下記の公式サイトを参照して下さい。
[Amazon ECS クラスター](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/clusters.html)

### (1) ecs-params.yml ファイルを作成します
Amazon ECS タスク定義にはdocker-compose.ymlには対応しないフィールドがあり、それを ecs-params.yml で指定します。
今回はリポジトリ内に用意しているためそれを使います。
ここでは無料枠があれば無料となる t2.micro マシンを使いたいのでメモリーサイズを調整しています。
このファイルが無い場合メモリーサイズは各コンテナに 500MB ずつ割り当てられるようです。
つまり3つコンテナを立ち上げるには 1.5GB 以上のメモリーを持ったEC2インスタンス(t3.smallやt2.smallなど)が必要になります。
t2.micro は残念がら 1GB しかメモリーがないので調整が必要ということです。
また、逆にもっと大きなメモリーサイズのEC2インスタンスを立ち上げて大量のメモリーを割り当てる場合にも設定が必要です。
但し下記は t2.micro で動かせたというだけで最適化は行っていません。
  
```yaml:ecs-params.yml
version: 1
task_definition:
  services:
    nginx:
      mem_limit: 150m
      mem_reservation: 128m
    web:
      mem_limit: 448m
      mem_reservation: 400m
    db:
      mem_limit: 448m
      mem_reservation: 400m
```
ファイル名が ecs-params.yml なら自動的に読み込まれますが、他の名前を付ける場合は、--ecs-params にパス名を指定します。
詳細は下記の公式ドキュメントを参照して下さい。
[Amazon ECS パラメータの使用](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/cmd-ecs-cli-compose-ecsparams.html)

### (2) ECS クラスター設定を作成します
この設定ではクラスター用のインスタンスの種類などを指定します。
ECS CLI を使い下記のコマンドを実行する事でクラスター設定を作成できます。

```sh：shell
$ ecs-cli configure --cluster django-ecs-sample --default-launch-type EC2 --config-name django-ecs-sample --region ap-northeast-1
```
指定しているパラメータについて

- --cluster : ここにはクラスター名を指定します。
- --default-launch-type : ここには作成するインスタンスの種類を指定します。
  EC2 を指定すると EC2インスタンスを立ち上げて docker 環境を構築します。
  サーバレスの Fargate を指定することもできます。
- --config-name : 作成する config の名前を指定します。
- --region : EC2を立ち上げるリージョンを指定します（例 ap-northeast-1）

下記のように返ってくれば django-ecs-sample という名前のクラスター設定が保存され、使えるようになります。

```sh
INFO[0000] Saved ECS CLI cluster configuration django-ecs-sample.
```

### (3) ECS クラスターを作成します

- EC2 keypair を用意します。
  superuser を作成する際に ssh で接続するため、ここで作成しておきます。
  既存のキーペアーを持っていて使える場合はここを飛ばしても大丈夫です。
  下記のコマンドを実行します。

```sh:shell
$ aws ec2 create-key-pair --key-name MyKeyPair --query 'KeyMaterial' --output text > MyKeyPair.pem
```

&emsp; 'MyKeyPair' と 'MyKeyPair.pem' の部分にはわかりやすい名前を指定します。
&emsp; 出来た pem ファイルを ~/.ssh/ フォルダへ保存しておきます。

```sh:shell
$ mkdir ~/.ssh
$ mv ./MyKeyPair.pem ~/.ssh
```
&emsp; また、作成した pem ファイルにはアクセス制限をかけておく事が推奨されていますので下記コマンドを実行します。(ubuntuの場合のみ）

```sh:shell
$ chmod 400 ~/.ssh/MyKeyPair.pem
```
詳細は下記の公式ページを参照して下さい。
[Amazon EC2 キーペアの作成、表示、削除](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-services-ec2-keypairs.html)

- ECS クラスターを作成します。
  ここでは t2.micro のEC2インスタンスを1つ作ります。
  (無料枠を使えない場合は t3.micro の方が安いのでそちらでも構いません。)

```sh:shell
$ ecs-cli up --keypair MyKeyPair --capability-iam --size 1 --instance-type t2.micro --cluster-config django-ecs-sample --ecs-profile default
```
&emsp; 'MyKeyPair' には先ほど作成した keypair 名を指定します。
&emsp; "Cluster creation succeeded."` と表示されれば成功です。
&emsp; 起動には数分間かかりますので気長に待ちましょう。
&emsp; またここでEC2インスタンスが立ち上がります。
&emsp; 一応最初の１年間は無料枠となっているインスタンスを使っていますが、他にもインスタンスを立ち上げている等々条件によって無料にならない場合もあります。
&emsp; ECSクラスターの破棄を行うまでは課金されますので中断する場合などはご注意ください。
&emsp; 下記のように表示されればクラスターの作成は成功です。

```sh:shell
VPC created: vpc-043b9c071191494f9
Security Group created: sg-0289045419da1393c
Subnet created: subnet-04f7f97d1b133656a
Subnet created: subnet-0d323c836901d8e40
Cluster creation succeeded.
```
&emsp; 何らかの理由でクラスターの作成をやり直す場合は、一旦下記のコマンドでクラスターを削除してからやり直してください。

```sh:shell
$ ecs-cli down --force --cluster-config django-ecs-sample
```

## 6. 作成したクラスターにサービスを作成し、アプリケーションをデプロイします

下記コマンドでサービスを作成してデプロイします。

```sh:shell
$ ecs-cli compose --file docker-compose-ecs.yml service up --cluster-config django-ecs-sample
```
`"ECS Service has reached a stable state"` と表示されればデプロイ成功です。
以下のコマンドで実行しているタスクを確認できます。

```sh:shell
$ ecs-cli compose service ps
Name                                        State    Ports                    TaskDefinition     Health
26f9f2f5-7ff2-4f59-9852-1299c9572a59/nginx  RUNNING  3.113.1.25:80->80/tcp  django-ecs-sample:5  UNKNOWN
26f9f2f5-7ff2-4f59-9852-1299c9572a59/db     RUNNING                         django-ecs-sample:5  UNKNOWN
26f9f2f5-7ff2-4f59-9852-1299c9572a59/web    RUNNING                         django-ecs-sample:5  UNKNOWN
```
上記の場合 nginx の Ports に出力されている 3.113.1.25 がアクセスするIPアドレスとなります。
ブラウザを使ってアクセスするために控えておいて下さい。

## 7. Webブラウザで動作確認を行います
ChromeやFirefoxなどWebブラウザを起動し、上記で確認した nginx の Ports アドレスを指定します。

```:Chrome,Firefox
http://3.113.1.25/polls
※上記のURI 3.113.1.25 の部分はnginxのPortsアドレスに置き換えて下さい。
```
ブラウザ上に下記のように表示されれば成功です。

```
No polls are available.
```
もしも

```
OperationalError at /polls/
(2002, "Can't connect to MySQL server on 'db' (115)")
```
のようなエラーが出た場合は MySQL との接続がうまく行っていない可能性が高いです。
mydb.cnf の password と docker-compose-ecs.yml の MYSQL_ROOT_PASSWORD が同じか確認して下さい。
上記が違っていた場合は docker-compose-ecs.yml の MYSQL_ROOT_PASSWORD を mydb.cnf の password に合わせてください。
修正後、下記「後始末（サービスとリソースを削除する）」の1,2を実行してサービスとクラスターを削除した後、クラスターの作成からやり直してみて下さい。

## 8. ssh で AWS EC2 へログインし createsuperuser を行います
 7 で `No polls are available.` と表示されれば MySQL との接続も問題なくアプリは完成！
と言いたい所ですが・・・
残念ながら superuser を作成していないため /admin でログインする事が出来ず Questions 等のデータを作成する事も出来ません。
どうしても createsuperuser を上手く実現する方法を思いつけず、苦し紛れの ssh 接続で対応する事にします。
    
手順
### (1) EC2 のセキュリティグループのインバウンドで ssh を開放します
この設定はAWSコンソール上で行います。
Webブラウザを立ち上げてAWSアカウントへログインします。
EC2 コンソール画面を表示させ、ECS用に作成された EC2 インスタンスを選択します。
（もしくは ECS コンソール画面からインスタンスを選択して EC2 コンソールを出す事も出来ます。）
下記画像のインスタンスの詳細の右側赤枠にある「パブリック DNS (IPv4)」の値(ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com)を控えておきます。
続いて左下赤枠にあるセキュリティグループのリンクをクリックしてセキュリティグループ画面を表示します。

![django-ecs-sample02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260738/b6e8c7c5-e865-c7f4-bbcc-e2cd0f44a81b.png)

セキュリティグループの詳細で「インバウンド」タブを選択、「編集」ボタンをクリックします。

![django-ecs-sample03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260738/547f7675-771c-7d84-dc4e-d28c42733e4e.png)

  インバウンドルールの編集画面で、ルールの追加ボタンをクリックします。
  タイプを「SSH」にしソースで「マイIP」を選択して保存をクリックします。
  固定IPアドレスを使っている場合はIPアドレスが変わらないためこのままで問題ありませんが、それ以外の方は速やかに進めて下さい。

![django-ecs-sample04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260738/cba1d9b9-b73a-9714-ced9-abacdf50947a.png)

### (2) ターミナル(もしくはGitBash)に戻り、ssh 接続を行います
  下記のコマンドで ssh 接続を行います。
  MyKeyPair.pem には クラスター作成時に作ったものを指定します。
  また、`ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com`の部分には上記 (1) で控えたパブリック DNS (IPv4)を入れます。
ec2-user というユーザー名は Amazon Linux 2 または Amazon Linux AMI を使った際に決まっているユーザー名です。
この辺りは [SSH を使用した Linux インスタンスへの接続](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html) を参照して下さい。

```sh:shell
$ ssh -i ~/.ssh/MyKeyPair.pem ec2-user@ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com
```
初めて ssh　接続する際は下記のような問いがでるので、yes と入力します。

```sh:shell
The authenticity of host 'ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com (XX.XX.XX.XX)' can't be established.
ECDSA key fingerprint is SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.
Are you sure you want to continue connecting (yes/no)?
```
ログインに成功すると下記のようなプロンプト画面になります。

```sh:shell
[ec2-user@ip-10-0-1-38 ~]$
```
続いて起動している web コンテナのIDを調べます。

```sh:shell
$ docker ps
CONTAINER ID        IMAGE                                                                              COMMAND                  CREATED             STATUS                    PORTS                 NAMES
9b923b483350        XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest   "nginx -g 'daemon of…"   40 minutes ago      Up 40 minutes             0.0.0.0:80->80/tcp    ecs-django-ecs-sample-20-nginx-94c0f0a1f7c3e09a2200
8f06e3da926f        XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest     "sh /code/run-my-app…"   40 minutes ago      Up 40 minutes             8001/tcp              ecs-django-ecs-sample-20-web-e69bd295ee81ad978201
719a8da19a44        XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest   "docker-entrypoint.s…"   41 minutes ago      Up 41 minutes             3306/tcp, 33060/tcp   ecs-django-ecs-sample-20-db-96eacce1d2fba3c8f501
862ad405bf2b        amazon/amazon-ecs-agent:latest                                                     "/agent"                 41 minutes ago      Up 41 minutes (healthy)                         ecs-agent  
```
上記の場合 django-ecs-sample-web:latest のタグが付いているコンテナIDは 8f06e3da926f なので、docker exec を使ってこのコンテナに入ります。

```sh:shell
$ docker exec -it 8f06e3da926f sh
```
`/code $ ` とプロンプトが変わります。
これ以降ssh接続中の説明では $ 以前はプロンプトです。$ 以降をコマンドとして入力して下さい。
`cd mysite` でフォルダを /code/mysite/ へ移動します。念の為manege.pyがあるか ls コマンドで調べます。

```sh:shell
/code $ cd mysite
/code/mysite $ ls -l
total 20
-rwxr-xr-x    1 197612   197121         626 Jan 18 09:51 manage.py
-rwxr-xr-x    1 197612   197121         119 Jan 18 09:54 mydb.cnf
drwxr-xr-x    1 197612   197121        4096 Jan 18 09:56 mysite
drwxr-xr-x    1 197612   197121        4096 Jan 20 14:51 polls      
```
manage.py がありましたね。  
それでは下記コマンドでスーパーユーザーを作成しましょう。

```sh:shell
/code/mysite $ python manage.py createsuperuser
ユーザー名 (leave blank to use '1000'): Admin
メールアドレス: Admin@test.com
Password:
Password (again):
Superuser created successfully.
```
上記のように、ユーザー名、EMailアドレス、パスワードの入力を求められるので適宜入力します。
（上記はサンプルです。）

```sh:shell
Superuser created successfully.
```
と出れば作成完了です。
docker コンテナと EC2 から exit でログアウトします。

```sh:shell
/code/mysite $ exit
[ec2-user@ip-10-0-1-38 ~]$ exit
ログアウト
Connection to ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com closed.
```
上記のように表示されれば ssh を使った作業は全て完了です。

### (3) セキュリティグループに追加した ssh の設定を削除します
先程の AWS コンソールに戻りインバウンドルールの編集画面で下記の画像の赤枠✖をクリックし、保存します。
![django-ecs-sample05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260738/504f980f-2775-4dff-7a2e-414f11baf2b5.png)

これで晴れて /admin でログインし、Questions と Choices を作成して Polls アプリを動かすことが出来ます。
お疲れ様でした！

## 9. 後始末（サービスとリソースを削除しておきましょう）
テストが終わったら無駄な課金をさけるためリソースを削除しておきます。
下記の手順で削除していきます。
### (1) サービスを削除します

```sh:shell
$ ecs-cli compose --file docker-compose-ecs.yml service rm
```

### (2) クラスターを削除します

```sh:shell
$ ecs-cli down --force
〜〜〜
  INFO[0121] Deleted cluster                               cluster=django-ecs-sample
```
という表示が出れば無事クラスターが削除されています。

### (3) ECR イメージを削除します
ECR も課金されますので、使わなくなったら削除しておきましょう。（微々たる金額ですが）  

```sh:shell
$ aws ecr batch-delete-image --repository-name django-ecs-sample-web --image-ids imageTag=latest
$ aws ecr batch-delete-image --repository-name django-ecs-sample-nginx --image-ids imageTag=latest
$ aws ecr batch-delete-image --repository-name django-ecs-sample-mysql --image-ids imageTag=latest
```

### (4) ECR リポジトリを削除します

```sh:shell
$ aws ecr delete-repository --repository-name django-ecs-sample-web
$ aws ecr delete-repository --repository-name django-ecs-sample-nginx
$ aws ecr delete-repository --repository-name django-ecs-sample-mysql
```
ここまで削除すれば課金されることはありません。
但し、ecs-cli push を複数回行った場合など latest 以外のイメージがあるとリポジトリの削除に失敗する事があります。
そのような場合は AWS コンソールから ECR リソースを確認して削除してください。

### (5) タスク定義を登録解除します
タスクも不要なのでリストアップされないようにしておきます。
まず下記コマンドでタスク定義のリストを表示させます。

```sh:shell
$ aws ecs list-task-definitions
```
下記のようにタスク定義が表示されます。

```sh:shell
{
    "taskDefinitionArns": [
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:1",
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:2",
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:3",
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:4"
    ]
}
```
今回いろいろテストしたためタスク定義が４つも作られていました（１度で成功した場合は1つしか作られません)。
これはタスク設定に変更があった場合 compose service up の度に新しいものが作成されるようです。
これら全てが不要ですので、下記のコマンドで登録解除します。

```sh:shell
$ aws ecs deregister-task-definition --task-definition django-ecs-sample:1
```
django-ecs-sample:1 の部分に上記リストのタスク名とリビジョンを入れます。
登録解除されれば解除されたタスク情報が返されます。
全てのリビジョンを登録解除して再び `list-task-definitions` で確認すると

```sh:shell
{
    "taskDefinitionArns": [
    ]
}
```
と、登録解除された事がわかります。
先程から登録解除と書いているようにあくまで解除されただけで削除されたわけではありません。
下記のコマンドで INACTIVE なタスク定義をリストアップさせると、

```sh:shell
$ aws ecs list-task-definitions --status INACTIVE
{
  "taskDefinitionArns": [
      "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:1",
      "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:2",
      "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:3",
      "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:4",
  ]
}
```
上記のように先程登録解除した django-ecs-sample:1 などが列挙されました。
これは既に動いているクラスターやサービスのタスク定義を登録解除してもタスクが終了するわけでは無いことを表しています。
またこれら登録解除済みのタスクを使っているクラスターのインスタンスが再起動した場合でも解除済みのタスク定義でタスクは起動します。
では何が違うのかと言えば、解除したタスク定義を使って新規でタスクを立ち上げる・サービスを更新する事は出来ないという意味です。
せめて何処でも使ってないタスク定義（今回のテストプログラムのようなもの）は削除したいと思いますが、今の所タスク定義を完全に削除する方法は無さそうです。

## あとがき

最後までお付き合い頂きありがとうございました。
私がよく理解できてないため解りにくい所もあったかと思います。
また、デプロイ後にコードを修正したりアップデートする方法については書けていません、調べてやってみて頂ければと思います。

最初にも書きましたが、こちらは私が作ったDjangoのテストアプリを ECS へ出来るだけ簡単にデプロイする為に作ったサンプルを元にしています。
そのため私がいかに早く楽にデプロイできるかに重点を置きました。
本来重要なセキュリティなどは置き去りとも言えます・・・

なんにせよ docker と ECS を使えば簡単に Django アプリを AWS 上で動かせるんだなと思って頂けたらと思います。

最後になりましたが、これを書くにあたって沢山の記事、勉強会での発表等を参考にさせて頂きました。
それらに係わられた全ての方に感謝しています、ありがとうございました。