この記事について
| 記事 | タイトル | 状態 |
|---|---|---|
| 第0回 | 全体ガイド | ✅ 完了 |
| 第1回 | 構成図編 | ✅ 完了 |
| 第2回 | VPC & Subnet | ✅ 完了 |
| 第3回 | NAT Gateway & Route Table | ✅ 完了 |
| 第4回 | Security Group | ✅ 完了 |
| 第5回 | ECR & Docker Image | 📍今回 |
| 第6回 | Public ALB | ⬜ 未読 |
| 第7回 | ECS Front & IAM | ⬜ 未読 |
| 第8回 | ECS API & Internal ALB | ⬜ 未読 |
| 第9回 | RDS MySQL | ⬜ 未読 |
| 第10回 | Secrets Manager & 完成 | ⬜ 未読 |
進捗: 45% (5/11記事) | Phase: 2-2 | 🎯 マイルストーン2達成: Phase 2完了!
📁 完全なコードはGitHubで公開:👉 GitHub: fargate-iac-02
1. はじめに
この記事は、Phase 2の後半として、ECRとDockerイメージのpushを行います。
1-1. 前回までの振り返り
Phase 2-1(第4回)で作成したもの:
- ✅ Security Group × 5
今回(第5回: Phase 2-2)で作成するもの:
- ECR Repository × 2(Front、API)
- Lifecycle Policy × 2
- Dockerイメージのビルド & push
1-2. この記事で学べること
- ECRの基本と役割
- Dockerイメージのビルド方法
- ECRへのpush手順
- Lifecycle Policyの設定
1-3. 対象読者
- Phase 2-1(第4回)を完了した方
- Dockerの基本を知っている方
1-4. 想定環境
- Terraform: v1.9.x
- Docker: v20.x以上
- AWS CLI: v2.x
- Phase 1-2: 実行済み
2. Phase 2-2で作成するもの
2-1. リソース一覧
| リソース | 数量 | 用途 | 月額費用 |
|---|---|---|---|
| ECR Repository | 2 | コンテナイメージ保存 | 約$0.10/GB |
| Lifecycle Policy | 2 | 古いイメージ自動削除 | 無料 |
合計: 4リソース
コスト: 約$0.10/GB(イメージサイズによる)
2-2. ECRの用途
| Repository名 | 格納するイメージ | サイズ(目安) |
|---|---|---|
| myproject/front | Nginx + 静的ファイル | 約50MB |
| myproject/api | Go + ビジネスロジック | 約20MB |
合計: 約70MB → 月額約$0.01
📝 注意: リポジトリ名は myproject/front のようにスラッシュを含みます。
3. ECRの役割
3-1. ECRとは
ECR (Elastic Container Registry) は、AWSのマネージドなDockerレジストリです。
特徴:
- Docker Hubのようなコンテナイメージ保存場所
- AWSサービスとの統合(IAM、VPC Endpoint等)
- 暗号化、脆弱性スキャン機能
料理で例えると:
- ECR = レストランの冷蔵庫
- Dockerイメージ = 調理済みの料理(冷凍保存)
- ECS Task = 料理を温めて提供
3-2. Docker Hubとの違い
| 項目 | ECR | Docker Hub |
|---|---|---|
| 料金 | 使用量課金 | 無料プランあり |
| 速度 | AWS内で高速 | インターネット経由 |
| セキュリティ | IAMで制御 | DockerID認証 |
| 用途 | 本番環境 | 開発環境、公開イメージ |
3-3. Lifecycle Policyとは
Lifecycle Policyは、古いイメージを自動削除するルールです。
設定例:
- 最新30個のイメージを保持
- それ以外は自動削除
メリット:
- ✅ ストレージコストの削減
- ✅ 手動削除の手間が不要
3-4. Terraformだけでは完結できないのか?
素朴な疑問:
「なぜDockerイメージのビルドやPushも必要なの?」
「すべてTerraformで完結できないの?」
3-4-1. Terraformの役割とできること / できないこと
| 作業 | Terraformでできる? | 分類 |
|---|---|---|
| VPC作成 | ✅ できる | AWSリソース |
| ALB作成 | ✅ できる | AWSリソース |
| ECR Repository作成 | ✅ できる | AWSリソース |
| Dockerイメージ作成 | ❌ できない | アプリケーション |
| ECRにPush | ❌ できない | データ転送 |
| ECS Task Definition作成 | ✅ できる | AWSリソース |
重要なポイント:
Terraformはインフラ管理ツールであって、アプリケーションビルドツールではありません。
3-4-2. 料理の比喩で理解する
🏢 Terraform = レストランの建設会社
- レストランを建てる(VPC、ALB、ECS)
- 冷蔵庫を設置する(ECR Repository作成)
- 「冷蔵庫の中の料理Aを使ってください」と指示(Task Definition)
🍳 Docker = 料理人
- 料理を作る(Docker Image作成)
- 冷蔵庫に料理を入れる(ECR Push)
📦 ECR = 冷蔵庫
- 料理(Dockerイメージ)を保存
🍽️ ECS = ウェイター
- 冷蔵庫から料理を取り出して、温めて提供
Terraformでできないこと:
- ❌ 料理を作る(Docker Image作成)
- ❌ 冷蔵庫に料理を入れる(ECR Push)
3-4-3. 実際の作業の流れ(Phase 2-2)
# ステップ1: Terraformでインフラ構築(✅ できる)
cd terraform/phase2-security
terraform apply
→ ECR Repository "myproject/front" 作成 ✅
→ 空の冷蔵庫ができた状態
# ステップ2: Dockerイメージ作成(❌ Terraformではできない)
cd ../../front
docker build -t myproject/front:latest .
→ Dockerイメージ作成 ✅
→ 料理ができた状態
# ステップ3: ECRにPush(❌ Terraformではできない)
docker push 014936...ecr.../myproject/front:latest
→ ECRに保存 ✅
→ 冷蔵庫に料理を入れた状態
もしステップ2・3をスキップすると:
Phase 3実行(ECS起動)
↓
ECS Taskが起動しようとする
↓
ECRから "myproject/front:latest" を取得しようとする
↓
イメージがない!(冷蔵庫が空)
↓
CannotPullContainerError ❌
↓
503 Service Temporarily Unavailable ❌
3-4-4. なぜ分離する必要があるのか?
理由1: 技術的制約
TerraformはAWSリソースの管理に特化しています。
Terraformの得意分野:
- AWSリソースの作成・削除・更新
- インフラの状態管理(terraform.tfstate)
- 依存関係の解決
Terraformの苦手分野:
- アプリケーションのビルド
- データの転送(Docker Image Push)
- 継続的なデプロイメント
理由2: 設計思想(関心の分離)
インフラ(Terraform)とアプリケーション(Docker)は分離すべき
なぜ?
- 変更頻度が違う
- 管理者が違う(インフラチーム vs 開発チーム)
- ライフサイクルが違う
例: 実務での変更頻度
インフラ変更: 月1回
→ terraform apply
アプリ変更: 週10回
→ docker build & push
→ ECS Serviceが自動的に新イメージをPull
→ terraform apply は不要
3-4-5. 実務での一般的なフロー
【開発フェーズ】
1. ローカル開発
docker-compose up
→ ローカルで動作確認
【移行準備フェーズ】
2. インフラ構築
terraform apply(ECR作成)
→ 冷蔵庫を設置
3. イメージ準備
docker build & push(イメージ準備)
→ 料理を作って冷蔵庫に入れる
【本番運用フェーズ】
4. サービス起動
terraform apply(ECS起動)
→ ECRのイメージを使って起動
5. アプリ更新時(頻繁)
docker build & push(イメージ更新)
ECS Service更新(新イメージを使う)
※ terraform apply は不要
3-4-6. まとめ
| 項目 | 説明 |
|---|---|
| Terraformの役割 | インフラ構築(箱を作る) |
| Dockerの役割 | アプリケーション準備(中身を作る) |
| なぜ分離? | 技術的制約 + 設計思想(関心の分離) |
| 実務での使い分け | インフラ変更: Terraform、アプリ変更: Docker |
このセクションのポイント:
✅ Terraformは「インフラ管理ツール」であり、アプリケーションビルドはできない
✅ DockerイメージのビルドとPushは別途必要
✅ インフラとアプリは変更頻度が違うため、分離して管理する
次のセクションから、実際にDockerイメージを作成してECRにPushする手順を進めていきます。
4. ディレクトリ構成
4-1. プロジェクト全体の構成
my-ecs-project/
├── terraform/
│ ├── phase1-network/ ← Phase 1完了
│ └── phase2-security/ 👉 今回更新
│ ├── main.tf
│ ├── variables.tf
│ ├── terraform.tfvars
│ ├── data.tf
│ ├── security_group.tf ← 第4回で作成済み
│ ├── ecr.tf 👉 今回追加
│ └── outputs.tf ← 更新
│
├── front/ 👉 今回作成(Dockerイメージ用)
│
└── api/ 👉 今回作成(Dockerイメージ用)
4-2. Phase 2-2で追加するファイル
| ファイル名 | 役割 |
|---|---|
| ecr.tf | ECR Repository × 2、Lifecycle Policy × 2 |
📝 補足:front/ と api/ について
本シリーズはTerraformによるインフラ構築がメインのため、アプリケーションコード(HTML/CSS/JS/Go)の詳細解説は省略します。
| ディレクトリ | 内容 | 用途 |
|---|---|---|
front/ |
Nginx + 静的HTML/JS | API Test / DB Test ボタンUI |
api/ |
Go言語 API | ヘルスチェック、DB接続テスト |
5. ファイル別コード解説
5-1. ecr.tf - ECR Repository × 2
ファイルの役割:
- ECR Repository × 2を作成
- Lifecycle Policyを設定
📝 重要: リポジトリ名は ${var.project_name}/front のようにスラッシュを含みます。
コード骨格図解:
簡易コード:
# terraform/phase2-security/ecr.tf
# === Frontend ===
resource "aws_ecr_repository" "front" {
name = "${var.project_name}/front" # スラッシュあり!
image_tag_mutability = "MUTABLE"
image_scanning_configuration { scan_on_push = true }
encryption_configuration { encryption_type = "AES256" }
}
resource "aws_ecr_lifecycle_policy" "front" {
repository = aws_ecr_repository.front.name
policy = jsonencode({
rules = [{
rulePriority = 1
description = "Keep last 30 images"
selection = { tagStatus = "any", countType = "imageCountMoreThan", countNumber = 30 }
action = { type = "expire" }
}]
})
}
# === API === (Frontend と同じ構成)
resource "aws_ecr_repository" "api" {
name = "${var.project_name}/api"
image_tag_mutability = "MUTABLE"
image_scanning_configuration { scan_on_push = true }
encryption_configuration { encryption_type = "AES256" }
}
resource "aws_ecr_lifecycle_policy" "api" {
repository = aws_ecr_repository.api.name
policy = jsonencode({ /* Frontend と同じ */ })
}
ポイント解説:
| 項目 | 値 | 説明 |
|---|---|---|
name |
${var.project_name}/front |
スラッシュあり(ハイフンではない) |
image_tag_mutability |
MUTABLE |
タグ上書き許可(開発向け。本番はIMMUTABLE推奨) |
scan_on_push |
true |
Push時に脆弱性スキャン実行 |
encryption_type |
AES256 |
イメージを暗号化して保存 |
| Lifecycle Policy | 最新30個保持 | 古いイメージを自動削除してコスト削減 |
📁 完全なコードは GitHub: fargate-iac-02 を参照
5-2. outputs.tf - 出力の更新
既存のoutputs.tfに、ECR URLを追加:
# terraform/phase2-security/outputs.tf
# ... 既存のSG出力 ...
output "ecr_front_url" {
description = "ECR Repository URL for Frontend"
value = aws_ecr_repository.front.repository_url
}
output "ecr_front_arn" {
description = "ECR Repository ARN for Frontend"
value = aws_ecr_repository.front.arn
}
output "ecr_api_url" {
description = "ECR Repository URL for API"
value = aws_ecr_repository.api.repository_url
}
output "ecr_api_arn" {
description = "ECR Repository ARN for API"
value = aws_ecr_repository.api.arn
}
6. Terraform実行手順
6-1. ecr.tfの追加
cd my-ecs-project/terraform/phase2-security
# ecr.tfを作成
# outputs.tfを更新
6-2. terraform plan
terraform plan
実行結果:
Plan: 4 to add, 0 to change, 0 to destroy.
内訳:
- ECR Repository × 2
- Lifecycle Policy × 2
6-3. terraform apply
terraform apply
所要時間: 約30秒
実行結果:
aws_ecr_repository.front: Creating...
aws_ecr_repository.api: Creating...
aws_ecr_repository.front: Creation complete
aws_ecr_repository.api: Creation complete
aws_ecr_lifecycle_policy.front: Creating...
aws_ecr_lifecycle_policy.api: Creating...
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
ecr_front_url = "014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/front"
ecr_front_arn = "arn:aws:ecr:ap-northeast-1:014936403628:repository/myproject/front"
ecr_api_url = "014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/api"
ecr_api_arn = "arn:aws:ecr:ap-northeast-1:014936403628:repository/myproject/api"
📝 確認ポイント:
- ECR URLに
myproject/frontのようにスラッシュが含まれている - これが正しい形式です
7. Dockerイメージのビルド & Push
このセクションでは、実際にDockerイメージをビルドしてECRにpushします。
7-1. 前提: frontとapiディレクトリの準備
7-1-1. プロジェクト構造の確認
cd my-ecs-project
# 現在の構造
my-ecs-project/
├── terraform/
│ ├── phase1-network/
│ └── phase2-security/
│
# 以下を作成します
├── front/ 👈 これから作成
└── api/ 👈 これから作成
7-1-2. Frontendのファイル準備
⚠️ 重要: Dockerイメージをビルドする前に、frontディレクトリとファイルを準備してください。
ステップ1: frontディレクトリ作成
# プロジェクトルートに移動
cd my-ecs-project
# frontディレクトリ作成
mkdir -p front
cd front
ステップ2: Dockerfile作成
cat > Dockerfile << 'EOF'
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
EXPOSE 80
EOF
Dockerfileの説明:
-
FROM nginx:alpine: 軽量なNginxイメージをベースに使用 -
COPY index.html ...: HTMLファイルをNginxの公開ディレクトリにコピー -
EXPOSE 80: ポート80を公開
ステップ3: index.html作成
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Frontend - ECS Fargate</title>
<style>
body {
font-family: 'Arial', 'Helvetica', sans-serif;
max-width: 900px;
margin: 50px auto;
padding: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background-color: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
h1 {
color: #333;
text-align: center;
font-size: 2.5em;
margin-bottom: 10px;
}
.status {
text-align: center;
color: #666;
margin: 20px 0;
font-size: 1.1em;
}
.status p {
margin: 5px 0;
}
.buttons {
text-align: center;
margin: 40px 0;
}
.button {
display: inline-block;
margin: 10px;
padding: 15px 30px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.button:hover {
background-color: #45a049;
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0,0,0,0.15);
}
.button:disabled {
background-color: #cccccc;
cursor: not-allowed;
transform: none;
}
.button.secondary {
background-color: #008CBA;
}
.button.secondary:hover {
background-color: #007399;
}
#result {
margin-top: 30px;
padding: 20px;
background-color: #f9f9f9;
border-left: 5px solid #4CAF50;
border-radius: 4px;
min-height: 60px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
}
.success {
color: #4CAF50;
font-weight: bold;
}
.error {
color: #f44336;
font-weight: bold;
}
.info {
color: #2196F3;
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 ECS Fargate Frontend</h1>
<div class="status">
<p class="success">✅ Frontend Container is Running!</p>
<p class="info">📍 Region: ap-northeast-1 (Tokyo)</p>
<p class="info">⚙️ Powered by: AWS ECS Fargate + Nginx</p>
</div>
<div class="buttons">
<button id="apiTestButton" class="button">🔗 API Test (Phase 4)</button>
<button id="databaseTestButton" class="button secondary" disabled>
🗄️ Database Test (Phase 5)
</button>
</div>
<div id="result"></div>
</div>
<script>
const resultDiv = document.getElementById('result');
const apiTestButton = document.getElementById('apiTestButton');
const databaseTestButton = document.getElementById('databaseTestButton');
// API Test Button
apiTestButton.addEventListener('click', async () => {
resultDiv.innerHTML = '<p class="info">⏳ Calling API...</p>';
apiTestButton.disabled = true;
try {
// Phase 4でInternal ALB経由でAPIを呼び出す
// 現時点ではAPIがないのでエラーメッセージを表示
resultDiv.innerHTML = `
<p class="error">⚠️ API is not available yet.</p>
<p>Please complete <strong>Phase 4</strong> to enable API functionality.</p>
<p class="info">Phase 4では、Internal ALB と ECS API コンテナを作成します。</p>
`;
} catch (error) {
resultDiv.innerHTML = `<p class="error">❌ Error: ${error.message}</p>`;
} finally {
apiTestButton.disabled = false;
}
});
// Database Test Button (Phase 5で有効化)
databaseTestButton.addEventListener('click', async () => {
resultDiv.innerHTML = '<p class="info">⏳ Testing database connection...</p>';
});
// 初期メッセージ
window.addEventListener('load', () => {
resultDiv.innerHTML = `
<p class="success">✨ Frontend is ready!</p>
<p>This is <strong>Phase 3</strong> (ECS Frontend).</p>
<p class="info">Click buttons above to test API and Database connections.</p>
`;
});
</script>
</body>
</html>
EOF
index.htmlの説明:
- シンプルで見やすいUI
- 「API Test」ボタン(Phase 4で機能)
- 「Database Test」ボタン(Phase 5で有効化)
- レスポンシブデザイン対応
ステップ4: 作成確認
# ファイルが作成されたか確認
ls -la
# 出力例:
# Dockerfile
# index.html
7-1-3. APIのファイル準備
ステップ1: apiディレクトリ作成
# プロジェクトルートに移動
cd my-ecs-project
# apiディレクトリ作成
mkdir -p api
cd api
ステップ2: Dockerfile作成
cat > Dockerfile << 'EOF'
# ビルドステージ
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod ./
COPY main.go ./
RUN go mod download
RUN go build -o server main.go
# 実行ステージ
FROM alpine:latest
RUN apk add --no-cache curl ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
EOF
Dockerfileの説明:
- マルチステージビルドでイメージサイズを削減
- Go 1.23を使用
- ポート8080で起動
ステップ3: main.go作成
cat > main.go << 'EOF'
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"time"
)
type Response struct {
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
Hostname string `json:"hostname"`
Status string `json:"status"`
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
hostname, _ := os.Hostname()
response := Response{
Message: "API is healthy",
Timestamp: time.Now(),
Hostname: hostname,
Status: "ok",
}
json.NewEncoder(w).Encode(response)
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
hostname, _ := os.Hostname()
response := Response{
Message: "Hello from ECS Fargate API!",
Timestamp: time.Now(),
Hostname: hostname,
Status: "success",
}
json.NewEncoder(w).Encode(response)
}
func main() {
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/api/test", apiHandler)
log.Println("API Server starting on :8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
EOF
main.goの説明:
-
/health: ヘルスチェック用エンドポイント -
/api/test: API Test用エンドポイント - CORS対応
- ホスト名を返す(どのコンテナから応答したかわかる)
ステップ4: go.mod作成
cat > go.mod << 'EOF'
module api
go 1.23
EOF
ステップ5: 作成確認
# ファイルが作成されたか確認
ls -la
# 出力例:
# Dockerfile
# main.go
# go.mod
7-2. AWS CLIでECRログイン
ステップ1: ECR URL取得
# Phase 2のディレクトリに移動
cd my-ecs-project/terraform/phase2-security
# ECR URLを取得
terraform output ecr_front_url
terraform output ecr_api_url
出力例:
ecr_front_url = "014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/front"
ecr_api_url = "014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/api"
📝 確認ポイント:
- リポジトリ名に
myproject/frontのようにスラッシュが含まれている - これが正しい形式です
ステップ2: ECRにログイン
# AWSアカウントID部分を取得(最初の数字部分)
ECR_REGISTRY=$(terraform output -raw ecr_front_url | cut -d'/' -f1)
echo $ECR_REGISTRY
# 出力例: 014936403628.dkr.ecr.ap-northeast-1.amazonaws.com
# ECRにログイン
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS --password-stdin $ECR_REGISTRY
成功メッセージ:
Login Succeeded
ポイント:
- ECRログインは12時間有効
- 有効期限が切れたら再度ログインが必要
7-3. Frontendイメージのビルド & Push
ステップ1: ECR URL取得(変数に保存)
cd my-ecs-project/terraform/phase2-security
# ECR URLを変数に保存
ECR_FRONT_URL=$(terraform output -raw ecr_front_url)
echo $ECR_FRONT_URL
# 出力例: 014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/front
ステップ2: frontディレクトリに移動
cd my-ecs-project/front
ステップ3: Dockerイメージをビルド
# イメージをビルド(ECR URLを直接タグに指定)
docker build -t $ECR_FRONT_URL:latest .
実行結果:
[+] Building 15.2s (8/8) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 98B
=> [internal] load .dockerignore
=> [internal] load metadata for docker.io/library/nginx:alpine
=> [1/2] FROM docker.io/library/nginx:alpine
=> [internal] load build context
=> => transferring context: 1.23kB
=> [2/2] COPY index.html /usr/share/nginx/html/
=> exporting to image
=> => exporting layers
=> => writing image sha256:abc123...
=> => naming to 014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/front:latest
ステップ4: ECRにPush
docker push $ECR_FRONT_URL:latest
実行結果:
The push refers to repository [014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/front]
5f70bf18a086: Pushed
a08e4f2d9859: Pushed
latest: digest: sha256:def456... size: 1234
ステップ5: Push成功確認
# ECRイメージ確認
aws ecr describe-images --repository-name myproject/front
実行結果:
{
"imageDetails": [
{
"registryId": "014936403628",
"repositoryName": "myproject/front",
"imageDigest": "sha256:def456...",
"imageTags": [
"latest"
],
"imageSizeInBytes": 9876543,
"imagePushedAt": "2024-12-20T10:30:00+00:00",
"imageScanStatus": {
"status": "COMPLETE",
"description": "The scan completed successfully"
}
}
]
}
✅ Push成功!
7-4. APIイメージのビルド & Push
ステップ1: ECR URL取得(変数に保存)
cd my-ecs-project/terraform/phase2-security
# ECR URLを変数に保存
ECR_API_URL=$(terraform output -raw ecr_api_url)
echo $ECR_API_URL
# 出力例: 014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/api
ステップ2: apiディレクトリに移動
cd my-ecs-project/api
ステップ3: Dockerイメージをビルド
# イメージをビルド
docker build -t $ECR_API_URL:latest .
実行結果:
[+] Building 45.3s (15/15) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 250B
=> [internal] load .dockerignore
=> [internal] load metadata for docker.io/library/golang:1.23-alpine
=> [builder 1/5] FROM docker.io/library/golang:1.23-alpine
=> [internal] load build context
=> => transferring context: 890B
=> [builder 2/5] WORKDIR /app
=> [builder 3/5] COPY go.mod ./
=> [builder 4/5] COPY main.go ./
=> [builder 5/5] RUN go build -o server main.go
=> [stage-1 1/3] FROM docker.io/library/alpine:latest
=> [stage-1 2/3] RUN apk add --no-cache curl ca-certificates
=> [stage-1 3/3] COPY --from=builder /app/server .
=> exporting to image
=> => exporting layers
=> => writing image sha256:ghi789...
=> => naming to 014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/api:latest
ステップ4: ECRにPush
docker push $ECR_API_URL:latest
実行結果:
The push refers to repository [014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/api]
1a2b3c4d5e6f: Pushed
7g8h9i0j1k2l: Pushed
latest: digest: sha256:jkl012... size: 2345
ステップ5: Push成功確認
# ECRイメージ確認
aws ecr describe-images --repository-name myproject/api
実行結果:
{
"imageDetails": [
{
"registryId": "014936403628",
"repositoryName": "myproject/api",
"imageDigest": "sha256:jkl012...",
"imageTags": [
"latest"
],
"imageSizeInBytes": 12345678,
"imagePushedAt": "2024-12-20T10:35:00+00:00",
"imageScanStatus": {
"status": "COMPLETE",
"description": "The scan completed successfully"
}
}
]
}
✅ Push成功!
7-5. シェルスクリプト化(推奨)
今後のイメージ更新を簡単にするため、シェルスクリプトを作成します。
7-5-1. スクリプト作成
cd my-ecs-project
cat > push-images.sh << 'SCRIPT'
#!/bin/bash
set -e # エラーで停止
echo "========================================="
echo " Docker Image Push to ECR"
echo "========================================="
echo ""
# Terraform outputからECR URLを取得
cd terraform/phase2-security
ECR_FRONT_URL=$(terraform output -raw ecr_front_url)
ECR_API_URL=$(terraform output -raw ecr_api_url)
ECR_REGISTRY=$(echo $ECR_FRONT_URL | cut -d'/' -f1)
echo "ECR URLs:"
echo " Front: $ECR_FRONT_URL"
echo " API: $ECR_API_URL"
echo ""
# ECRログイン
echo "Logging in to ECR..."
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS --password-stdin $ECR_REGISTRY
echo "✅ ECR Login successful"
echo ""
# プロジェクトルートに移動
cd ../..
# Frontendイメージ
echo "========================================="
echo " Building Frontend image..."
echo "========================================="
cd front
docker build -t $ECR_FRONT_URL:latest .
echo "✅ Frontend image built"
echo ""
echo "Pushing Frontend image..."
docker push $ECR_FRONT_URL:latest
echo "✅ Frontend image pushed"
echo ""
# APIイメージ
echo "========================================="
echo " Building API image..."
echo "========================================="
cd ../api
docker build -t $ECR_API_URL:latest .
echo "✅ API image built"
echo ""
echo "Pushing API image..."
docker push $ECR_API_URL:latest
echo "✅ API image pushed"
echo ""
# Push確認
echo "========================================="
echo " Verifying images in ECR..."
echo "========================================="
echo "Frontend:"
aws ecr describe-images --repository-name myproject/front \
--query 'imageDetails[0].[imageTags[0],imagePushedAt]' \
--output table
echo ""
echo "API:"
aws ecr describe-images --repository-name myproject/api \
--query 'imageDetails[0].[imageTags[0],imagePushedAt]' \
--output table
echo ""
echo "========================================="
echo " ✅ All images pushed successfully!"
echo "========================================="
SCRIPT
chmod +x push-images.sh
7-5-2. スクリプト実行
./push-images.sh
メリット:
- 一度の実行でFrontendとAPI両方をPush
- ECR URLを自動取得
- エラーハンドリング付き
- Push後の確認も自動
8. 動作確認
8-1. AWSコンソールで確認
ECR Repository:
- AWSコンソール → ECR → Repositories
- 2つのRepositoryが作成されていることを確認
-
myproject/front(スラッシュあり) -
myproject/api(スラッシュあり)
-
イメージの確認:
- Repository名をクリック
-
latestタグのイメージが存在することを確認 - Image scan結果を確認(脆弱性の有無)
8-2. Terraform出力で確認
cd my-ecs-project/terraform/phase2-security
terraform output
結果:
ecr_front_url = "014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/front"
ecr_front_arn = "arn:aws:ecr:ap-northeast-1:014936403628:repository/myproject/front"
ecr_api_url = "014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/api"
ecr_api_arn = "arn:aws:ecr:ap-northeast-1:014936403628:repository/myproject/api"
確認ポイント:
- リポジトリ名に
myproject/frontのようにスラッシュが含まれている
8-3. AWS CLIで確認
# Frontendイメージ確認
aws ecr describe-images --repository-name myproject/front \
--query 'imageDetails[*].[imageTags[0],imageSizeInBytes,imagePushedAt]' \
--output table
# APIイメージ確認
aws ecr describe-images --repository-name myproject/api \
--query 'imageDetails[*].[imageTags[0],imageSizeInBytes,imagePushedAt]' \
--output table
9. トラブルシューティング
9-1. エラー1: ECRログイン失敗
エラーメッセージ:
Error saving credentials: error storing credentials
原因:
- Docker Desktopの認証情報ストアの問題
解決策:
# ~/.docker/config.jsonを確認
cat ~/.docker/config.json
# credsStoreを削除または修正
# "credsStore": "desktop" の行を削除
9-2. エラー2: イメージのpush失敗
エラーメッセージ:
denied: Your authorization token has expired
原因:
- ECRログインの有効期限切れ(12時間)
解決策:
# 再度ログイン
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS --password-stdin 014936403628.dkr.ecr.ap-northeast-1.amazonaws.com
9-3. エラー3: Dockerfileが見つからない
エラーメッセージ:
unable to prepare context: unable to find 'Dockerfile'
原因:
- ディレクトリが間違っている
解決策:
# 正しいディレクトリに移動
cd my-ecs-project/front # Frontendの場合
cd my-ecs-project/api # APIの場合
# Dockerfileが存在することを確認
ls -la Dockerfile
9-4. エラー4: ECRリポジトリ名が間違っている
エラーメッセージ:
repository myproject-front not found
原因:
- リポジトリ名が間違っている
- 正しくは
myproject/front(スラッシュあり)
確認方法:
# Terraform outputで正しいリポジトリ名を確認
cd terraform/phase2-security
terraform output ecr_front_url
# 出力例(正しい):
# "014936403628.dkr.ecr.ap-northeast-1.amazonaws.com/myproject/front"
# ↑
# スラッシュがある
# AWSコンソールでも確認
# ECR → Repositories → リポジトリ名を確認
解決策:
# 正しいリポジトリ名でpush
ECR_FRONT_URL=$(terraform output -raw ecr_front_url)
echo $ECR_FRONT_URL # スラッシュが含まれているか確認
docker build -t $ECR_FRONT_URL:latest .
docker push $ECR_FRONT_URL:latest
よくある間違い:
| 間違い | 正しい |
|---|---|
myproject-front |
myproject/front |
myproject_front |
myproject/front |
myprojectfront |
myproject/front |
9-5. エラー5: Go buildエラー(API)
エラーメッセージ:
go: finding module for package ...
build failed
原因:
- go.modが正しく作成されていない
解決策:
cd my-ecs-project/api
# go.modを再作成
cat > go.mod << 'EOF'
module api
go 1.23
EOF
# 再度ビルド
docker build -t $ECR_API_URL:latest .
9-6. エラー6: イメージが大きすぎる
現象:
- pushに時間がかかる
- ECRのストレージコストが高い
原因:
- マルチステージビルドを使用していない
解決策(API Dockerfile改善):
# 現在のDockerfile(シングルステージ)
FROM golang:1.23
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]
# 改善版(マルチステージ)
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod ./
COPY main.go ./
RUN go mod download
RUN go build -o server main.go
FROM alpine:latest
RUN apk add --no-cache curl ca-certificates
WORKDIR /root/
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
効果:
- イメージサイズが約1/10に削減(700MB → 70MB)
- セキュリティリスクの低減
10. ベストプラクティス
10-1. イメージタグの付け方
❌ 悪い例:
# latestタグのみ
docker tag myproject-front:latest ecr.../myproject/front:latest
docker push ecr.../myproject/front:latest
✅ 良い例:
# latestタグ + バージョンタグ
docker build -t ecr.../myproject/front:latest .
docker tag ecr.../myproject/front:latest ecr.../myproject/front:v1.0.0
docker push ecr.../myproject/front:latest
docker push ecr.../myproject/front:v1.0.0
理由:
- バージョン管理が容易
- ロールバックが簡単
- トラブル時にどのバージョンか特定できる
10-2. 環境変数の管理
❌ 悪い例:
# Dockerfile内にハードコード
ENV DB_PASSWORD="my-secret-password"
✅ 良い例:
# Dockerfileでは定義しない
# ECS Task Definitionで環境変数を設定
# または Secrets Managerを使用
10-3. .dockerignoreの活用
推奨設定:
# front/.dockerignore
node_modules/
.git/
*.log
.DS_Store
# api/.dockerignore
vendor/
.git/
*.log
.DS_Store
go.sum
効果:
- ビルド時間の短縮
- イメージサイズの削減
- 不要なファイルの混入防止
11. 🎉 Phase 2完了!
11-1. できたこと
✅ ECR Repository × 2の作成
✅ Lifecycle Policyの設定
✅ Dockerイメージのビルド & Push
✅ front/ と api/ ディレクトリの作成
✅ Dockerfile と アプリケーションファイルの作成
11-2. Phase 2で作成したリソース(合計)
| Phase | リソース数 | 主要リソース |
|---|---|---|
| Phase 2-1 | 15 | Security Group × 5 |
| Phase 2-2 | 4 | ECR Repository × 2 |
| 合計 | 19 | - |
11-3. 作成したファイル
| ディレクトリ | 主要ファイル |
|---|---|
| terraform/phase2-security/ | ecr.tf, outputs.tf |
| front/ | Dockerfile, index.html 等 |
| api/ | Dockerfile, main.go 等 |
📁 front/ api/ の詳細は GitHub: fargate-iac-02 を参照
11-4. 🎯 マイルストーン2達成!
🎉 セキュリティ基盤が完成しました!
Security Group × 5、ECR × 2が稼働し、
DockerイメージがECRにpush完了しています。
次のPhaseでは、これらのイメージを使って
ECS Fargateでコンテナを起動します!
12. 次回予告
第6回: Phase 3-1 - Public ALB
次回は、Phase 3として以下を作成します:
- Public ALB(Application Load Balancer)
- Target Group
- Listener
- ALBの役割とヘルスチェック
Phase 3-1が完了すると、外部からのアクセスを受け付ける準備が整います!
13. 参考リンク
(続く)