典型的なwebサービスのawsを使ったインフラ構成を考えてみるS3, ECS

## 概要
フロントエンドをVueを使って作り、APIなどのバックエンドをpythonのFlaskという軽量なフレームワークを使って作りました。今回は、ローカル環境下で動いていたアプリケーションをgithubから自動的にAWSの方にdeployすることができるようなインフラ構成を設計してみます。

不十分な点などありましたら、アドバイスいただけると幸いです。

## 使うもの
**AWS関連**

- EC2
- RDS
- ECR
- ECS(fargate)
- S3
- CloudFront
- Route53
- IAM

**CI/CD関連**

- Circle CI


## システム構成図
![インフラ のコピー-Page-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264708/2f226598-bd71-4e5e-38b2-591a6589b554.png)

まず、public subnetとprivate subnetを持つVPCを作成します。外部からのアクセスを許容するpublic subnetには、ロードバランサーと踏み台(ログイン)サーバーを設置します。外部から直接アクセスできないprivate subnetには、APIサーバーとデータベースを置きます。

次に、API serverであるFlaskアプリとproxy serverとして利用するNginxは、コンテナ化して、ECS(Forgate)で管理します。Forgateを使うことで、自動でコンテナのスケーリングや再起動などをしてくれます。RDSは、AWSのamazon Auroraを利用します。こちらも、定期的にレプリカを作成してくれるので、万が一データを損失する場合やデータベースにアクセスできなくなった場合にも安心です。amazon Auroraにアクセスできるのは、(APIサーバーと)踏み台サーバーのEC2からのみで、外部からデータベースをいじることはできません。Login serverのEC2のインスタンスは、秘密鍵が保存されているローカルPCからのみアクセスできます。

フロントエンドのコードは、S3にデプロイし、CloudFront経由で配信します。あとは、Route53でのドメイン設定や、Certificate Managerでの証明書取得、ロードバランサーの設置など、細々した設定をしてあげると完成です。

## CI/CD関連
![インフラ のコピー-Page-2 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264708/9b76eb12-fab9-9b8d-5897-c99b28b9fda9.png)

Circle CIが非常に優秀で、githubにpushすると、ECRにpushして、ECSにdeployまでしてくれます。具体的には、[公式ドキュメント](https://circleci.com/docs/ja/2.0/ecs-ecr/)を一読することをお勧めします。


```config:config.yml
version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@0.0.2
  aws-ecs: circleci/aws-ecs@0.0.3
workflows:
  build-and-deploy:
    jobs:
      - aws-ecr/build_and_push_image:
          account-url: "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
          repo: "${AWS_RESOURCE_NAME_PREFIX}"
          region: ${AWS_DEFAULT_REGION}
          tag: "${CIRCLE_SHA1}"
      - aws-ecs/deploy-service-update:
          requires:
            - aws-ecr/build_and_push_image
          aws-region: ${AWS_DEFAULT_REGION}
          family: "${AWS_RESOURCE_NAME_PREFIX}-service"
          cluster-name: "${AWS_RESOURCE_NAME_PREFIX}-cluster"
          container-image-name-updates: "container=${AWS_RESOURCE_NAME_PREFIX}-service,tag=${CIRCLE_SHA1}"
```

上記`.circleci/config.yml`をgithubにpushするとcircleCIの設定が適応されます。CircleCIで定めた環境変数は、CircleCIのダッシュボードの`Environment Variables
`から設定します(後述)。

### S3へのdeploy

   - IAMでS3へのアップロード用のuserを作り、`AmazonS3FullAccess`権限を与える
   - aws cliからuploadするscriptsを書く
   
こちらに関しては、たくさん説明記事があるので、細かい内容は省略します。
参考: [Amazon S3でSPAをサクッと公開する](https://qiita.com/kiyokiyo_kzsby/items/77bdb81a1ce1852b30ca#s3%E3%81%B8%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%82%92%E4%BD%9C%E3%82%8B)


## インフラ構築までの流れ

### VPCの作成
   - IPv4 CIDR 10.2.0.0/16
  
### subnetの作成
   - IPv4 CIDR 10.2.0.0/20(public-subnet-a)
   - IPv4 CIDR 10.2.16.0/20(public-subnet-c)
   - IPv4 CIDR 10.2.32.0/20(private-subnet-a)
   - IPv4 CIDR 10.2.48.0/20(private-subnet-c)
    
アベイラビリティゾーンA, Cに2つずつ、public subnetと private subnetを設置します。CIDRの設定に気をつけてください。

### Internet Gatewayの生成
   - Internet Gatewayを生成して、先ほど生成したVPCにアタッチする

### ルートテーブルの生成
   - ルートテーブルを新規に生成
   - 送信先 0.0.0.0/0 をインターネットゲートウェイ (igw-xxxxxxxx) にルーティング(ルートの変更より追加)
   - public subnetとそのルートテーブルを紐付ける

※ デフォルトでは、subnetを生成するとプライベートルートテーブルが選択されます。したがって、自分でルートテーブルを作って、public subnetとの紐付けを行う必要があります。
  
### NAT Gatewayの作成
   - NAT Gatewayを作成して、public subnetの一方にアタッチする
   - これにより、private subnetへアクセスできるようになる


※VPCとsubnet,Internet GWなどの詳細な設定方法は[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/batch/latest/userguide/create-public-private-vpc.html)を参照してください。
   
### ECRでレポジトリを作成
- circleciに権限を付与する
IAMでcircleciによるdeploy用のuserを作成します。
今回は、ECR/ECSに関する権限を付与します。
    - AmazonEC2ContainerRegistryFullAccess
    - AWSCodeDeployRoleForECS
    - AmazonEC2ContainerServiceFullAccess
    - AmazonECSTaskExecutionRolePolicy
    - AWSDeepRacerCloudFormationAccessPolicy
  
- Environment Variablesの設定
    - AWS_ACCOUNT_ID(ex:754569708956)
    - AWS_DEFAULT_REGION(ex:ap-northeast-1)
    - AWS_RESOURCE_NAME_PREFIX(ex:flask-app)
    - AWS_ACCESS_KEY_ID
    - AWS_SECRET_ACCESS_KEY

※**AWS_RESOURCE_NAME_PREFIXは、ECRのレポジトリの名前と同じにしないとエラーが出ます**。

### nginxコンテナをアップロード
   - こちらは、頻繁に変更しないと思うので、circleCIには含めず手動で行う
   - 詳細は[公式ページ](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/ECR_AWSCLI.html)を参照のこと
   - confファイルで、nginxはproxy serverとしての設定する

```bash:ディレクトリ構成
nginx
├── Dockerfile
└── conf
    └── default.conf
```
```Dockerfile:Dockerfile
FROM nginx
COPY conf/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
ENTRYPOINT nginx -g 'daemon off;'
```

```config:default.conf
server {
    listen       80;
    server_name  localhost;

    location / {
        #root   /usr/share/nginx/html;
        #index  index.html index.htm;
        proxy_pass      http://127.0.0.1:5000;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
```

### ECS Fargateの作成
   - fargateでclusterを作成する
VPCは先ほど作った物を使いますので、ここで新しく作成する必要はありません。
   - fargateでtaskを作成する
    タスク実行ロールは、`ecsTaskExecutionRole`を指定します。ECRで登録したdocker imageのURLを貼り付けます。コンテナのportを公開するのを忘れないようにしましょう(flask container port:5000)。

   - fargateでserviceを作成する
       - subnetは先ほど作ったprivate subnetを二つ割り当てます
       - fargate service用のsecurity groupを作成する(port:80)
       - EC2でロードバランサーを作成する
       - ELB用のsecurity groupを作成する(port:80)
       - target groupを作成する(port:80, ターゲットの種類:ip)
       - ロードバランス用のコンテナではnginxのcontainerを指定して、ターゲットグループは先ほど作った物を指定
       
**※ flask-app用のコンテナの名前は、AWS_RESOURCE_NAME_PREFIX-serviceと同じ物にしないと、circleCIのdeployの時にエラーになります**


### RDBとの連携
- 踏み台サーバーを立てる（EC2）
    - mysql-clientをinstallする
- RDSでamazon Auroraを選択する
    - aurora DB用のsecurity groupを作成する(port:3306)
    - private subnetをまとめたsubnet groupを作成する
- 踏み台サーバーからendpointに向けてログインできるか確認する
- ECSのコンテナの方で環境変数を設定する
    - DB_NAME
    - DB_USER
    - PASSWORD
    - HOST
    




## エラーハンドリング
1. **CannotPullContainerError: Error response from daemon: Get... : net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)**
NAT GWがきちんと設定されていなかったら、このエラーが出ます。NATの設定を見直してみましょう。[参考](https://github.com/aws/amazon-ecs-agent/issues/1266#issuecomment-368916456)

2. **Task failed ELB health checks in (target-group...)**
ELBのhealth checkに失敗すると表示されます。ELBを設定した時に、defaultでは、/がhealth checkのendpointになります。APIサーバーで/のPATHでGETを用意していていなかったら、ここでエラーになるので、ELBの設定の時に、PATHを変更するか、APIサーバーの方で、Health check用のAPIを作るようにします。


## まとめ
AWSのサービスをうまく利用することで、再現性が高くスケーラブルなアプリケーションを作成することができました。また、terraformなどを使って、awsの設定自体も出来るだけ、コードに落とせるようにしたらいいなあと思いました。