概要
フロントエンドをVueを使って作り、APIなどのバックエンドをpythonのFlaskという軽量なフレームワークを使って作りました。今回は、ローカル環境下で動いていたアプリケーションをgithubから自動的にAWSの方にdeployすることができるようなインフラ構成を設計してみます。
不十分な点などありましたら、アドバイスいただけると幸いです。
使うもの
AWS関連
- EC2
- RDS
- ECR
- ECS(fargate)
- S3
- CloudFront
- Route53
- IAM
CI/CD関連
- Circle CI
システム構成図
まず、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関連
Circle CIが非常に優秀で、githubにpushすると、ECRにpushして、ECSにdeployまでしてくれます。具体的には、公式ドキュメントを一読することをお勧めします。
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をサクッと公開する
インフラ構築までの流れ
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などの詳細な設定方法は公式ドキュメントを参照してください。
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には含めず手動で行う
- 詳細は公式ページを参照のこと
- confファイルで、nginxはproxy serverとしての設定する
nginx
├── Dockerfile
└── conf
└── default.conf
FROM nginx
COPY conf/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
ENTRYPOINT nginx -g 'daemon off;'
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
エラーハンドリング
-
CannotPullContainerError: Error response from daemon: Get... : net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
NAT GWがきちんと設定されていなかったら、このエラーが出ます。NATの設定を見直してみましょう。参考 -
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の設定自体も出来るだけ、コードに落とせるようにしたらいいなあと思いました。