はじめに
Go(Gin)で作成したWeb APIをAWSへデプロイしました。
コンテナはAmazon ECS on AWS Fargate、データベースはAmazon RDS for PostgreSQLで実行します。Application Load Balancer(ALB)、Route 53、AWS Certificate Manager(ACM)を組み合わせ、独自ドメインからHTTPSでアクセスできるマルチAZ構成にしました。
この記事では、Dockerfileの準備からECR、ECS、RDS、ALB、Route 53の設定までを順番にまとめます。
構築要件
- ECS Fargateでアプリケーションを実行する
- Amazon RDS for PostgreSQLを使用する
- 2つのアベイラビリティゾーンを利用する
- Application Load Balancerを使用する
- Route 53で独自ドメインを管理する
- ACMでSSL/TLS証明書を発行する
- HTTPをHTTPSへリダイレクトする
- ECSタスクのヘルスチェックを有効にする
- RDSをインターネットへ直接公開しない
構成概要
| 項目 | 設定 |
|---|---|
| リージョン | 東京(ap-northeast-1) |
| VPC CIDR | 10.0.0.0/24 |
| AZ | ap-northeast-1a / ap-northeast-1c |
| コンテナレジストリ | Amazon ECR |
| コンテナ実行環境 | Amazon ECS on AWS Fargate |
| ECSサービス | Desired count 2 |
| ロードバランサー | Application Load Balancer |
| データベース | Amazon RDS for PostgreSQL(Multi-AZ) |
| DNS | Amazon Route 53 |
| 独自ドメイン | my-golang-api.com |
| 証明書 | AWS Certificate Manager |
| アプリケーションポート | 8080 |
| PostgreSQLポート | 5432 |
User
↓
Route 53
↓
Application Load Balancer
├─ HTTP:80 → HTTPS:443へ301リダイレクト
└─ HTTPS:443 → ターゲットグループ
↓
ECS Fargate
↓
RDS for PostgreSQL
1. DockerfileをFargate向けに準備する
ビルド環境・開発環境・本番環境を分離するマルチステージビルドを使用しました。
FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
FROM builder AS development
RUN go install github.com/air-verse/air@latest
CMD ["air", "-c", ".air.toml"]
FROM alpine:3.20 AS production
WORKDIR /app
RUN apk add --no-cache ca-certificates tzdata curl
COPY --from=builder /app/server /app/server
EXPOSE 8080
CMD ["/app/server"]
マルチステージビルド
Go SDKが必要なのはコンパイル時だけです。本番イメージにはコンパイル済みバイナリだけをコピーし、イメージサイズと不要な依存関係を減らします。
-
builder:Goアプリケーションをコンパイル -
development:Airを利用するローカル開発環境 -
production:ECSで実行する本番環境
本番用は明示的にproductionを指定します。
docker build --target production -t my-golang-app:v⚪︎ .
⚪︎はビルドする数字を記入する。
OSとCPUアーキテクチャを一致させる
GOOS=linuxはFargateのLinux環境で実行するための指定です。CGO_ENABLED=0により、外部Cライブラリへの依存を減らします。
Apple SiliconのMacでECSタスク定義がX86_64の場合は、次のようにビルドします。
docker buildx build \
--platform linux/amd64 \
--target production \
-t <AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-golang-app:v2 \
--push .
| Dockerイメージ | ECSタスク定義 |
|---|---|
linux/amd64 |
X86_64 |
linux/arm64 |
ARM64 |
不一致の場合、ECSでexec format errorなどが発生します。
ポートを統一する
| 設定箇所 | ポート |
|---|---|
| Gin | 8080 |
| Dockerfile | 8080 |
| ECSタスク定義 | 8080 |
| ターゲットグループ | 8080 |
| ECS用セキュリティグループ | 8080 |
EXPOSE 8080だけで通信が許可されるわけではありません。実際の通信可否はECSのポートマッピング、セキュリティグループ、ターゲットグループで決まります。
Ginはコンテナ外部から接続できる状態で待ち受けます。
if err := router.Run(":8080"); err != nil {
log.Fatal(err)
}
127.0.0.1:8080だけで待ち受けると、ALBから接続できません。
ヘルスチェックを用意する
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
ECSとALBのヘルスチェックパスを/healthへ統一します。タスク定義のコンテナヘルスチェック例は次のとおりです。
{
"command": [
"CMD-SHELL",
"curl -fsS http://localhost:8080/health || exit 1"
],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 30
}
ヘルスチェックで使用するcurlやwgetが本番イメージに存在しないと、アプリケーションが動いていてもタスクはUNHEALTHYになります。
機密情報を含めない
DBパスワードやAWSアクセスキーをDockerfileのENVへ書いてはいけません。削除後もイメージレイヤーに残る可能性があります。本番のDB認証情報はSecrets Managerに保存し、ECSタスク定義から参照します。
.dockerignoreも用意します。
.git
.gitignore
.env
.env.*
README.md
tmp
coverage.out
*.log
.DS_Store
2. VPCとサブネットを作成する
- VPC名:
my-golang-app-vpc - IPv4 CIDR:
10.0.0.0/24 - DNS解決:有効
- DNSホスト名:有効
用途ごとに6つのサブネットを作成します。
| 用途 | AZ | CIDR |
|---|---|---|
| Public subnet | 1a | 10.0.0.0/27 |
| Public subnet | 1c | 10.0.0.32/27 |
| ECS Private subnet | 1a | 10.0.0.64/27 |
| ECS Private subnet | 1c | 10.0.0.96/27 |
| DB Private subnet | 1a | 10.0.0.128/27 |
| DB Private subnet | 1c | 10.0.0.160/27 |
ALBとNAT GatewayはPublicサブネット、ECSはECS用Privateサブネット、RDSはDB用Privateサブネットへ配置します。
3. Internet GatewayとNAT Gatewayを設定する
Internet GatewayをVPCへアタッチします。Publicルートテーブルには0.0.0.0/0 → Internet Gatewayを追加し、2つのPublicサブネットへ関連付けます。
PrivateサブネットのECSは、ECRからのイメージ取得やCloudWatch Logsへの送信にアウトバウンド通信が必要です。そのため、各AZのPublicサブネットへNAT GatewayとElastic IPを配置します。
- ECS Private 1a → 1aのNAT Gateway
- ECS Private 1c → 1cのNAT Gateway
DB用PrivateサブネットはVPC内部の通信だけでよいため、インターネット向けデフォルトルートを追加しません。すでに適切なルートテーブルが関連付けられている場合、重複して作成する必要はありません。
4. セキュリティグループを作成する
ALB用
| プロトコル | ポート | ソース |
|---|---|---|
| HTTP | 80 | 0.0.0.0/0 |
| HTTPS | 443 | 0.0.0.0/0 |
ECS用
| プロトコル | ポート | ソース |
|---|---|---|
| TCP | 8080 | ALB用セキュリティグループ |
RDS用
| プロトコル | ポート | ソース |
|---|---|---|
| PostgreSQL | 5432 | ECS用セキュリティグループ |
接続元にはIPアドレスではなくセキュリティグループを指定します。
5. RDS for PostgreSQLを作成する
異なるAZのDB用Privateサブネットを登録したDBサブネットグループを作成します。
- 名前:
my-golang-app-db-subnet-group - DB識別子:
my-golang-app-db - エンジン:PostgreSQL
- DBクラス:
db.t4g.micro - Multi-AZ:有効
- パブリックアクセス:なし
- ストレージ暗号化:有効
- ポート:5432
- セキュリティグループ:RDS用
アプリケーションはRDSエンドポイントへ接続します。
6. Secrets ManagerへDB接続情報を登録する
{
"host": "<RDS_ENDPOINT>",
"port": "5432",
"dbname": "appdb",
"username": "<DB_USER>",
"password": "<DB_PASSWORD>"
}
ブログやスクリーンショットへ実際の値を掲載しないよう注意します。
7. ECRへDockerイメージをpushする
ECRにmy-golang-appリポジトリを作成し、認証します。
aws ecr get-login-password --region ap-northeast-1 \
| docker login \
--username AWS \
--password-stdin \
<AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com
docker buildx build \
--platform linux/amd64 \
--target production \
-t <AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/my-golang-app:v2 \
--push .
latestだけでなく、バージョンやGitコミットハッシュなど、デプロイ内容を識別できるタグを使います。
ECRイメージタグv2とECSタスク定義リビジョンmy-golang-app:2は別の概念です。
8. ECSクラスターとタスク定義を作成する
- クラスター:
my-golang-app-cluster - 起動タイプ:Fargate
- タスク定義ファミリー:
my-golang-app - リビジョン:2
- ネットワークモード:
awsvpc - コンテナポート:8080
- ロググループ:
/ecs/my-golang-app - DB認証情報:Secrets Manager
- コンテナヘルスチェック:有効
タスク実行ロールには、ECRからの取得、CloudWatch Logsへの送信、対象シークレットの読み取りに必要な権限を付与します。
9. ターゲットグループを作成する
- 名前:
my-golang-app-tg - ターゲットタイプ:IP
- プロトコル:HTTP
- ポート:8080
- ヘルスチェック:
/health
Fargateのawsvpcでは、ターゲットタイプに「インスタンス」ではなく「IP」を指定します。
10. ALBを作成する
- 名前:
my-golang-app-alb - スキーム:インターネット向け
- サブネット:Public subnet 1a / 1c
- セキュリティグループ:ALB用
最初はHTTP:80からターゲットグループへ転送し、ALB経由でアプリケーションへ到達できることを確認します。
11. ECSサービスを作成する
- サービス:
my-golang-app-service - タスク定義:
my-golang-app:2 - Desired count:2
- サブネット:ECS Private subnet 1a / 1c
- パブリックIP:無効
- セキュリティグループ:ECS用
- ターゲットグループ:
my-golang-app-tg - コンテナポート:8080
次の状態を確認します。
Desired tasks: 2
Running tasks: 2
Pending tasks: 0
ECSのHealth statusと、ターゲットグループの2つのIPターゲットがHealthyになれば正常です。
12. Route 53とACMを設定する
Route 53でmy-golang-api.comを取得し、Public Hosted Zoneを確認します。
ACMを東京リージョンへ切り替え、次のドメインで証明書をリクエストします。
my-golang-api.com
*.my-golang-api.com
DNS検証を選択し、Route 53へ検証用CNAMEを追加します。ALBとACM証明書は同じリージョンに作成する必要があります。
13. ALBにHTTPSリスナーを追加する
- HTTPS:443 →
my-golang-app-tgへ転送 - 証明書:東京リージョンのACM証明書
HTTP:80リスナーはHTTPSへリダイレクトします。
| 項目 | 設定 |
|---|---|
| アクション | URLへリダイレクト |
| プロトコル | HTTPS |
| ポート | 443 |
| ホスト | #{host} |
| パス | /#{path} |
| クエリ | #{query} |
| ステータス | HTTP 301 |
完全なURLを入力する画面では次の形式です。
https://#{host}:443/#{path}?#{query}
14. Route 53のA AliasをALBへ設定する
- レコード名:空欄(ルートドメイン)
- タイプ:A
- エイリアス:有効
- ルーティング先:Application Load Balancer
- リージョン:東京
- ALB:
my-golang-app-alb - ポリシー:シンプル
ALBには通常の固定IPではなくAliasレコードを使用します。
15. 動作確認
curl -I http://my-golang-api.com
301 Moved PermanentlyとHTTPSのLocationが返れば正常です。
curl -i https://my-golang-api.com/albums
HTTP 200とAPIレスポンスが返れば、Route 53、ACM、ALB、ECS、RDSを経由する通信が成功しています。
トラブルシューティング
ECSタスクが起動しない
- ECRのイメージURIとタグを確認する
- イメージとタスク定義のCPUアーキテクチャを一致させる
- タスク実行ロールを確認する
- ECS PrivateサブネットからNAT Gatewayへ到達できるか確認する
- CloudWatch Logsを確認する
- Secrets Managerの読み取り権限を確認する
ターゲットがUnhealthyになる
- アプリケーションが
0.0.0.0:8080で待ち受けているか - ECS用SGがALB用SGからTCP 8080を許可しているか
- ターゲットグループのポートが8080か
- ヘルスチェックパスがHTTP 200を返すか
- コンテナにヘルスチェックコマンドが存在するか
ECSのHealth statusがUNKNOWNになる
ALBのターゲットがHealthyでも、タスク定義にコンテナヘルスチェックがないとECSのHealth statusはUNKNOWNになることがあります。ヘルスチェックを追加した新しいタスク定義リビジョンでサービスを更新します。
RDSへ接続できない
- RDS用SGの接続元がECS用SGか
- PostgreSQLの5432番ポートが許可されているか
- RDSエンドポイントとDB名が正しいか
- ECSとRDSが同じVPCか
- RDSが
availableか
HTTPS証明書を選択できない
ALBとACM証明書のリージョンを確認します。今回は両方とも東京リージョンです。
まとめ
ALBだけをPublicサブネットへ配置し、ECSタスクとRDSをPrivateサブネットへ配置しました。
- インターネット → ALB:80、443
- ALB → ECS:8080
- ECS → RDS:5432
ALB、ECSタスク、NAT Gateway、RDSを2つのAZへ分散し、独自ドメインからHTTPSでアクセスできる構成になりました。
Dockerfile、DockerイメージのCPUアーキテクチャ、コンテナポート、ヘルスチェックコマンドをAWS側の設定と一致させることが、ECRからECSへのデプロイをスムーズに進めるポイントです。