😊

【初心者向け】AWS × Go × React で構築するモダン・フルスタックWebアプリ開発

に公開

はじめに

前回の記事ではバックエンドをAWSで構築しました。
本記事では、DBとフロントエンドのローカル開発環境の構築から、AWS上でのセキュアなインフラ構築、CI/CDによる自動デプロイ、そして最新のTailwind CSS v4を用いたUI刷新までの手順です。

前提条件

  • Dockerをインストール済み
  • AWSアカウント作成済み
  • IAMユーザーに以下の権限を付与済み
    • AmazonEC2ContainerRegistryPowerUser (ECR の操作:イメージ登録・取得)
    • AmazonECS_FullAccess (ECS の操作:サービス・タスク管理)
  • IAMユーザーのアクセスキーを作成済み
  • GitHubのアカウント作成済み
  • Node.jsをインストール済み

本記事で実現したこと

ローカルでのコンテナ開発から、AWS上での運用までを一気通貫で自動化した、「スケーラブルなWebアプリケーション」を構築しました。

  • フロントエンド: CloudFront + S3 による高速な静的配信と、最新の Tailwind CSS v4 によるモダンUI。
  • バックエンド: ECS Fargate によるサーバーレス・コンテナ運用。
  • データベース: RDS (MySQL) による堅牢なデータ管理。
  • 自動化 (CI/CD): GitHubへの push だけで、フロント・バック両方が数分で自動更新される環境。

システム構成図

本アプリは、セキュリティと可用性を考慮した「三層アーキテクチャ」を採用しています。

各レイヤーの役割

レイヤー 使用サービス 役割
配信レイヤー CloudFront / S3 世界中のエッジサーバーからReactアプリを高速配信。バックエンド(API)への通信もここを経由させ、ドメインを統一。
アプリケーションレイヤー ALB / ECS Fargate 負荷分散(ALB)を行い、サーバーの管理が不要なコンテナ実行環境(Fargate)でGo APIを稼働。
データレイヤー RDS MySQL プライベートサブネットに配置し、外部からの直接接続を遮断。ECSからのみ通信を許可するセキュアな構造。

開発・デプロイフロー

コード修正からAWSに反映までの流れも完全に自動化しています。

  1. Local: Docker Compose で「Go + MySQL」を動かし、爆速で開発。
  2. GitHub: コードを push すると GitHub Actions が起動。
  3. Build: GoはDockerイメージ化、Reactはビルドして最適化。
  4. Deploy: AWS ECR/S3 へアップロードし、ECSサービスを自動更新。

最終的に、以下のようなアプリが完成しました。

ここからは、実際の手順やソースコードとコマンドを詳しく記述していきます。

Go + MySQLの接続

1. 現在のディレクトリ構造

go-docker-project/
├── main.go            (修正)
├── go.mod             (既存)
├── Dockerfile         (既存)
└── docker-compose.yml (新規作成)

2. docker-compose.yml の作成

プロジェクトのルートフォルダ(main.goがある場所)に、新しく docker-compose.yml というファイルを作成し、以下の内容を貼り付けてください。 これが「Goコンテナ」と「MySQLコンテナ」を同時に立ち上げる指示書になります。

ソースコード
docker-compose.yml
services:
  # MySQLデータベースコンテナ
  db:
    image: mysql:8.4
    container_name: mysql-db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: myappdb
      MYSQL_USER: user
      MYSQL_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - db-data:/var/lib/mysql

  # GoのAPIコンテナ
  app:
    build: .
    container_name: go-api
    ports:
      - "8080:8080"
    environment:
      DB_USER: user
      DB_PASS: password
      DB_HOST: db   
      DB_NAME: myappdb
    depends_on:
      db:
        condition: service_started

volumes:
  db-data:

3. Goのコード(main.go)をMySQL接続用に修正

DBコンテナが立ち上がってからMySQLが実際にクエリを受け付けるようになるまでタイムラグがあるため、接続リトライ処理を組み込んでいます。

ソースコード
main.go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	// 環境変数から接続情報を取得
	dbUser := os.Getenv("DB_USER")
	dbPass := os.Getenv("DB_PASS")
	dbHost := os.Getenv("DB_HOST")
	dbName := os.Getenv("DB_NAME")

	// 接続文字列 (DSN) を作成
	dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s", dbUser, dbPass, dbHost, dbName)

	// DBが開くまで少し待機(MySQLの起動完了を待つため)
	var db *sql.DB
	var err error
	for i := 0; i < 10; i++ {
		db, err = sql.Open("mysql", dsn)
		if err == nil {
			err = db.Ping()
			if err == nil {
				break
			}
		}
		log.Println("DBの起動を待っています...")
		time.Sleep(3 * time.Second)
	}

	if err != nil {
		log.Fatal("DB接続に失敗しました: ", err)
	}
	defer db.Close()

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		err := db.Ping()
		if err != nil {
			fmt.Fprintf(w, "DB接続エラー: %v", err)
			return
		}
		fmt.Fprintf(w, "MySQLとの接続に成功しました!Go APIは正常に動作しています。")
	})

	fmt.Println("サーバーが8080ポートで起動しました...")
	http.ListenAndServe(":8080", nil)
}

4. 依存ライブラリの追加

MySQL 8.4では、古いクライアント(ライブラリ)との互換性のために、Goのコード側で使用している github.com/go-sql-driver/mysql が最新であることを確認してください。

もし接続時に「Authentication plugin... cannot be loaded」のようなエラーが出た場合は、ターミナルで以下のコマンドを打ち、最新のドライバーに更新してください。

# 依存関係を追加
go get github.com/go-sql-driver/mysql
# go.mod / go.sum を更新
go mod tidy

5. 動作確認

準備ができたら、Docker Composeで2つのコンテナを一気に起動します。

# コンテナをビルドして起動
docker-compose up --build

ログに「サーバーが8080ポートで起動しました...」と出ればOKです。
ブラウザで「http://localhost:8080」にアクセスしてください。
「MySQLとの接続に成功しました!」
と表示されれば、ローカル環境の構築は完了です。

これでローカルでの「Go + DB」の土台が整いました。この構成をAWS RDSへと移行し、より実践的なインフラ構成を目指します。

構築中に発生したトラブルと解決策

① Port is already allocated (8080)
以前起動していたコンテナや、他のアプリがポート8080を占有している場合に発生しました。

解決策: 一度すべてのコンテナを掃除することで解消しました。

# すべてのコンテナを停止
docker stop $(docker ps -q)
# すべてのコンテナを削除
docker rm $(docker ps -a -q)
# ネットワークのゴミを掃除
docker network prune -f
# イメージを再構築して起動
docker compose up --build

ECSからRDSの接続確認

AWS上にデータベース(RDS)を構築し、ECS上のGoアプリから安全に接続します。今回はセキュリティを重視し、RDSをインターネットに公開しない「パブリックアクセス不可」の設定で構築します。

1. RDSの構築

RDSを新規作成します。設定は以下の通りです。

項目 補足
エンジンのタイプ MySQL
エンジンバージョン MySQL 8.4.7
パブリックアクセス いいえ パブリックアクセス不可で構築
DBインスタンス識別子 my-app-db
マスターユーザー名 admin
セキュリティグループ rds-sg(新規作成) セキュリティグループ名の例
アベイラビリティーゾーン ap-northeast-1a
拡張モニタリング 有効化可(任意) ログ監視を行う場合は有効化推奨
最初のデータベース名 myappdb
マイナーバージョン自動アップグレード 任意(ダウンタイム考慮) ダウンタイムを許容する場合は有効化可

2. セキュリティグループの連結設定

RDSにパブリックアクセスを許可しない代わりに、「ECSからの通信だけを通す」設定をセキュリティグループ(SG)で行いました。

RDS用SGのインバウンドルール

ルール 補足
タイプ MySQL/Aurora (3306) RDS の標準ポート
ソース ECSサービスのセキュリティグループ(例: sg-xxxxxxxx ECS のみからのアクセスを許可

この設定により、インターネットからは隔離されつつ、AWS内部の特定のアプリからのみ通信が可能なセキュアな環境が整いました。

3. GitHub Secretsによる環境変数の管理

RDSのエンドポイントやパスワードなどの機密情報を、コードに直接書かずにGitHub Secretsで管理するようにします。
GitHubリポジトリの Settings > Secrets and variables > Actions に以下の環境変数を追加します。

環境変数 補足
DB_USER admin RDS作成時のマスターユーザー名
DB_PASS ******** RDSのパスワード(Secretsに登録)
DB_HOST my-app-db.xxxxx.ap-northeast-1.rds.amazonaws.com RDSのエンドポイント
DB_NAME myappdb 初期データベース名

4. GitHub Actionsの修正

deploy.yml 内の amazon-ecs-render-task-definition ステップを修正し、デプロイ時にSecretsの値をECSのコンテナに環境変数として渡すようにします。

deploy.yml
      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: go-container
          image: ${{ steps.build-image.outputs.image }}
          # ここに環境変数を追加!
          environment-variables: |
            DB_USER=${{ secrets.DB_USER }}
            DB_PASS=${{ secrets.DB_PASS }}
            DB_HOST=${{ secrets.DB_HOST }}
            DB_NAME=${{ secrets.DB_NAME }}

以下は environment-variables に渡す環境変数の対応表です。GitHub Secrets とコンテナ内での役割を明確化します。

環境変数 GitHub Secrets コンテナ内での用途
DB_USER DB_USER DB 接続ユーザー名
DB_PASS DB_PASS DB 接続パスワード
DB_HOST DB_HOST RDS のエンドポイント
DB_NAME DB_NAME 使用するデータベース名

5. 動作確認

GitHubへプッシュします。

# 変更したファイルをステージング
git add .
# git add task-definition.json
# コミット
git commit -m "feat: add RDS environment variables and MySQL connection"
# プッシュ(デプロイ開始)
git push origin main

ECSのサービスログを確認し、以下の出力をもって疎通を確認します。

ECS ログの確認手順

  1. AWS コンソールにログインし、[Elastic Container Service] を開く
  2. 対象のクラスター → 今回デプロイしたクラスター名をクリック
  3. [サービス] タブから動作中のサービス(例: go-api-service)を選択
  4. 画面中央の [ログ] タブをクリック
    ここに、main.go の fmt.Println や log.Println で出力した内容が表示されます。
サーバーが8080ポートで起動しました...

実際にブラウザからECSのロードバランサーURLへアクセスし、DB接続済みのメッセージが表示されます。

MySQLとの接続に成功しました!Go APIは正常に動作しています。

パブリックアクセスを制限したことで、より実践的なインフラ構成へと一歩近づきました。 次はこの環境で実際にDBへのデータ保存や取得を行うAPI機能を実装していきます。

ALB(Application Load Balancer)の導入

現在の「パブリックIP:8080」という不安定なアクセス方法を卒業し、ALB(Application Load Balancer)という「正式な窓口」を設置することです。また、これに合わせてCloudWatchでログを永続化し、トラブル時にすぐ原因追跡できるようにします。

1. ターゲットグループ(Target Group)の作成

ALBの通信先となるECSコンテナを定義するグループを作成します。

AWSコンソール > EC2 > ターゲットグループ を開きます。
「ターゲットグループの作成」をクリック。

ターゲットグループ作成時の推奨設定例:

項目 補足
ターゲットタイプ IPアドレス ECS タスクへルーティングする場合は IP を選択
ターゲットグループ名 my-api-tg 管理しやすい名前を指定
プロトコル / ポート HTTP / 8080 Go アプリの待ち受けポート
VPC ECS や RDS と同じ VPC 同一 VPC を選択するのが標準
ヘルスチェックパス / アプリのヘルスチェック用エンドポイント

2. ALB(Application Load Balancer)の作成

正式な「サービスの窓口」となるロードバランサーを構築します。

EC2 > ロードバランサー を開きます。
「ロードバランサーの作成」 > Application Load Balancer を選択。

ロードバランサー作成時の推奨設定例:

項目 補足
名前 my-api-alb 管理しやすい名前を指定
スキーム インターネット向け インターネット公開が必要な場合
ネットワークマッピング 2 つ以上の AZ 高可用性のため複数 AZ を選択
セキュリティグループ alb-sg(新規作成) ALB 用 SG を別途用意
インバウンドルール HTTP (80) を 0.0.0.0/0 必要に応じて HTTPS に切替え推奨
リスナー HTTP / 80 ALB のリスナーを設定
ルーティング先(アクション) my-api-tg へ転送 先に作成したターゲットグループを指定

3. セキュリティグループの連結(SG ID指定)

ここが重要ポイントです。ECS(Goアプリ)のSG設定を変更し、「ALBからの通信以外は一切受け付けない」状態にします。

ECS用セキュリティグループ(例):

項目 設定例 補足
タイプ カスタムTCP アプリがリッスンするポート(例: 8080)
ポート範囲 8080
ソース ALB 用 SG の ID(例: sg-xxxxxx) ALB からのみ受け付けるよう指定

効果: コンテナの IP を直接叩く通信は遮断され、ALB 経由のみアクセス可能になります。

なぜ「ID」で指定するのか?

ソース欄に IP アドレス(0.0.0.0/0)ではなく SG ID(例: sg-xxxx)を指定する理由:

  • SG ID 指定: 「このセキュリティグループが付いている通信相手なら、IP アドレスが何でも許可する」という設定
  • IP 変動対応: AWS では ALB や ECS の IP アドレスが変わることがあるが、SG 連結すれば自動的に追いかけて許可し続ける
  • セキュリティ向上: 0.0.0.0/0 のような広い範囲許可より安全

4. CloudWatch Logs による監視の有効確認

ECS コンテナが CloudWatch Logs にログを送信しているかを手順で確認します。

  1. AWS コンソールで [Elastic Container Service] を開く
  2. 対象のクラスターを選択 → [タスク] をクリック
  3. 使用中のタスク定義(例: my-go-task)の最新リビジョンを開く
  4. コンテナの詳細内の ログ設定 を確認
  • ログドライバー: awslogs になっていること
  • オプションに awslogs-group(例: /ecs/my-go-task)が設定されていること

メリット

  • アプリがクラッシュした場合や ALB のヘルスチェックが失敗した際に、過去ログを CloudWatch で遡って原因を調査できます。

なぜ awslogs が重要か

  • ECS のコンテナは停止・再作成が頻繁に発生するため、コンテナ内部にのみログがあると消失してしまう。
  • awslogs を使えばログが CloudWatch に蓄積され、コンテナが消えてもログを参照できます。
    awslogsあり: コンテナが消えても、ログはCloudWatchという別の場所に保管されるため、「なぜ昨晩アプリが落ちたのか?」を後から調査できるようになります。

動作確認

セキュリティグループの変更が完了したら、ALB 経由での動作を確認します。

  1. AWS コンソール EC2 > ロードバランサー を開く
  2. 作成した ALB を選択
  3. 「説明」タブから「DNS 名」(例: go-api-alb-12345.ap-northeast-1.elb.amazonaws.com)をコピー
  4. ブラウザで ALB の DNS 名にアクセスして動作確認

トラブルシューティング

ALB作成直後、一時的に Unhealthy になりましたが、以下の理由によるタイムラグであることを確認しました。

  1. ヘルスチェックの連続合格回数: ALBが「正常」と判断するまで数回のチェックが必要。
  2. DNSの浸透: ALBのDNS名がインターネット上で解決されるまでの待ち時間。

インフラの「窓口(ALB)」「部屋(ECS)」「金庫(RDS)」が、すべて専用のセキュリティグループで保護されたプロ仕様の構成が完成しました。
次は実際にデータを保存・取得するAPI機能を実装していきます。

Go × GORMで実現する、RDSへの自動マイグレーションとCRUD API実装

標準パッケージ database/sql ではなく、実務で広く使われている GORM を導入します。これにより、複雑なSQLを書くことなく、Goの構造体(Struct)を定義するだけでDB操作が可能になります。

1. ライブラリのインストール

ターミナル(Goのプロジェクトルート)で以下のコマンドを実行してください。

go get gorm.io/gorm
go get gorm.io/driver/mysql

2. 接続リトライ処理の実装

Docker ComposeやECS環境では、アプリがDBより先に起動してしまい接続エラーになることがあります。これを防ぐため、接続に成功するまで数回リトライする「堅牢な接続ロジック」を組み込みました。

ソースコード
main.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User モデルの定義(これがそのままDBのテーブル構造になります)
type User struct {
	ID   uint   `gorm:"primaryKey" json:"id"`
	Name string `json:"name"`
}

func main() {
	// 接続情報の構築
	dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASS"), // ECS側の環境変数キーと一致させる
		os.Getenv("DB_HOST"),
		os.Getenv("DB_NAME"),
	)

	var db *gorm.DB
	var err error

	// DB接続のリトライ処理(最大10回)
	for i := 0; i < 10; i++ {
		db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
		if err == nil {
			break
		}
		log.Printf("DB接続試行 %d回目: 失敗。再試行します...", i+1)
		time.Sleep(2 * time.Second)
	}

	if err != nil {
		log.Fatal("DBに接続できませんでした:", err)
	}
	log.Println("DB接続成功!")

	// ★ 自動マイグレーション実行(テーブルがなければ作成)
	db.AutoMigrate(&User{})

	// APIハンドラーの実装
	http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.Method {
		case http.MethodGet:
			var users []User
			db.Find(&users)
			json.NewEncoder(w).Encode(users)
		case http.MethodPost:
			var user User
			json.NewDecoder(r.Body).Decode(&user)
			db.Create(&user)
			json.NewEncoder(w).Encode(user)
		default:
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	})

	// ALBヘルスチェック用
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Server is running")
	})

	log.Println("Server started at :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

3. 動作確認

コンテナ起動: docker-compose up --build

PowerShellでのcurl実行
事象: WindowsのPowerShellで curl を使うと、型変換エラーやJSONのパースエラーが発生。

解決: PowerShell標準の Invoke-RestMethod を使用。

Invoke-RestMethod -Method Post -Uri "http://localhost:8080/users" -ContentType "application/json" -Body '{"name":"Tanaka"}'

期待する結果: {"id":1, "name":"Tanaka"} のようなJSONが返ってくれば成功です!

次にDBに保存された一覧を取得します。

curl http://localhost:8080/users

期待する結果: [{"id":1, "name":"Tanaka"}] と表示されれば、DBとの往復ができています。

4. GitHub へコードを Push する

変更した main.go や go.mod, go.sum を GitHub に送信します。これにより、GitHub Actions が自動的にビルドを開始し、ECS へ新しいイメージをデプロイします。
現在のターミナル(Go プロジェクトのルート)で以下を実行してください。

git add .
git commit -m "Add GORM, Migration, and CRUD API"
git push origin main

AWS(RDS)での動作確認
ALB の DNS 名を使って、AWS 上の RDS に対してデータを読み書きしてみましょう。 ※URL は ALB の DNS 名に置き換えてください。
ユーザーを登録する(POST)

Invoke-RestMethod -Method Post -Uri "http://[あなたのALBのDNS名]/users" -ContentType "application/json" -Body '{"name":"AWS-User"}'

ユーザー一覧を取得する(GET)

Invoke-RestMethod -Uri "http://[あなたのALBのDNS名]/users"

「コードを書く ➡ Git Push ➡ AWS上のDBテーブルが自動更新 ➡ APIが最新化」 というモダンな開発パイプラインが完成しました。
次はAWSにへ反映させます。

React + Vite 導入と API 疎通確認(CORS解決)

バックエンド(Go)とデータベース(RDS/Local)が整ったため、フロントエンド開発に着手します。モダンなビルドツール Vite を利用して React + TypeScript の環境を構築し、バックエンド API との接続を実現します。

1. フロントエンド環境構築(Vite + React)

これまでの backend フォルダとは別に、フロントエンド専用のプロジェクトを作成します。

プロジェクト作成

# Viteプロジェクトの生成
npm create vite@latest my-react-app -- --template react-ts
# フォルダに移動
cd my-react-app
# 必要なパッケージのインストール
npm install
# API通信ライブラリ Axios の導入
npm install axios
# 開発サーバーの起動
npm run dev

ブラウザで http://localhost:5173 を開き、ViteとReactのロゴが表示されたら成功です!

2. APIからデータを取得して表示する

src/App.tsx の中身をすべて消して、以下のコードに書き換えてください。作成したGoのAPI(http://localhost:8080/users)からデータを取ってくる処理です。

ソースコード
App.tsx
import { useEffect, useState } from 'react'
import axios from 'axios'

interface User {
  id: number
  name: string
}

function App() {
  const [users, setUsers] = useState<User[]>([])
  const [newName, setNewName] = useState('') // 入力フォーム用の状態

  useEffect(() => {
    fetchUsers()
  }, [])

  const fetchUsers = async () => {
    const response = await axios.get('http://localhost:8080/users')
    setUsers(response.data)
  }

  // 名前を登録する関数
  const addUser = async () => {
    if (!newName) return
    try {
      await axios.post('http://localhost:8080/users', { name: newName })
      setNewName('')    // 入力欄を空にする
      fetchUsers()      // 一覧を再取得
    } catch (error) {
      console.error("登録エラー:", error)
    }
  }

  return (
    <div style={{ padding: '40px', maxWidth: '600px', margin: '0 auto', fontFamily: 'sans-serif' }}>
      <h1>🚀 フルスタック連携アプリ</h1>
      
      {/* 登録フォーム */}
      <div style={{ marginBottom: '30px' }}>
        <input 
          value={newName} 
          onChange={(e) => setNewName(e.target.value)}
          placeholder="名前を入力"
          style={{ padding: '10px', fontSize: '16px' }}
        />
        <button onClick={addUser} style={{ padding: '10px 20px', marginLeft: '10px' }}>
          ユーザー追加
        </button>
      </div>

      <div style={{ background: '#f4f4f4', padding: '20px', borderRadius: '8px' }}>
        <h2>ユーザー一覧(RDS/Local DB)</h2>
        <ul>
          {users.map(user => (
            <li key={user.id}>ID: {user.id} - <strong>{user.name}</strong></li>
          ))}
        </ul>
      </div>
    </div>
  )
}

export default App

3. バックエンドのCORS対応

ブラウザのセキュリティ制限(Same-Origin Policy)により、異なるポート間での通信がブロックされるため、Go側のプログラムに CORS (Cross-Origin Resource Sharing) 許可の設定を追加します。

main.go
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
    // 全てのリクエストからのアクセスを許可
    w.Header().Set("Access-Control-Allow-Origin", "*")
    w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
    w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

    // プリフライトリクエスト(事前確認)への対応
    if r.Method == http.MethodOptions {
        w.WriteHeader(http.StatusOK)
        return
    }
    
    // --- ここから下は既存のコード ---
    w.Header().Set("Content-Type", "application/json")
    switch r.Method {
    case http.MethodGet:
        var users []User
        db.Find(&users)
        json.NewEncoder(w).Encode(users)
    case http.MethodPost:
        // ... (省略)
    }
})

※ 修正したら、必ず docker-compose up --build でバックエンドを再起動してください。

5. 動作確認

以下の手順でローカルでの動作確認を行います。

① バックエンド(Go)の起動

cd backend
docker-compose up --build

コンソールに「DB接続成功!」や「サーバーが8080ポートで起動しました...」が表示されることを確認してください。

② フロントエンド(React)の起動

cd my-react-app
npm run dev

③ ブラウザで確認

  • 開発サーバー: http://localhost:5173 にアクセス
  • フォームから名前を入力し送信 → ユーザー一覧が更新されれば成功

これで、「フロントエンドから指示を出し、バックエンドがDBを更新し、その結果を画面に表示する」というWebアプリの基本サイクルが完成しました。

フロントエンド(React)を追加した状態のフォルダ構成図です。

バックエンド(Go/Docker)とフロントエンド(Vite/React)は、プロジェクトのルートフォルダ内でそれぞれ独立したフォルダとして管理するのが一般的で、管理もしやすくなります。
プロジェクト全体のフォルダ構成

my-fullstack-app/           # プロジェクトのルート
├── backend/                # 【作成したGoプロジェクト】
│   ├── main.go             # Goのソースコード
│   ├── go.mod
│   ├── go.sum
│   ├── Dockerfile
│   └── docker-compose.yml  # DBやGoコンテナの設定

└── my-react-app/           # 【作成するフロントエンド】
    ├── src/
    │   ├── App.tsx         # APIを叩くメインのコード(ここを編集)
    │   └── main.tsx
    ├── index.html
    ├── package.json        # axiosなどのライブラリ管理
    ├── tsconfig.json       # TypeScriptの設定
    └── vite.config.ts      # Viteの設定

React アプリの AWS 公開(S3 + CloudFront)

ローカル環境(localhost)で開発した React アプリをビルドし、AWS の S3(ストレージ)と CloudFront(CDN)を組み合わせて世界中に公開します。最新のセキュリティ設定である OAC(Origin Access Control)を利用し、セキュアな配信環境を構築しています。

1. フロントエンドのビルド

まず、TypeScript などのソースコードをブラウザが解釈可能な静的ファイルに変換します。

# ビルドの実行
npm run build

これにより、プロジェクト直下に dist フォルダが生成され、公開用の index.html や assets が準備されました。

2. AWS S3 のセットアップ

S3 バケット作成時の推奨設定例:

項目 値(例) 補足
バケット名 my-react-app-YYYYMMDD 世界で一意の名前が必要
公開設定 パブリックアクセスをすべてブロック CloudFront 経由で公開する場合はバケットを非公開にする
バージョニング 任意 必要に応じて有効化
暗号化 S3 管理の暗号化(SSE-S3)等 機密性が高い場合は SSE-KMS を検討

3. CloudFront の構築

CloudFront を作成する際の手順と推奨設定をまとめます。

① Get started の設定

  • Distribution name: my-react-app-dist など、管理しやすい名前を指定
  • Distribution type: 「Single website or app」(デフォルトのまま)を選択
  • Domain: 独自ドメインを使わない場合は空欄のままで 「Next」

② Specify origin の設定

  • Origin type: S3 を選択
  • S3 origin: 作成したバケット(例: my-react-app-YYYYMMDD)を選択
  • Origin path: 空欄で問題なし
  • Allow private S3 bucket access to CloudFront: チェックを有効に
    → 「S3 を公開せず、CloudFront のみアクセス可能」という OAC 設定
  • Origin settings: 「Use recommended origin settings」を選択
    → 最適なアクセス制御(OAC)が自動作成される
  • Cache settings: 「Use recommended cache settings」を選択
    → S3 静的ファイル配信に最適なキャッシュ設定が適用
  • 「Next」をクリック

③ Enable Security の設定

  • WAF (Web Application Firewall): 学習目的なら「Do not enable security protections」(無効)を選択
  • 「Next」をクリック

④ Review and Create

  • 設定内容を確認して「Create distribution」をクリック

⑤ Default Root Object の設定

Distribution 作成後、以下の手順で index.html を既定オブジェクトに設定します。

  1. 作成したディストリビューションの詳細画面を開く
  2. 「General」タブを選択
  3. 「Settings」セクションの「Edit」ボタンをクリック
  4. スクロール → 「Default root object」に index.html を入力
  5. 「Save changes」をクリック

4. S3 バケットポリシーの適用

CloudFront が S3 のコンテンツを取得できるようにポリシーを設定します。

  1. CloudFront > 対象ディストリビューション > Origins タブ
  2. S3 オリジンを選択 → 「Edit」をクリック
  3. 表示されたポリシー JSON を copy
  4. S3 バケット > アクセス許可 > バケットポリシー
  5. ポリシーを貼り付けて保存

5. 公開と動作確認

S3 バケットに dist フォルダの中身をアップロードし、CloudFront のドメイン名(https://xxxx.cloudfront.net)にアクセスします。

結果: 無事に React アプリの画面が表示されることを確認。

現状フロントエンドは AWS 上にあるが、API(Go)がローカルのままのため、データ取得時にエラーが発生します。

フロントエンドの自動デプロイ構築とモノレポ化

プロジェクト全体の構成を「モノレポ(1つのリポジトリでフロントとバックを管理する構成)」に整理し、GitHub Actions を使ってフロントエンド(React)が自動で AWS S3 / CloudFront へデプロイされる仕組みを構築します。

1. プロジェクト構造の整理

フロントエンドとバックエンドを明確に分けるため、以下のディレクトリ構成に再編します。また、GitHub Actions が正しく認識されるよう、.github フォルダをリポジトリのルート階層へ配置します。

my-fullstack-app/ (Gitリポジトリルート)
├── .github/
│   └── workflows/
│       ├── deploy.yml          # バックエンド用
│       └── frontend-deploy.yml # フロントエンド用 (新規作成)
├── backend/                # Go / Docker 関連
└── my-react-app/           # React / Vite 関連

2. GitHub Secrets に S3 用の情報を追加

GitHubのリポジトリ設定(Settings > Secrets and variables > Actions)に、以下の変数を追加してください。

  • AWS_S3_BUCKET_NAME: 作成したS3バケット名(例: my-react-app-xxxx)
  • CLOUDFRONT_DISTRIBUTION_ID: CloudFrontの管理画面で確認できるディストリビューションID(例: E1ABC2DEFG3HIJ)

: AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY はバックエンド設定時に追加済みであれば、そのまま使用できます。

3. フロントエンド自動デプロイの設定

ルート直下の .github/workflows/ にfrontend-deploy.ymlを作成します。

ソースコード
frontend-deploy.yml
name: Frontend Deploy

on:
  push:
    branches: [ main ]
    paths:
      - 'my-react-app/**'
      - '.github/workflows/frontend-deploy.yml'

jobs:
  deploy:
    name: Build and Deploy React App
    runs-on: ubuntu-latest
    
    defaults:
      run:
        working-directory: my-react-app

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup Node.js v24 (LTS)
        uses: actions/setup-node@v4
        with:
          node-version: '24'
          cache: 'npm'
          cache-dependency-path: my-react-app/package-lock.json

      - name: Install Dependencies
        run: npm ci

      - name: Build Project
        run: npm run build

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Deploy to S3
        run: |
          aws s3 sync dist/ s3://${{ secrets.AWS_S3_BUCKET_NAME }} --delete

      - name: Create CloudFront Invalidation
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
            --paths "/*"

バックエンドの自動デプロイ設定(deploy.yml)も、backend/ フォルダの変更を監視するよう修正が必要です。

ソースコード
deploy.yml
name: Deploy to Amazon ECS

# mainブランチにコードがpushされたときにこのワークフローを実行します
on:
  push:
    branches:
      - "main"
    # 追加: backendフォルダ内の変更があった時のみ実行
    paths:
      - 'backend/**'
      - '.github/workflows/deploy.yml'

# 各設定値を環境変数として定義(後で変更しやすくするため)
env:
  AWS_REGION: ap-northeast-1          # デプロイ先のリージョン(東京)
  ECR_REPOSITORY: my-go-app          # ECRのリポジトリ名
  ECS_SERVICE: my-go-service        # ECSのサービス名
  ECS_CLUSTER: my-go-cluster        # ECSのクラスター名
  ECS_TASK_DEFINITION: my-go-task  # タスク定義のファミリー名
  CONTAINER_NAME: go-api-container # コンテナ名(タスク定義内の名前)

jobs:
  deploy:
    runs-on: ubuntu-latest # GitHub Actionsを実行するマシンのOS
    
    # 追加: backendフォルダを作業ディレクトリに指定
    defaults:
      run:
        working-directory: backend

    steps:
      # 1. GitHubリポジトリから最新のソースコードを取得します
      - name: Checkout
        uses: actions/checkout@v4

      # 2. GitHub Secretsに保存した認証情報を使ってAWSへログインします
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      # 3. Amazon ECRにログインしてイメージをプッシュできる状態にします
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # 4. Dockerイメージをビルドし、ECRにプッシュします(タグにはコミットIDを使用)
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }} # コミットごとに一意のIDをタグにします
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      # 5. 現在AWSに登録されているタスク定義(設定ファイル)をダウンロードします
      - name: Download task definition
        run: |
          aws ecs describe-task-definition --task-definition ${{ env.ECS_TASK_DEFINITION }} --query taskDefinition > task-definition.json

      # 6. 新しいタスクとして登録する際に邪魔になる不要な項目をJSONから削除します
      - name: Clean up task definition
        run: |
          jq 'del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy, .enableFaultInjection)' task-definition.json > temp.json && mv temp.json task-definition.json

      # 7. ダウンロードしたタスク定義の中の「イメージURI」を、今回作った新しいイメージに書き換えます
      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          # working-directoryがbackendなので、その中のjsonを参照するよう指定
          task-definition: backend/task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}
          environment-variables: |
            DB_USER=${{ secrets.DB_USER }}
            DB_PASS=${{ secrets.DB_PASS }}
            DB_HOST=${{ secrets.DB_HOST }}
            DB_NAME=${{ secrets.DB_NAME }}

      # 8. 書き換えた新しいタスク定義をAWSに登録し、ECSサービスを更新(デプロイ)します
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true # 正常に起動するまで完了を待ちます

修正ポイント

  • defaults: run: working-directory: backend: これを入れることで、ステップ4〜6のコマンドが自動的に backend フォルダ内で実行されます。
  • task-definition: backend/task-definition.json: ステップ7のアクションだけはリポジトリのルートを基準にファイルを探すことがあるため、明示的にパスを指定しています。

これでコメントを維持しつつ、新しい構成でデプロイが走るようになります。

4. Git 管理の移行

ディレクトリ階層の変更に伴い、Git リポジトリをルート階層で再初期化します。
my-fullstack-app(一番上の階層)の直下で実行してください。

git init
git branch -M main
git add .
git commit -m "chore: setup fullstack repository and CI/CD"
git push -u origin main -f

GitHubの画面をリロードし、以下を確認します。

  • ファイル構成: ルート階層に .github フォルダが表示されているか
  • Actions タブ: 画面上部の「Actions」タブを開き、左側に「Frontend Deploy」と「Deploy to Amazon ECS」の2つが表示され、自動で動作しているか

今回、.github/workflows フォルダの中に2つの YAML ファイルを配置したため、GitHub Actions が「どちらのデプロイも実行する必要がある」と判断して同時に動き出します。

  • Deploy to Amazon ECS(deploy.yml): Go のコードをビルドして Docker イメージを作成し、AWS ECS(バックエンド)を更新します。
  • Frontend Deploy(frontend-deploy.yml): React のコードをビルドして、AWS S3(フロントエンド)へアップロードし、CloudFront のキャッシュをクリアします。

両方のワークフローが**緑色のチェックマーク(Success)**になったら、以下の2つを確認してください。

  • フロントエンド確認: CloudFront の URL を開き、変更が反映されているか
  • バックエンド確認: ECS のパブリック IP などで API が正常に動作しているか

現在、フロントエンドとバックエンドはそれぞれ AWS 上に存在していますが、通信(API 連携)が未接続です。次のステップで CORS 対応を行い、フロントエンドから AWS 上のバックエンドを呼び出す設定をします。

AWS 上に Go + React + RDS を構築

1. バックエンド(Go)の修正とデプロイ

ディレクトリ構造の変更(モノレポ化)に伴い、ソースコードと設定を修正します。

① main.go の CORS 設定修正

フロントエンド(CloudFront)のドメインを許可します。末尾に / を入れないのがポイントです。

ソースコード
main.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time" // これが必要(undefined: time の解決)

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// User モデルの定義
type User struct {
	ID   uint   `gorm:"primaryKey" json:"id"`
	Name string `json:"name"`
}

func main() {
	// DSNの作成(undefined: dsn の解決)
	dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASS"),
		os.Getenv("DB_HOST"),
		os.Getenv("DB_NAME"),
	)

	var db *gorm.DB
	var err error

	// DB接続のリトライ処理
	for i := 0; i < 10; i++ {
		db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
		if err == nil {
			break
		}
		log.Printf("DB接続試行 %d回目: 失敗。再試行します...", i+1)
		time.Sleep(2 * time.Second)
	}

	if err != nil {
		log.Fatal("DBに接続できませんでした:", err)
	}

	log.Println("DB接続成功!")

	// 自動マイグレーション
	db.AutoMigrate(&User{})

	// ユーザー関連のAPI
	http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
		// CORS許可設定(特定のフロントエンドURLを許可するように更新)
		// あなたのCloudFrontのURL(https://xxx.cloudfront.net)に書き換えてください
		w.Header().Set("Access-Control-Allow-Origin", "https://xxxxxxxxxxxxxx.cloudfront.net")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

		// ブラウザの事前確認(OPTIONS)が来たら即座に返す
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		switch r.Method {
		case http.MethodGet:
			var users []User
			db.Find(&users)
			json.NewEncoder(w).Encode(users)
		case http.MethodPost:
			var user User
			if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
			db.Create(&user)
			json.NewEncoder(w).Encode(user)
		default:
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	})

	// ALBヘルスチェック用
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Server is running")
	})

	log.Println("Server started at :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

修正ポイント

CORS ヘッダー設定

  • Access-Control-Allow-Origin: * ではなく CloudFront ドメイン(例: https://xxxxxxxxx.cloudfront.net)を指定
    → セキュリティ向上とブラウザからの接続安定化
  • Authorization ヘッダー: 将来の認証機能追加に備えて許可ヘッダーに含める

モノレポ構成での GitHub Actions 対応

ディレクトリを backend/ 分離構成に変更した場合、GitHub Actions が「Dockerfile の位置」を特定できなくなります。deploy.yml を修正する必要があります。

ソースコード
deploy.yml
name: Deploy to Amazon ECS

# mainブランチにコードがpushされたときにこのワークフローを実行します
on:
  push:
    branches:
      - "main"
    # 追加: backendフォルダ内の変更があった時のみ実行
    paths:
      - 'backend/**'
      - '.github/workflows/deploy.yml'

# 各設定値を環境変数として定義(後で変更しやすくするため)
env:
  AWS_REGION: ap-northeast-1          # デプロイ先のリージョン(東京)
  ECR_REPOSITORY: my-go-app           # ECRのリポジトリ名
  ECS_SERVICE: my-go-service        # ECSのサービス名
  ECS_CLUSTER: my-go-cluster        # ECS of cluster名
  ECS_TASK_DEFINITION: my-go-task  # タスク定義のファミリー名
  CONTAINER_NAME: go-api-container # コンテナ名(タスク定義内の名前)

jobs:
  deploy:
    runs-on: ubuntu-latest # GitHub Actionsを実行するマシンのOS
    
    # backendフォルダを作業ディレクトリに指定
    defaults:
      run:
        working-directory: backend

    steps:
      # 1. GitHubリポジトリから最新のソースコードを取得します
      - name: Checkout
        uses: actions/checkout@v4

      # 2. GitHub Secretsに保存した認証情報を使ってAWSへログインします
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      # 3. Amazon ECRにログインしてイメージをプッシュできる状態にします
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # 4. Dockerイメージをビルドし、ECRにプッシュします
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          # コマンドの途中にコメントを挟まず、シンプルに実行します
          docker build -t $ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG .
          docker push $ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG" >> $GITHUB_OUTPUT

      # 5. 現在AWSに登録されているタスク定義をダウンロードします
      - name: Download task definition
        run: |
          aws ecs describe-task-definition --task-definition ${{ env.ECS_TASK_DEFINITION }} --query taskDefinition > task-definition.json

      # 6. JSONから不要な項目を削除します
      - name: Clean up task definition
        run: |
          jq 'del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy, .enableFaultInjection)' task-definition.json > temp.json && mv temp.json task-definition.json

      # 7. タスク定義のイメージURIを新しいものに書き換えます
      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          # すでにbackend/にいるので、ファイル名のみ指定します
          task-definition: backend/task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}
          environment-variables: |
            DB_USER=${{ secrets.DB_USER }}
            DB_PASS=${{ secrets.DB_PASS }}
            DB_HOST=${{ secrets.DB_HOST }}
            DB_NAME=${{ secrets.DB_NAME }}

      # 8. ECSサービスを更新(デプロイ)します
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

修正ポイント - deploy.yml

ステップ 4: Docker ビルド

  • run: | の直下のコメント行を削除(エラーの原因)
  • リポジトリ名の参照を ${{ env.ECR_REPOSITORY }} に統一して変数が確実に渡るよう改善

ステップ 7: Task Definition パス

  • task-definition: backend/task-definition.json と指定
  • もし File not found エラーが出た場合は、backend/ を削除して task-definition: task-definition.json を試してください

gitにプッシュします。
リポジトリのルートで実行してください:

git add .github/workflows/deploy.yml backend/main.go
git commit -m "fix: simplify docker build command in deploy.yml"
git push origin main

React側で、APIのURLを localhost から AWSの ALB(ロードバランサー)のURL に変更します。
my-react-app/src/App.tsx(またはAPIを呼んでいる箇所)を修正します。
※末尾の /users を忘れないようにしてください。

ソースコード
App.tsx
import { useEffect, useState } from 'react'
import axios from 'axios'

interface User {
  id: number
  name: string
}

function App() {
  const [users, setUsers] = useState<User[]>([])
  const [newName, setNewName] = useState('') // 入力フォーム用の状態

  // ★あなたのALBのDNS名をここで定義しておくと管理が楽です
  const API_URL = "http://あなたのALBのDNS名.ap-northeast-1.elb.amazonaws.com/users";

  useEffect(() => {
    fetchUsers()
  }, [])

  const fetchUsers = async () => {
    // localhostからALBのURLに変更
    const response = await axios.get(API_URL)
    setUsers(response.data)
  }

  // 名前を登録する関数
  const addUser = async () => {
    if (!newName) return
    try {
      // localhostからALBのURLに変更
      await axios.post(API_URL, { name: newName })
      setNewName('')    // 入力欄を空にする
      fetchUsers()      // 一覧を再取得
    } catch (error) {
      console.error("登録エラー:", error)
    }
  }

  return (
    <div style={{ padding: '40px', maxWidth: '600px', margin: '0 auto', fontFamily: 'sans-serif' }}>
      <h1>🚀 フルスタック連携アプリ</h1>
      
      {/* 登録フォーム */}
      <div style={{ marginBottom: '30px' }}>
        <input 
          value={newName} 
          onChange={(e) => setNewName(e.target.value)}
          placeholder="名前を入力"
          style={{ padding: '10px', fontSize: '16px' }}
        />
        <button onClick={addUser} style={{ padding: '10px 20px', marginLeft: '10px' }}>
          ユーザー追加
        </button>
      </div>

      <div style={{ background: '#f4f4f4', padding: '20px', borderRadius: '8px' }}>
        <h2>ユーザー一覧(RDS/AWS)</h2>
        <ul>
          {users.map(user => (
            <li key={user.id}>ID: {user.id} - <strong>{user.name}</strong></li>
          ))}
        </ul>
      </div>
    </div>
  )
}

export default App

GitHubへプッシュ
修正が終わったら、リポジトリのルートで以下のコマンドを実行してください。

git add my-react-app/src/App.tsx
git commit -m "feat: connect frontend to AWS ALB endpoint"
git push origin main

依存関係の同期(ローカル操作)
Dockerビルド時のライブラリ不足エラーが出た場合、ライブラリ情報を最新化します。

cd backend
go mod tidy
git add go.mod go.sum
git commit -m "chore: update dependencies"
  1. ブラウザでの最終疎通確認
    CloudFrontのURLにアクセス: ブラウザで自分のCloudFrontのURL(https://xxxx.cloudfront.net)を開きます。

データの確認: 画面に、AWS上のMySQL(RDS)から取得されたデータが表示されたら……フルスタックWebアプリの完成です!

CloudFront(https)からALB(http)を呼ぶとブラウザがブロックすることがあります。その場合は一旦「保護されていない通信を許可」するか、後ほどALBをHTTPS化する作業が必要です。

ブラウザのアドレスバーの「保護されていない通信」→「サイトの設定」をクリック。

「安全でないコンテンツ (Insecure content)」 を 「許可」 に設定。

リロード: ページを更新。

これにより、HTTPSの制約が一時的に緩和され、ALB(HTTP)との通信が可能になります。

「ブラウザの設定変更」なしで誰にでも見せられる状態(本番公開レベル)にするには、以下の ALBのHTTPS化 が必要です。

  1. SSL証明書の取得 (AWS Certificate Manager)
    AWSのACMというサービスで、無料で証明書を取得できます。ただし、これには「独自ドメイン」が必要です(Route 53などで取得したもの)。

  2. ALBにHTTPSリスナーを追加
    ALBの設定で、ポート80(HTTP)だけでなくポート443(HTTPS)も受け付けるようにし、そこに取得した証明書を紐付けます。

  3. App.tsx を HTTPS に書き換える
    API_URL を https://... に書き換えてプッシュすれば、ブラウザにブロックされることなく、世界中の誰でもあなたのアプリを使えるようになります。

AWSフルスタック開発:環境変数による開発・本番環境の動的切り替え

Viteの環境変数機能を利用して、ビルドモードに応じてAPIのURLを切り替えます。

  1. フロントエンド(React/Vite)の設定
    ① 設定ファイルの作成
    my-react-app フォルダ直下に以下の2ファイルを作成。

.env.development (ローカル開発用)

VITE_API_URL=http://localhost:8080/users

.env.production (AWS本番用)

VITE_API_URL=http://[あなたのALBのDNS名]/users

② ソースコードの修正 (App.tsx)
URLを直接書く(ハードコード)のをやめ、環境変数を参照するように変更。

App.tsx
// 直接URLを書くのをやめる
// const API_URL = "http://localhost:8080/users"; 

// 環境変数から読み込む(Viteが起動モードに合わせて自動選択する)
const API_URL = import.meta.env.VITE_API_URL;

useEffect(() => {
  fetchUsers()
}, [])

const fetchUsers = async () => {
    const response = await axios.get(API_URL);
    setUsers(response.data);
};
  1. バックエンド(Go)の設定
    CORS(アクセス許可)の設定を、環境変数によって動的に変更できるようにします。

① ソースコードの修正 (main.go)
os.Getenv を使って、許可するフロントエンドのURLを取得します。

main.go
// インポートに追加
import "os"

// ユーザー関連のAPI
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
    // 環境変数 "FRONTEND_URL" を取得
    frontendURL := os.Getenv("FRONTEND_URL")
    
    // ローカル実行時など、環境変数がなければ localhost をデフォルトにする
    if frontendURL == "" {
        frontendURL = "http://localhost:5173"
    }

    // 動的にOriginを許可
    w.Header().Set("Access-Control-Allow-Origin", frontendURL)
    w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
    w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
    
    // ...以降、OPTIONS対応やCRUD処理
})
  1. CI/CDパイプライン(GitHub Actions)の設定
    デプロイ時に、AWS側のコンテナへ環境変数を注入します。

deploy.yml の修正
amazon-ecs-render-task-definition ステップの environment-variables セクションに FRONTEND_URL を追加します。

- name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: backend/task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}
          # ここでコンテナにURLを教える
          environment-variables: |
            FRONTEND_URL=https://xxxxxxxxx.cloudfront.net
            DB_USER=${{ secrets.DB_USER }}
            DB_PASS=${{ secrets.DB_PASS }}
            DB_HOST=${{ secrets.DB_HOST }}
            DB_NAME=${{ secrets.DB_NAME }}
  1. 反映コマンドと動作確認
# 1. 変更をステージング(新規の.envファイルも含める)
git add .

# 2. コミット
git commit -m "feat: implement environment variable switching"

# 3. プッシュ(GitHub Actionsが走る)
git push origin main

動作確認のポイント

ローカル環境

  • npm run devgo run main.go を起動
  • ブラウザで localhost:5173 にアクセスして正常に動くか確認

AWS 環境

  • CloudFront の URL を開く
  • ブラウザのデベロッパーツール(F12)でネットワークタブを確認
  • API リクエスト(users)が ALB の URL に向かっていることを確認

メリット

  • 安全性: 本番用 URL を誤ってローカルで叩くミスを防止
  • 効率性: 設定後は git push するだけで、環境に合わせた自動デプロイが完了

Go + React + AWS:削除機能の実装とバリデーション強化

  1. バックエンド(Go)の修正
    HTTPの DELETE メソッドに対応し、URLパラメータからIDを受け取ってDBレコードを削除するロジックを実装します。

① main.go の修正
CORSの設定更新と、MethodDelete のケース追加を行います。

main.go
// 1. CORS設定で DELETE メソッドを許可する
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")

// 2. 処理分岐(switch)に DELETE を追加
switch r.Method {
case http.MethodPost:
    // ...既存のデコード処理...
    // バリデーション追加
    if user.Name == "" {
        http.Error(w, "Name is required", http.StatusBadRequest)
        return
    }
    db.Create(&user)

case http.MethodDelete:
    // URLクエリパラメータから ?id= を取得
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "ID is required", http.StatusBadRequest)
        return
    }
    // GORMで物理削除を実行
    if err := db.Delete(&User{}, id).Error; err != nil {
        http.Error(w, "削除失敗", http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusNoContent) // 成功時は204(内容なし)を返す
  1. フロントエンド(React)の修正
    各ユーザーに削除ボタンを紐付け、APIへリクエストを飛ばす処理を追加します。

① App.tsx の修正
UIの改善と、deleteUser 関数の実装を行います。

App.tsx
// 削除実行関数
const deleteUser = async (id: number) => {
  // ユーザーへの確認ダイアログ
  if (!window.confirm("このユーザーを削除してもよろしいですか?")) return;

  try {
    // URL末尾にIDを付与して送信
    await axios.delete(`${API_URL}?id=${id}`);
    fetchUsers(); // 削除後にリストを最新化
  } catch (error) {
    console.error("削除失敗:", error);
    alert("削除に失敗しました");
  }
};

// ...UI部分
<h2>ユーザー一覧(RDS/Local DB)</h2>
        <ul style={{ padding: 0, listStyle: 'none' }}>
          {users.map(user => (
            <li 
              key={user.id} 
              style={{ 
                marginBottom: '10px', 
                background: 'white', 
                padding: '10px', 
                borderRadius: '4px',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
                boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
              }}
            >
              <span>ID: {user.id} - <strong>{user.name}</strong></span>
              {/* ★ 削除ボタン */}
              <button 
                onClick={() => deleteUser(user.id)}
                style={{ 
                  background: '#ff4d4f', 
                  color: 'white', 
                  border: 'none', 
                  padding: '5px 10px', 
                  borderRadius: '4px', 
                  cursor: 'pointer' 
                }}
              >
                削除
              </button>
            </li>
          ))}
  1. ローカル環境での検証コマンド
    Docker環境を更新し、正しく動作するか確認します。
# バックエンドの再ビルドと起動
docker-compose up --build

# フロントエンドの起動
cd my-react-app
npm run dev
  1. AWSへのデプロイ(GitHub Actions)
    修正が確認できたら、一気にAWS環境へ反映させます。
# すべての変更を記録
git add .

# メッセージを付けてコミット
git commit -m "feat: implement delete user and add validation"

# GitHubへ送信
git push origin main

デプロイ後の確認項目

CI/CD の動作

  • GitHub Actions: deploy-backenddeploy-frontend が共に「Success」

機能動作の確認

  • CloudFront の URL で「削除ボタン」が表示されるか
  • 削除ボタンを押してデータが実際に削除されるか
  • 空文字で「追加」を押した際、アラートが表示されるか

完成

✅ 「ユーザーを登録 → 一覧表示 → 不要なデータ削除」という基本サイクルが完成しました!

Tailwind CSS v4導入とモダンなUI/UXへの刷新

最新のTailwind CSS v4を導入し、ボタンの連打防止(ローディング制御)などの実務的な処理を追加します。

1. Tailwind CSS v4 のセットアップ

v4系では設定方法が大幅に簡略化されました。

① 必要なパッケージのインストール

npm install -D tailwindcss @tailwindcss/postcss postcss

② PostCSSの設定 (postcss.config.js)
新しいプラグイン名を指定します。

postcss.config.js
export default {
  plugins: {
    '@tailwindcss/postcss': {},
    autoprefixer: {},
  },
}

③ CSSの読み込み (index.css)
以前のバージョンのスタイルをすべて削除し、最新の1行インポートに変更します。

index.css
@import "tailwindcss";
  1. フロントエンドの改善(App.tsx)
    インラインスタイルを排除し、Tailwindのユーティリティクラスで構築。さらに通信中の「二重送信」を防ぐロジックを組み込みました。
ソースコード
App.tsx
import { useEffect, useState } from 'react'
import axios from 'axios'

interface User {
  id: number
  name: string
}

function App() {
  const [users, setUsers] = useState<User[]>([])
  const [newName, setNewName] = useState('') // 入力フォーム用の状態
  const [loading, setLoading] = useState(false) // ★ローディング状態を追加

  // ALBのDNS名
  const API_URL = import.meta.env.VITE_API_URL;

  useEffect(() => {
    fetchUsers()
  }, [])

  const fetchUsers = async () => {
    try {
      const response = await axios.get(API_URL)
      setUsers(response.data || []) // データが空の場合の考慮
    } catch (error) {
      console.error("データ取得エラー:", error)
    }
  }

  // 名前を登録する関数
  const addUser = async () => {
    // バリデーション:空文字やスペースのみを除外
    if (!newName.trim()) {
      alert("名前を入力してください")
      return
    }

    setLoading(true) // ★登録開始時にローディングをtrueにする
    try {
      await axios.post(API_URL, { name: newName })
      setNewName('')    // 入力欄を空にする
      await fetchUsers()      // 一覧を再取得
    } catch (error) {
      console.error("登録エラー:", error)
      alert("登録に失敗しました")
    } finally {
      setLoading(false) // ★成否に関わらず終了時にfalseにする
    }
  }

  // ★ ユーザーを削除する関数
  const deleteUser = async (id: number) => {
    if (!window.confirm("このユーザーを削除してもよろしいですか?")) return

    try {
      // クエリパラメータ (?id=数値) でIDを送信
      await axios.delete(`${API_URL}?id=${id}`)
      fetchUsers() // 一覧を再取得
    } catch (error) {
      console.error("削除エラー:", error)
      alert("削除に失敗しました")
    }
  }

  return (
    // Tailwindクラスで全体のレイアウトを調整
    <div className="min-h-screen bg-gray-100 py-10 px-4 font-sans text-gray-900">
      <div className="max-w-xl mx-auto bg-white rounded-xl shadow-lg p-8">
        <h1 className="text-3xl font-bold mb-8 text-center">🚀 フルスタック連携アプリ</h1>
        
        {/* 登録フォーム */}
        <div className="flex gap-3 mb-10">
          <input 
            className="flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-base"
            value={newName} 
            onChange={(e) => setNewName(e.target.value)}
            placeholder="名前を入力"
          />
          <button 
            onClick={addUser} 
            disabled={loading}
            className="bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white font-bold py-2 px-6 rounded-lg transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed shadow-md"
          >
            {loading ? '追加中...' : 'ユーザー追加'}
          </button>
        </div>

        <div className="bg-gray-50 p-6 rounded-xl">
          <h2 className="text-xl font-semibold mb-4 border-b pb-2 text-gray-700">ユーザー一覧(RDS/Local DB)</h2>
          <ul className="space-y-3">
            {users.map(user => (
              <li 
                key={user.id} 
                className="flex justify-between items-center bg-white p-4 rounded-lg shadow-sm border border-gray-100 hover:shadow-md transition-shadow"
              >
                <span className="text-lg">ID: <span className="text-gray-500 text-sm">{user.id}</span> - <strong className="text-gray-800">{user.name}</strong></span>
                {/* ★ 削除ボタン */}
                <button 
                  onClick={() => deleteUser(user.id)}
                  className="bg-red-500 hover:bg-red-600 active:bg-red-700 text-white text-sm font-bold py-1.5 px-4 rounded-md transition duration-200 shadow-sm"
                >
                  削除
                </button>
              </li>
            ))}
          </ul>
          {users.length === 0 && (
            <p className="text-center text-gray-400 py-4">登録されているユーザーはいません</p>
          )}
        </div>
      </div>
    </div>
  )
}

export default App

3. 実装の成果(UX向上ポイント)

機能 改善内容 メリット
ローディング制御 通信中にボタンを disabled(無効化)にする 誤った連打による重複登録を物理的に防ぐ
フィードバック ボタンテキストを「追加中...」に動的変更 ユーザーが「今処理中である」ことを認識できる
デザイン Tailwind でカードレイアウト、シャドウ、ホバーエフェクト どこが操作可能で、何がまとまりかが直感的に伝わる
レスポンシブ Tailwind のグリッド/フレックスシステム スマホとPC両方で最適なレイアウトを維持
  1. AWSへの反映
    Viteはビルド時にTailwindを解析して最適なCSSを書き出してくれるため、特別な設定変更なしでデプロイ可能です。
git add .
git commit -m "style: upgrade UI with Tailwind CSS v4 and add loading state"
git push origin main

完成した画面です!

91works Tech Blog

Discussion