はじめに
AWSとか、インフラとか、なんとなくの知識しかないエンジニア1年生の私が、実際に環境構築してWebアプリをデプロイすることで理解が少し深まったよっていうお話をします。
今回やりたいこと
最終的な構成図はこんな感じです。
こういった図を作るのも初めてなので、雰囲気で見てもらえると嬉しいです。
※学習が主な目的のため、今回はシングルAZ構成にしています。
※構成図の表現上、ECSとRDSが別のサブネットにあるように見えますが、今回は両方とも同じプライベートサブネットに配置しています。
以下の作業をAWS CLIを使って構築していきます。
- ローカル環境のアプリをDockerでコンテナ化する
- DockerイメージをECRにプッシュする
- ECSでクラスター、タスク定義、サービスを作成する
- RDSでMySQLデータベースを構築し、アプリから接続する
- 独自ドメインを取得してHTTPS化する
この記事では、コマンドの詳細な解説よりも、各サービスがどのような役割を担っているのかを「料理」に例えながら、イメージで理解することを重視して書きたいと思います。
VPCとサブネットは事前に作成済みであることを前提としています。
本記事ではAWSの無料利用枠を使用しています。無料利用枠の対象外となる場合は、数千円程度の費用が発生する可能性があります。
1 アプリのコンテナ化
今回はFastAPIで作成したWebアプリをデプロイします。
まずはローカル環境でアプリをコンテナ化するため、DockerをインストールしDockerfileを作成します。
Docker自体の詳しい説明は省略しますが、以下の記事などが参考になります。
料理で例えると、
- 普通にローカルで動かしている状態は「料理人が家で自由に料理をしている」だけ
- Dockerfileを書くと「どんな調理器具、材料、調味料で料理するかがレシピ化」された状態
- 出来上がるDockerイメージは「どこでも誰でも同じ料理が作れる料理人の分身」
→アプリケーションを「どこでも動かせる箱(コンテナ)」に入れることができます!
2 ECRにプッシュ
1で作成したDockerイメージを、AWSのプライベートなコンテナイメージ保管場所である ECR (Elastic Container Registry) にアップロードします。
# イメージのビルド
$ docker build . -t <リポジトリ名>:<タグ>
# リポジトリの作成
$ aws ecr create-repository --repository-name <リポジトリ名>
# ECRログイン
$ aws ecr get-login-password --region <region> \
| docker login --username AWS --password-stdin <AWSアカウントID>.dkr.ecr.<region>.amazonaws.com
# タグ付け
$ docker tag <リポジトリ名>:<タグ> <AWSアカウントID>.dkr.ecr.<region>.amazonaws.com/<リポジトリ名>:<タグ>
# ECRにプッシュ
$ docker push <AWSアカウントID>.dkr.ecr.<region>.amazonaws.com/<リポジトリ名>:<タグ>
料理で例えると、
- ECRは「料理人の待機部屋」のようなもの
- Dockerイメージ(料理人の分身)をここに並べておくことで、いつでも呼び出すことができる
→Githubにコードを置いておくように、ECRはコンテナ置き場の役割を果たします!
3 ECSにデプロイ
ECRに置いたコンテナイメージを使って、いよいよアプリケーションを動かしていきます。
ECS (Elastic Container Service) を使って、コンテナの実行を管理します。
今回は、サーバーの管理が不要で手軽にコンテナを実行できる Fargate という起動タイプを選択しました。
# クラスター
$ aws ecs create-cluster --cluster-name <クラスター名>
# タスク定義(taskdef.jsonは別で作成)
$ aws ecs register-task-definition --cli-input-json file://taskdef.json
# サービス作成(ALBとターゲットグループは事前に用意)
$ aws ecs create-service \
--cluster <クラスター名> \
--service-name <サービス名> \
--task-definition <タスク定義名> \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<サブネットID1>,<サブネットID2>],securityGroups=[<ECS-SG>],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=<ターゲットグループARN>,containerName=<コンテナ名>,containerPort=8008"
taskdef.jsonについて
タスク定義ではどのようなコンテナを作成するのかをJSON形式で記述します。 今回はプロジェクトのルートディレクトリに「taskdef.json」というファイルを置き、そこに定義を書きました。タスク定義のテンプレートを使って設定項目を埋めていきます。
一度作成したタスク定義は変更できないため、更新する際は新しいリビジョンを作成する必要があります。
アプリのイメージだけを差し替えたい場合は、ECRにプッシュするイメージタグのバージョンを更新し、タスク定義のその部分だけを変更して再登録します。
セキュリティグループ(SG)について
AWSではリソースごとに「セキュリティグループ」というファイアウォールを設定します。 これは「ドアマン」のようなもので、どこから・どのポートの通信を受け付けるかを決めます。今回は下記のように設定しました。
- ALB-SG:インターネット(0.0.0.0/0)から 80, 443 を許可
- ECS-SG:ALB-SG からアプリのポート(例:8008)だけ許可
- RDS-SG:ECS-SG からDBポート(3306)だけ許可
ALBとターゲットグループの準備
ECSサービスを作成する前に、リクエストの振り分け役となる ALB (Application Load Balancer) と、その振り分け先を定義するターゲットグループを作成しておく必要があります。
# ALBの作成
$ aws elbv2 create-load-balancer \
--name <ALB名> \
--subnets <サブネットID1> <サブネットID2> \
--security-groups <ALB-SG>
# ターゲットグループの作成
$ aws elbv2 create-target-group \
--name <TG名> \
--protocol HTTP \
--port 8008 \
--vpc-id <VPC-ID> \
--target-type ip \
--health-check-path /health
ALB は「レストランの受付」、ターゲットグループは「受付名簿」のような存在で来店したお客さんを正しく案内できるようにします。
ECSは「厨房」全体を管理するシステムです。
- クラスター作成:厨房のスペース(作業場所)を作ること
- タスク定義:料理人(コンテナ)の働き方を決めるシフト表(使用メモリ、CPU、環境変数など)
- サービス作成:シフト表に従って、常に最低1人の料理人が働き続けるようにレストランの運営を開始すること
4 RDSの作成と接続
次に、アプリケーションのデータを保存するためのデータベースを用意します。AWSのマネージドデータベースサービスである RDS (Relational Database Service) を使って、MySQLデータベースを構築します。
まず、RDSを配置する複数のサブネットを指定するための「DBサブネットグループ」を作成します(異なるアベイラビリティゾーン(AZ)にまたがる2つ以上のサブネットが必要です)。
$ aws rds create-db-subnet-group \
--db-subnet-group-name <DBサブネットグループ名> \
--db-subnet-group-description "DB subnet group" \
--subnet-ids <サブネットID1> <サブネットID2>
ECSからのアクセスのみを許可するセキュリティグループを用意したら、DBインスタンスを作成します。
$ aws rds create-db-instance \
--db-instance-identifier <DB名> \
--db-instance-class db.t3.micro \
--engine mysql \
--allocated-storage 20 \
--master-username <ユーザー名> \
--master-user-password <パスワード> \
--vpc-security-group-ids <RDS-SG-ID> \
--db-subnet-group-name <DBサブネットグループ名> \
--publicly-accessible false
作成したDBのエンドポイント(接続先アドレス)を確認し、その情報を環境変数などを使ってECSタスクに渡します。
RDSは、食材を安全に保管しておくための巨大な「食料保管庫」です。
セキュリティグループを設定することで、「決められた料理人(ECSタスク)だけが、この保管庫に入れる」ように厳しく管理できます。
5 ドメイン取得とHTTPS化
最後に、デプロイしたアプリケーションに独自のドメインを割り当て、HTTPSで安全に通信できるように設定します。
今回は、Route 53以外の安価なサービスで取得したドメインを使用しました。
まず、取得したドメインをAWSで管理するために、Route 53でホストゾーンを作成します。
$ aws route53 create-hosted-zone \
--name <ドメイン名> \
--caller-reference <任意の一意の文字列>
次に、ACM (AWS Certificate Manager) でSSL/TLS証明書を発行し、DNS検証を使ってRoute 53にレコードを登録します。(証明書の検証など、一部の操作はAWSマネジメントコンソールから行う方が直感的で簡単でした)
証明書が準備できたら、ALBにHTTPS用のリスナーを追加します。
$ aws elbv2 create-listener \
--load-balancer-arn <ALB-ARN> \
--protocol HTTPS \
--port 443 \
--certificates CertificateArn=<証明書ARN> \
--default-actions Type=forward,TargetGroupArn=<ターゲットグループARN>
これにより、80番ポート(HTTP)へのアクセスは自動的に443番ポート(HTTPS)へリダイレクトされ、HTTPSの通信がターゲットグループへ転送されるようになります。
最後に、ドメインへのアクセスがALBに向くように、Route 53にAレコード(エイリアスレコード)を作成します。
$ aws route53 change-resource-record-sets \
--hosted-zone-id <ホストゾーンID> \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "<ドメイン名>",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "<ALBのホストゾーンID>",
"DNSName": "<ALB-DNS名>",
"EvaluateTargetHealth": false
}
}
}]
}'
料理で例えると、
- Route 53:レストランの場所を示す「看板」
- ACM:信頼できるお店であることを示す「営業許可証」や「食品衛生責任者のプレート」
- ALBリスナー:お客さんを正しい受付窓口(HTTP or HTTPS)へ案内する「案内係」
まとめ
インフラ学習、特にクラウドサービスは抽象的な概念が多く、最初はとっつきにくい部分がありました。
だからこそ、今回のように身近なものに例えて各サービスの役割をイメージし、その上で実際に手を動かしてみることが、理解への一番の近道だと思います。
私が効果的だと感じた学習ステップは以下の3つです。
「① 概念を例えで理解 → ② CLIで手を動かす → ③ コンソールでも確認する」
この3ステップで、サービスの役割と設定の繋がりが明確になり、理解度が格段に上がりました!
「習うより慣れろ」ではなく、「習ったら即慣れろ」の精神で、これからも学習を続けていきたいと思います。
参考