実務レベルのAWS Webアプリ構築ハンズオン
こんにちは、フリーランスエンジニアのたいち(@taichi_hack_we)です。
今回は前回の記事↓で作成したKotlin、Spring BootバックエンドAPIを使って上記の実務レベルAWS構成でWebアプリを構築していきます。
検証環境、本番環境アカウントを用意して1ユーザーアカウントでログインできるようにする
下記記事にそって、1つのIdentity Centerユーザーアカウントで検証環境と本番環境それぞれのAWSアカウントにログインできるようにしましょう。検証、本番アカウントを分ける理由は全環境を1つのアカウントで構築すると各環境のAWSリソースが混在して管理が面倒だからです。
また環境ごとにユーザID、パスワード、多要素認証を用意するのが手間なので、1ユーザアカウントで各環境のAWSアカウントにログインできるようにします。
以下の内容で設定してください。
- Identity Centerユーザー
- ユーザー名:任意の値 ex.taichi
- 多要素認証:ON
- AWS Organizationsの検証環境用AWSアカウント
- AWSアカウント名:aws-practice-stg
- IAMロール名:OrganizationAccountAccessRole(デフォルトのまま)
- AWS Organizationsの本番環境用AWSアカウント
- AWSアカウント名:aws-practice-prd
- IAMロール名:OrganizationAccountAccessRole(デフォルトのまま)
- AWS Organizationsの組織構造:Root > aws-practice > aws-practice-prdとaws-practice-stg
- Identity Centerの検証環境用許可セット
- ポリシー:AdministratorAccess
- 許可セット名:aws-practice-stg
- Identity Centerの本番環境用許可セット
- ポリシー:AdministratorAccess
- 許可セット名:aws-practice-prd
- Identity Centerの検証環境用AWSアカウントへのユーザー、許可セット割り当て
- ユーザー:Identity Centerで作成したユーザー ex.taichi
- 許可セット:aws-practice-stg
- Identity Centerの本番環境用AWSアカウントへのユーザー、許可セット割り当て
- ユーザー:Identity Centerで作成したユーザー ex.taichi
- 許可セット:aws-practice-prd
検証、本番の許可セットポリシーが同じAdministratorAccess
なのに検証、本番で許可セットをそれぞれ用意している理由は、ログイン後のAWSマネジメントコンソールで許可セット名が表示されるためです。aws-practice-stg
のように許可セット名に環境名が含まれていれば、自分が今いる環境がひと目でわかります。
上記の設定が完了すると以下手順で各環境にログインできるようになります。
-
https://{固有のID}.awsapps.com/start/#/?tab=accounts
でAWS access portalへアクセス - ユーザID、パスワードでログイン
- AWS access portalから好きな環境へ入る
環境を切り替えたいときは再度AWS access portalから好きな環境へ入ります。
aws-practice-stg
アカウントのリージョンを東京にする
aws-practice-stg
にログインしたら、最初に画面右上から東京 ap-northeast-1
リージョンを選択します。
以降のAWSリソースは特に記載がない限り東京リージョンで作成してください。
AWS CLIを使えるようにする
AWS環境構築でAWS CLIを使う場面があるので設定します。
AWS CLI用のIAMユーザー作成
以下の設定でAWS CLI用のIAMユーザーを作成しましょう。
- ユーザー名:aws-practice-terraform-stg
- AWS マネジメントコンソールへのユーザーアクセスを提供する:チェックなし
- 許可ポリシー:AdministratorAccess
ユーザー名がaws-practice-terraform-stg
なのは、これから構築するAWS構成をTerraform管理するときにも使うユーザーだからです。マネジメントコンソールへのアクセスはセキュリティのために提供しません。さまざまなAWSリソースを扱うのでAdministratorAccess
権限を設定します。
IAMユーザーのアクセスキー作成
自分のPCからAWSリソースへアクセスするためのアクセスキーを作成します。
ユースケースにコマンドラインインターフェースを選択し、アクセスキーのcsvファイルをダウンロードして完了をしましょう。csvファイルはIAMユーザーを使うために必要なので保管してください。
AWS CLIの設定
ACS CLIのインストール
AWS CLIのインストールは公式ドキュメントの手順でも良いのですが、homebrewを使えば以下のコマンドで一発です。
brew install awscli
インストールが成功すれば以下のコマンドが実行できます。
aws --version
aws-cli/2.24.27 Python/3.12.9 Darwin/24.4.0 source/arm64
バージョンアップやアンインストールもhomebrewで簡単にできます。
AWS CLI設定ファイルの作成
aws configure
コマンドで対話形式でAWS CLIの設定ファイルを作成できます。以下の設定をしましょう。
aws configure
AWS Access Key ID [None]: アクセスキーのcsvファイルのAccess key ID
AWS Secret Access Key [None]: アクセスキーのcsvファイルのSecret access key
Default region name [None]: ap-northeast-1
Default output format [None]: json
設定が完了すると~/.aws/credentials
と~/.aws/config
の2つのファイルが作成されます。
~/.aws/credentials
の更新
credentials
ファイルを開きます。
code ~/.aws/credentials
[default]
aws_access_key_id = アクセスキーのcsvファイルのAccess key ID
aws_secret_access_key = アクセスキーのcsvファイルのSecret access key
credentials
はIAMユーザーのアクセスキーを管理するファイルです。AWS CLIで使いたいユーザーやアクセスキーが増えたらcredentials
に追記します。
[default]
はデフォルトで使われるユーザーなので、わかりやすく[aws-practice-terraform-stg]
に更新しましょう。
~/.aws/config
の更新
config
ファイルを開きます。
code ~/.aws/config
[default]
region = ap-northeast-1
output = json
config
にはリージョンとAWS CLIの出力形式が管理されています。config
は[default]
を[profile aws-practice-terraform-stg]
に更新しましょう。これでcredentials
のaws-practice-terraform-stg
とconfig
の[profile aws-practice-terraform-stg]
がひも付きます。
AWS CLIの動作確認
AWS_PROFILE
という環境変数にcredentials
で設定したユーザー名を設定すると、AWS CLIがcredentials
とconfig
の内容を読み込みます。
export AWS_PROFILE=aws-practice-terraform-stg
AWS_PROFILE
の設定ができたら以下のコマンドでAWS CLIが使えるか試しましょう。
aws iam list-users --output table
-------------------------------------------------------------------------------
| ListUsers |
+-----------------------------------------------------------------------------+
|| Users ||
|+------------+--------------------------------------------------------------+|
|| Arn | arn:aws:iam::{ユーザーID}:user/aws-practice-terraform-stg ||
|| CreateDate| 2025-05-06T02:00:18+00:00 ||
|| Path | / ||
|| UserId | ユーザーID ||
|| UserName | aws-practice-terraform-stg ||
|+------------+--------------------------------------------------------------+|
AWS_PROFILEの自動設定
direnvでaws-practice
ディレクトリではAWS_PROFILE=aws-practice-terraform-stg
が環境変数として自動設定されるようにします。direnvについて詳しく知りたい方は以下の記事を参考にしてください。
まずはaws-practiceディレクトリでdirenvの設定をします。
cd {aws-practiceへのパス}
direnv allow
作成されたaws-practice/.envrc
に以下を追記しましょう。
export AWS_PROFILE=aws-practice-terraform-stg
再度direnv allow
で追記した内容を使えるようにします。その後、別のディレクトリから再度aws-practiceディレクトリへ移動し、環境変数が自動設定されるか確認しましょう。
cd ..
cd aws-practice
echo $AWS_PROFILE
aws-practice-terraform-stg
が表示されればOKです。
VPC作成
AWSのプライベートなネットワークであるVPCを作成します。設定内容は以下のとおり。
IPv4 CIDRは10.0.0.0/16
がネットワーク部が最も小さくなる設定値です。ネットワーク部を最小にすることで、VPC内により多くのIPアドレスを作成できます。
複数のVPCが必要な場合や小規模なサービスの場合は、ネットワーク部を大きく設定します。ネットワーク部を大きくするとIPアドレス空間が小さくなって攻撃対象領域が減ります。
VPCが作成できたら編集から
- DNS解決を有効化
- DNSホスト名を有効化
のチェックをONにしてください。
DNSホスト名が有効化されていないと、VPC内のAWSリソースにホスト名が付与されません。するとECSタスクが別のECSタスクと通信時に名前解決ができないといった障害が起こります。
サブネット作成
作成したVPC内に4つのサブネットを作成します。
- public-subnet-1a-stg
- アベイラビリティゾーン:ap-northeast-1a
- IPv4 VPC CIDRブロック:10.0.0.0/18
- public-subnet-1c-stg
- アベイラビリティゾーン:ap-northeast-1c
- IPv4 VPC CIDRブロック:10.0.64.0/18
- private-subnet-1a-stg
- アベイラビリティゾーン:ap-northeast-1a
- IPv4 VPC CIDRブロック:10.0.128.0/18
- private-subnet-1c-stg
- アベイラビリティゾーン:ap-northeast-1c
- IPv4 VPC CIDRブロック:10.0.192.0/18
VPCの10.0.0.0/16
を4つのサブネットに分割する計算は以下のとおり。
- VPC内のIPアドレス数は
2^16=65536
個 - 4サブネットに分割するので1サブネットのIPアドレス数は
65536/4=16384
- 1サブネットのIPアドレス数が
16384=2^14
なので、サブネットのCIDRは/18 - VPCのCIDR/16とサブネットの/18の差分は2。2ビットでの2進数は00、01、10、11
- 第3オクテットの1、2ビット目が00、01、10、11なので10進数に直すと0、64、128、256
インターネットゲートウェイ作成
パブリックサブネットのAWSリソースをインターネットにつなげるためにはインターネットゲートウェイが必要です。aws-practice-igw-stg
という名前タグで作成してください。
作成したインターネットゲートウェイをVPCaws-practice-stg
にアタッチしましょう。
NATゲートウェイについて理解する
役割と使用例
NATゲートウェイはサブネットから外への通信のみ許可します。許可された通信のresponseも許可されます。ですがインターネットゲートウェイと違い、外からサブネットへの通信は許可されません。
なのでプライベートサブネットのAWSリソースのセキュリティを保ちつつ、サブネット外と通信したいときに使います。たとえばプライベートサブネットのECSタスクがNAT経由でECRにアクセスするといった使い方ができます。
コスト対策
NATゲートウェイは1時間あたり$0.062、処理データ1GBあたり$0.062のコストがかかります。1ドル150円で計算すると、つけっぱなしで処理データが0でも月7000円弱の費用です。
個人利用で月7000円はキツいので、本記事ではコストが10分の1ほどになるNATインスタンスを使います。他のコスト対策として、難易度は上がりますがVPC Endpointを使う方法もあります。
NATゲートウェイの設定例
実務で扱えるようになるため、NATゲートウェイの設定を理解しましょう。本記事のAWS構成でNATゲートウェイを使う場合、以下の設定になります。
- 名前:aws-practice-nat-1a
- サブネット:public-subnet-1a-stg
- 接続タイプ:パブリック
- Elastic IP割り当てID:あり
- タグ:Name aws-practice-nat-1a(自動入力される)
プライベートサブネットのAWSリソースのインターネット接続が目的の場合、NATゲートウェイはパブリックサブネットに配置します。通信がプライベートサブネット → NAT → インターネットの順に流れるためです。
またElastic IPの割り当ても必須です。インターネット通信のためにはパブリックなIPアドレスが必要だからです。
AWSのベストプラクティスは複数AZでの冗長化推奨
NATゲートウェイはAZ内で冗長化されているので、使っているNATで障害が起きてもシステムを維持できます。ただAZ障害が起こるとプライベートサブネットのAWSリソースは全てNATを介した通信ができなくなります。そのためAWSのベストプラクティスとして複数AZでのNAT冗長化が推奨されています。
NATインスタンス作成
セキュリティグループ作成
NATインスタンス用のセキュリティグループを作成します。以下のようにプライベートサブネットからのインバウンド通信を許可します。
- セキュリティグループ名:aws-practice-nat-stg
- 説明:Managed by Terraform
- VPC:aws-practice-stg
- インバウンドルール1つめ
- タイプ:すべてのトラフィック
- ソース:カスタム 10.0.128.0/18(private-subnet-1a-stgのIP)
- インバウンドルール2つめ
- タイプ:すべてのトラフィック
- ソース:カスタム 10.0.192.0/18(private-subnet-1c-stgのIP)
- アウトバウンドルール(デフォルト)
- タイプ:すべてのトラフィック
- 送信先:カスタム 0.0.0.0/0
NATインスタンスにSession ManagerでアクセスするためのIAMロールを設定
AmazonSSMManagedInstanceCore
ポリシーを持ったIAMロールを作成します。このロールはあとでNATインスタンスに設定します。
AmazonSSMManagedInstanceCore
ポリシーを持ったIAMロールが必要な理由はNATインスタンスにSession Managerで接続するのに必要だからです。IAMロールの設定内容は以下のとおり。
- 信頼されたエンティティタイプ:AWSのサービス
- サービスまたはユースケース:EC2
- 許可ポリシー:AmazonSSMManagedInstanceCore
- ロール名:aws-practice-nat-stg
EC2インタンス作成
NATインスタンスとして使うEC2インスタンスを以下設定で作成します。
- 名前:aws-practice-nat-1a-stg
- AMI:Amazon Linux 2023 AMI
- アーキテクチャ:64ビット(x86)
- インスタンスタイプ:t2.micro
- VPC:aws-practice-stg
- サブネット:public-subnet-1a-stg
- パブリックIPの自動割り当て:有効化
- セキュリティグループ:aws-practice-nat-stg
- IAMインスタンスプロフィール:aws-practice-nat-stg
IAMインスタンスプロフィールでSession ManagerでアクセスするためのIAMロールを使っています。EC2インスタンスに直接IAMロールは紐づけられません。代わりにIAMロールが入ったIAMインスタンスプロフィールを紐づけます。
また未使用時にNATインスタンスを停止すればコスト削減ができます。
実務でNATインスタンスを使う場合、Elastic IPをつけるのが一般的です。インスタンス起動のたびにIPアドレスが変わると運用に影響が出るためです。ただ本記事ではコスト削減のためにElastic IPをつけません。
Session Managerによる接続確認
EC2インスタンスが実行中になったら、Session Managerで接続できるか確認してください。以下のような画面が表示できればOKです。
EC2でNATの設定をする
EC2をNATとして動かすため、Session ManagerでEC2に入り、以下の記事にそって設定をしてください。
実施するのは、
- NAT AMIを作成する
- 送信元/送信先チェックを無効にする
の手順です。
NATの設定ができたら設定済のEC2からAMIを作成します。本番環境のNATインスタンスで使うためです。設定内容は以下のとおり。
NATインスタンスの動作確認
NATインスタンスの動作確認は参考記事のNATインスタンスをテストするの手順でできます。
また本記事の後半で書いているECSタスクの起動が正常にできれば、NATインスタンスの動作確認にもなります。ECSタスクの起動時はECRからのDockerイメージ取得などの外部通信が発生するからです。
ルートテーブル作成
プライベートサブネット用
以下設定でルートテーブルを作成します。
- 名前:aws-practice-rtb-private-stg
- VPC:aws-practice-stg
そして0.0.0.0/0
をNATインスタンスに向けたルートを追加します。
作成したルートテーブルをプライベートサブネットに関連付けましょう。
作成したプライベートサブネット用ルートテーブルをメインルートテーブルにしておくと、サブネット作成時のデフォルトルートテーブルになります。サブネットとルートテーブル関連付けを忘れても通信がインターネットに向かないので安全です。
パブリックサブネット用
以下設定でルートテーブルを作成します。
- 名前:aws-practice-rtb-public-stg
- VPC:aws-practice-stg
そして0.0.0.0/0
インターネットゲートウェイに向けたルートを追加します。
作成したルートテーブルをパブリックサブネットに関連付けましょう。
RDS作成
RDSに必要なAWSリソースを作成します。
セキュリティグループ
セキュリティグループ設計のベストプラクティス
セキュリティグループ設計は1サービス1セキュリティグループがベストプラクティスです。1サービス1セキュリティグループはシンプルでわかりやすく、どこからの通信を許可しているのかひと目でわかります。
たとえば、
- WebサーバーEC2とWebサーバーEC2用セキュリティグループ
- RDSとRDS用セキュリティグループ
があるとします。この状態でRDS用セキュリティグループのインバウンドルールにWebサーバー用EC2セキュリティグループのみが設定されていれば、RDSはWebサーバーからの通信のみ許可していることがすぐ理解できます。
1つのサービスに1つのセキュリティグループがわかりやすく運用しやすい設計です。
作成
以下設定でRDS用のセキュリティグループを作成します。
- セキュリティグループ名:aws-practice-db-stg
- 説明:Managed by Terraform(今後Terraformで設定する値をあらかじめ登録)
- VPC:aws-practice-stg
- インバウンドルール:なし
- アウトバウンドルール:すべてのトラフィックで0.0.0.0/0(デフォルト設定)
RDSはAPIサーバーからの通信のみを許可します。ですが、現時点でAPIサーバーはないのでインバウンドルールを一旦なしで設定します。
DBサブネットグループ
複数AZにまたがるサブネットをDBサブネットグループとして指定することで、DBが障害時にフェイルオーバーするので冗長性が高まります。作成するDBサブネットグループは以下です。
- 名前:aws-practice-db-subnet-group-stg
- 説明:Managed by Terraform
- VPC:aws-practice-stg
- サブネット:private-subnet-1a-stg、private-subnet-1c-stg
DBは外部からアクセスされたくないので、プライベートサブネットを指定します。
パラメータグループ
RDSのパラメータグループでデータベースの設定を簡単に作成できます。以下の設定で作成しましょう。
- パラメータグループ名:aws-practice-db-parameter-group-stg
- 説明:Managed by Terraform
- エンジンのタイプ:PostgreSQL
- パラメータグループファミリー:postgres17
- タイプ:DB Parameter
RDS
以下設定でRDSを作成します。
RDSは未使用時に停止すればコスト削減ができます。ですが7日間で自動起動してしまいます... なので後でEvent Bridge Schedulerを使って7日ごとに起動 → 停止するよう設定します。
Secrets Managerで機密性のある環境変数を管理
DBのユーザーやパスワードなど、機密性のある環境変数はSecrets Managerで管理します。以下設定でシークレットを作成しましょう。
- シークレットのタイプ:その他のシークレットのタイプ
- キー/値のペア
- DB_HOST:作成したRDSのエンドポイント(例
aws-practice-stg{固有の文字列}amazonaws.com
) - DB_PORT:5432
- DB_NAME:aws_practice
- DB_USER_NAME:RDS作成後に確認したマスターユーザー名
- DB_PASSWORD:RDS作成後に確認したマスターパスワード
- DB_HOST:作成したRDSのエンドポイント(例
- 暗号化キー:aws/secretsmanager
- シークレットの名前:rds-main-stg
- 自動ローテーション:OFF
RDSのデフォルトユーザーをそのまま使うのは権限が強すぎてセキュリティ上良くないので、後で新しいユーザーを作成してSecrets Managerの設定値を更新します。
ECR
前回の記事で作成したバックエンドAPIのDockerイメージをpushするECRリポジトリを作成し、pushしてみます。
プライベートリポジトリ作成
設定内容は以下のとおり。
- リポジトリ名:aws-practice-stg
- イメージタグのミュータビリティ:Immutable
- 暗号化設定:AES-256
タグはImmutableにしてコミットハッシュを使うのがセキュリティ的にも運用的にもベストプラクティスです。詳しくは以下の記事が参考になります。
ライフサイクルポリシー設定
ECRにpushしたイメージがたまっていくとコストがかさみます。なので最新3つのイメージのみ保持するよう、以下設定でaws-practice-stg
のライフサイクルポリシーを作成します。
- ルールの優先順位:1
- ルールの説明:最新3つのイメージのみ保持
- イメージのステータス:すべて
- 一致条件:次の数値を超えるイメージ数 3
バックエンドAPIのDockerイメージをECRへpush
前回の記事のデータベース環境構築のところで扱ったMakefileを使ってECRへログイン、Dockerイメージbuild、ECRへのpushをします。
MakefileのAWSアカウント情報更新
Makefileの以下の箇所は私のAWSアカウントIDになっているので、自身のアカウントIDに更新してください。
# AWS_ACCOUNT_IDを自身の情報に更新する
AWS_ACCOUNT_ID ?= 355195805635
aws-practiceプロジェクトのルートへ移動
cd {aws-practiceへのパス}
aws-practiceへ移動すればdirenvでAWS CLIの設定が自動適用されます。
ECRログイン
ECRにログインするコマンドはmake docker-login ENV=stg
で、実態は以下のコマンドです。
# ECRにログインする
# e.g. make docker-login ENV=stg ECR_NAME=aws-practice
docker-login: .check-env .check-ecr-name
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${IMAGE_REPOSITORY_URI}
このECRログインコマンドはaws-practice-stgのECRリポジトリのプッシュコマンドを表示ボタンで確認できます。これから実行するDockerイメージbuild、ECRへのpushコマンドもここに書いてあるものを元に作成しています。
ECRログインコマンドを実行して以下の表示が出ればOKです。
make docker-login ENV=stg
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 355195805635.dkr.ecr.ap-northeast-1.amazonaws.com/aws-practice-stg
Login Succeeded
Dockerイメージをbuild
Dockerイメージbuildのコマンドはmake build-image ENV=stg DOCKERFILE_DIR=./api
で、実態は以下です。
# Dockerイメージをビルドする
# e.g. make build-image ENV=stg
# Fargateで動かすならplatformはlinux/arm64の方が安いが、Github Actionsの有料プランでしかarm64が使えないためlinux/amd64を指定
build-image: .check-env .check-ecr-name
docker build --platform=linux/amd64 -t ${IMAGE_REPOSITORY_URI}:${GIT_COMMIT_HASH} -f ${DOCKERFILE_DIR}/Dockerfile ${DOCKERFILE_DIR}
buildコマンドを実行してエラーが出なければOKです。
ECRへpush
buildしたDockerイメージのpushコマンドはmake push-image ENV=stg
で、実態は以下です。
# DockerイメージをECRにpushする
# e.g. make push-image ENV=stg
push-image: .check-env .check-ecr-name
docker push ${IMAGE_REPOSITORY_URI}:${GIT_COMMIT_HASH}
pushが成功するとaws-practice-stg
のリポジトリでpushしたDockerイメージを確認できます。
ECRログイン、Dockerイメージbuild、ECRへpushを一括実行
make release-image ENV=stg DOCKERFILE_DIR=./api
でECRログイン、Dockerイメージbuild、ECRへpushを一括実行できます。
# DockerイメージをビルドしてECRにpushする
# e.g. make release-image ENV=stg
release-image: docker-login build-image push-image
(続きは随時更新します)
Discussion