16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[完全版] もうデプロイで迷わない!Go製バックエンドアプリのAWSデプロイ実践手順全集

Last updated at Posted at 2025-12-17

はじめに

「ローカルでゴリゴリ開発してるけど、毎回作って終わりになってしまっている...」
「実はデプロイ方法についてはあんまりわかっていない...」
「いろいろ方法あるのはわかってるけど慣れ親しんだ方法ばっかり使っている...」
「AWSでデプロイすると金かかるしなあ」

上記は自分がこれまでずっと抱えていた課題感です。

本記事では、過去の自分と同じ課題感を持つデプロイ迷子のエンジニアに向けてGo製バックエンドアプリのAWSでの基本的なデプロイ方法の手順をすべて解説します。

「とりあえずこの記事を見れば、自分の要件にあったデプロイ方法がわかって、すぐに実行できる」状態にすることが本記事のゴールです。

Goを知らない方でも読める内容になっておりますのでぜひご覧ください!

こんな人に読んでほしい

  • 上記のような課題感を持つ人
  • シンプルにAWSの勉強をしたい人
  • 「バックエンドのAWSデプロイなんて余裕やけど一応確認しておこうかな」な人
  • 「最近やってなくて忘れ気味やし脳内シミュレーションだけでもしておくかあ」な人

本記事で解説する手法

本記事では「低レベルなインフラ管理」から「モダンなサーバーレス/PaaS」まで7種類の方法を扱います。
※正確には他にも方法はありますが、これらの方法と比べて利用頻度はかなり低いです。

  1. 仮想サーバー (VM) ベースの手動デプロイ
    1-1. EC2に実行ファイルを直接配置
    1-2. Docker on EC2
  2. コンテナオーケストレーション (ECS) によるデプロイ
    2-1. ECS on Fargate
    2-2. ECS on EC2
  3. サーバーレスファンクション (Lambda) によるデプロイ
    3-1. API Gateway and Lambda
    3-2. Lambda Web Adapter
  4. フルマネージド PaaS ライクなデプロイ
    4-1. App Runner

本記事では書かないこと(参考リンクなどは一部載せています)

  • AWSアカウントの作成方法
  • IAMユーザやVPC、キーペアなどの作成方法
  • Go製サンプルアプリの詳細な解説
  • ガチガチのセキュリティを意識した構成

今回用いるサンプルアプリ

本記事ではAWSへのデプロイに主眼を置くため、「Hello World」を返すだけのアプリを用います。以下ソースコードとローカル環境で起動し、ブラウザからアクセスしたときの結果です。

main.go
package main

import (
	"fmt"
	"html/template"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	http.HandleFunc("/", helloHandler)

	fmt.Printf("Server is running on :%s\n", port)
	http.ListenAndServe(":"+port, nil)
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("templates/index.html")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	tmpl.Execute(w, nil)
}

image.png

1. 仮想サーバー (VM) ベースの手動デプロイ

最も基本的で、インフラの自由度が高い方法です。
以下の2種類の方法をご紹介します。

  • 1-1. EC2に実行ファイルを直接配置
  • 1-2. EC2にDockerコンテナを配置

1-1. EC2に実行ファイルを直接配置

これが一番どシンプルな方法です。
Linuxサーバに実行バイナリをSCPで送って実行するだけです。
以下の手順で進めます。

  1. EC2インスタンスを作成
  2. 実行ファイルを作成
  3. 実行ファイルをEC2に転送
  4. アプリ起動
  5. 動作確認
  6. systemdで自動起動

1-1-1. EC2インスタンスを作成

アプリケーションおよびOSイメージ
今回は、マシンイメージとしてAmazon Linux、インスタンスタイプはt3.microを使用します。(ご自身のアプリに応じて変更してください。)

項目 設定値
マシンイメージ Amazon Linux
インスタンスタイプ t3.micro
キーペア ご自身のキーペア(なければ作成)
VPC ご自身のVPC
サブネット ご自身のパブリックサブネット
パブリックIPの自動割り当て 有効化
ストレージ 1 × 8 GiB gp3

image.png

セキュリティグループ
セキュリティグループはすべてのIPアドレスからのHTTP(ポート80番)でアクセスを許可します。

項目 設定値
タイプ HTTP
プロトコル TCP
ポート範囲 80
ソース Anywhere (0.0.0.0/0, ::/0)

1-1-2. 実行ファイルを作成

実行ファイルとは、コンピュータが直接読み込んで走らせることができる形に変換されたプログラムのことです。
実行ファイルの作成には以下のコマンドを用います。

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app main.go

実行するとappという実行ファイルが生成されます。
これをEC2に転送し、EC2で実行ファイルを実行します。

image.png

CGO_ENABLED
CGOとはGoからC言語のライブラリを呼び出す仕組みのことで、有効のままビルドするとOSにglibcなどのCライブラリが必要になる場合があります。しかし、軽量なランタイムを使用している場合、Cライブラリが存在せずにエラーになることがあります。なのでGoランタイムだけで動く静的な実行ファイルを確実に生成するためにCGO_ENABLED=0とします。

GOOS
GOOSはOSを指定する値です。
Goは1つの環境で他のOS用のバイナリを作る「クロスコンパイル」が可能です。
よく使うのは以下3種類です。
今回はAmazon Linuxを用いるためlinuxとします。

GOOS 意味
linux Linux
darwin macOS
windows Windows

GOARCH
GOARCHはCPUのアーキテクチャを決める値です。
以下の2種類がありますが、今回用いるAmazon Linuxの多くはamd64なので今回はamd64とします。

GOARCH 意味
amd64 x86_64(Intel/AMD)
arm64 Graviton(ARM CPU)

1-1-3. 実行ファイルをEC2に転送

先ほど作成した実行ファイルをEC2に転送します。
今回はシンプルなアプリを用いているので転送にはscpという転送用のプロトコルを用います。

scp以外にもGitHubにプッシュしてEC2でプルしたり、S3に格納してEC2からダウンロードしたりする方法を用いることもあります。

以下のコマンドで実行ファイルであるappと実際に表示するテンプレートが入ったtemplatesをEC2内の/home/ec2-user/配下に転送します。

scp -i k-tsurumaki-key.pem -r app templates ec2-user@<EC2のIPアドレス>:/home/ec2-user/

-i k-tsurumaki-key.pemにはEC2のキーペアを指定します。

image.png

これで転送完了です。

1-1-4. アプリ起動

転送した実行ファイルを実行してアプリを起動します。

1-1-4-1. EC2への接続

そのためにはEC2に接続する必要があります。
接続方法として代表的なのが以下の3つです。

接続方法 EC2 Instance Connect SSM Session Manager SSHクライアント(秘密鍵)
概要 一時的な公開鍵を送ってSSH接続するAWS公式方式 SSM Agent経由でブラウザ/CLI接続 ローカルPCからSSHで直接接続
接続方法 AWSコンソール / AWS CLI AWSコンソール / AWS CLI ssh -i key.pem ec2-user@ip
鍵管理 秘密鍵不要 秘密鍵不要 秘密鍵必要
パブリックIP 必要(または EC2 Instance Connect Endpoint) 不要(完全プライベート可) 必要(またはVPN/DirectConnect)
EC2側の要件 EC2 Instance Connect有効化 SSM Agent+IAMロール SSHデーモン+鍵配置
ログ CloudTrail CloudTrail + SSMログ(詳細) 基本なし(自前で準備)
セキュリティ ・鍵レスだが22番ポート開放が必要
・セキュリティは普通
・最も安全
・鍵不要
・22番ポート開放不要
・鍵漏洩リスク
・22番ポート開放が必要
向いている用途 簡易的な接続 企業/本番環境でのセキュア運用 開発者が自由にSSHしたい時

今回は一番セキュアなセッションマネージャを用いて接続します。
しかし、現状セッションマネージャで接続しようとしても以下のように接続ボタンがグレースケールとなっており接続できません。

image.png

セッションマネージャを用いるためにはEC2にAmazonSSMManagedInstanceCore(またはこれと同等のロール)を付与する必要があります。対象のEC2を選択し、「アクション」→「セキュリティ」→「IAMロールの変更」からロールを新規作成します。

image.png

その際、AmazonSSMManagedInstanceCoreを許可ポリシーとして選択します。

image.png
image.png

しばらくすると接続ボタンがアクティブになり、セッションマネージャでEC2に接続できるようになります。

image.png

接続ボタンがアクティブになるまでにはしばらく時間がかかります。

接続ボタンを押して以下のような画面になれば接続成功です。

image.png

1-1-4-2. 起動

セッションマネージャでログインした場合、デフォルトのユーザはssm-userとなります。
まず以下のコマンドでec2-userにユーザを変更し、scpで転送した実行ファイルがある/home/ec2-userに移動します。

sudo su ec2-user
cd /home/ec2-user

lsコマンドで確認してみるとたしかに実行ファイルapptemplatesがあることがわかります。

image.png

以下のコマンドで実行ファイルを実行します。

export PORT=80
sudo ./app

今回は80番ポートでデプロイしています。
80番ポートで実行する場合はsudo ./appとする必要があります。
(8080番ポートなどを用いる場合は./appで問題ありません。)

1-1-5. 動作確認

EC2のパブリックIPアドレスにアクセスしてみます。
以下のようにテンプレートの内容が表示されると正しく動作しています。

image.png

1-1-6. systemdで自動起動

4.まででデプロイ自体は完了なのですが、この状態だとターミナルを切断した瞬間にアプリが停止したり、サーバーが再起動したときにアプリが起動しなかったりします。

これを解決し、Goアプリを「サーバーのシステムサービス」として永続的に稼働させるために、Linux標準のサービス管理ツールである systemd のユニットファイルを作成します。

/etc/systemd/system/go-website.service
[Unit]
Description=Go Website
After=network.target

[Service]
export PORT=80
# 特権ポート80番で開くためのCAP_NET_BIND_SERVICE権限を付与
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
User=ec2-user
WorkingDirectory=/home/ec2-user
Environment=PORT=80
ExecStart=/home/ec2-user/app
Restart=always

[Install]
WantedBy=multi-user.target

作成したら、systemdに設定を読み込ませてサービスを有効化・起動します。

sudo systemctl daemon-reload
sudo systemctl enable go-website
sudo systemctl start go-website
sudo systemctl status go-website

セッションマネージャを再接続して確認してみると自動で起動していることがわかります。

image.png

再度systemdの設定を解除するには以下のコマンドを実行します。

# サービスを停止
sudo systemctl stop go-website.service

# OS起動時の自動起動設定を解除
sudo systemctl disable go-website.service

(うん、起動するだけなのでシンプルですが、、、)

1-2. Docker on EC2

EC2にDockerをインストールし、コンテナイメージをプルして実行する方法です。
コンテナを使用する方法の中でもっとも原始的な方法です。
以下の手順で進めます。
※EC2は「1-1-1. EC2インスタンスを作成」で作成したものをそのまま用います。

  1. Go用のマルチステージビルド Dockerfile の作成(軽量化のコツ)
  2. ECRへのプッシュ
  3. EC2で docker login & docker run
  4. 動作確認

1-2-1. Go用のマルチステージビルド Dockerfile の作成

以下のようなDockerfileを用意します。
このDockerfileは、以下の2つのステージを分けて実行しています。

  • Build stage (golang:1.25 AS builder)
    • ビルドに必要な巨大なGoコンパイラ環境でバイナリ(実行ファイル)を作成
  • Runtime stage (alpine:3.20)
    • 実行環境には、Goコンパイラを含まない非常に小さなAlpine Linuxベースイメージを採用し、ビルドステージで作成した実行ファイルのみをコピーして実行

これにより、最終的なDockerイメージは、開発・ビルドツールを含まない必要最小限のサイズになり、セキュリティの向上と起動の高速化が実現されます。

# ================================
# 1. Build stage
# ================================
FROM golang:1.25 AS builder

WORKDIR /app

# Go module filesを先にコピー(キャッシュ効く)
COPY go.mod ./
RUN go env -w GOPROXY=https://proxy.golang.org && go mod download

# 残りのソースコードコピー
COPY . .

# Linux用の静的バイナリをビルド
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server main.go

# ================================
# 2. Runtime stage
# ================================
FROM alpine:3.20

WORKDIR /app

# ビルドしたバイナリをコピー
COPY --from=builder /app/server .

# templates ディレクトリをコピー
COPY --from=builder /app/templates ./templates

# ポート公開(デフォルト8080)
EXPOSE 8080

# 本番実行コマンド
CMD ["./server"]

このDockerfileを以下のコマンドでビルドします。
これによりDockerイメージが生成されます。

# ビルドしてイメージを作成
docker build -t go-website .

image.png

1-2-2. ECRへのプッシュ

続いて、作成したイメージをECRにプッシュします。
ECRはdockerイメージを格納するAWSサービスです。

そのためにまず、先ほど作成したイメージのタグをECR用に付け替えます。

# 作成したイメージのタグをECR用に付け替え
docker tag go-website:latest <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/k-tsurumaki-backend/go-website:latest

続いてAWSにログインし、ECRにリポジトリを作成します。
そのためにAWS CLIのインストールと初期設定が必要です。
以下のサイトが参考になります。

↓AWS CLIのインストール

↓AWS CLIの初期設定(アクセスキーを発行してAWS CLIと紐づけ)

上記の設定が終わったら以下のコマンドでログイン、ECRリポジトリを作成します。
ECRリポジトリはAWSコンソールから作成するでもOKです。

# AWS ECR へ Docker CLI でログイン
aws ecr get-login-password --region ap-northeast-1 --profile <プロファイル名> --no-verify-ssl | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com

# ECR リポジトリを作成(まだ無い場合だけ実行、ブラウザで作成してもOK)
aws ecr create-repository --repository-name k-tsurumaki-backend/go-website --region ap-northeast-1 --profile <プロファイル名> --no-verify-ssl

以下のコマンドでAWS CLIの設定済みプロファイル一覧を取得できます。

aws configure list-profiles

最後に作成したリポジトリめがけてイメージをプッシュします。

# ECR にプッシュ
docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/k-tsurumaki-backend/go-website:latest

1-2-3. EC2で docker login & docker run

続いてEC2側にもDocker、AWS CLIをインストールし、ECRからイメージをPull
します。

1-2-3-1. Dockerのインストール

以下は公式サイトのインストール手順です。
コピペしてポチポチしていけばインストールできます。

# Docker インストール
sudo dnf -y update
sudo dnf install docker
docker -v
 
# Docker 起動
sudo systemctl start docker.service
sudo systemctl status docker.service
 
# Docker 自動起動設定
sudo systemctl is-enabled docker.service
sudo systemctl enable docker.service
sudo systemctl is-enabled docker.service
 
# Docker グループにユーザを追加
sudo usermod -aG docker ec2-user
 
# Docker Compose のインストール
sudo curl -SL https://github.com/docker/compose/releases/download/v2.40.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
ls -l /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
ls -l /usr/local/bin/docker-compose
docker-compose -v
 
# sudo なしで Docker を使えるように設定
sudo gpasswd -a $(whoami) docker
sudo chgrp docker /var/run/docker.sock
 
# Docker 動作テスト
docker info
docker run hello-world
 
# 掃除
docker stop $(docker ps -q)
docker container prune
docker image prune -a
docker volume prune -a

1-2-3-2. AWS CLIのインストール

続いてこちらも公式サイトAWS CLIのインストールです。
同様にポチポチで行けます。

# ユーザ変更&ホームディレクトリへ移動
sudo su ec2-user
cd ~

# 必要なパッケージをインストール (unzipとcurlは通常デフォルトで入っていますが、念のため)
sudo dnf install -y unzip curl
 
# AWS CLI v2 バイナリをダウンロード
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
 
# 解凍
unzip awscliv2.zip
 
# インストール
sudo ./aws/install
 
# バージョン確認
aws --version

1-2-3-3. ログイン&プル

以下のコマンドでECRにログインし、先ほどローカルからPushしたイメージをPullします。

aws ecr get-login-password --region ap-northeast-1 --no-verify-ssl | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com

 docker pull <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/k-tsurumaki-backend/go-website:latest

Pullする際のURIは以下からコピーできます。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_3933650_461aa4ed-bb95-4ebe-91bc-d62a1eb13801.jpg

1-2-3-4. コンテナ起動

以下のコマンドで起動できます。
今回はコンテナ内の8080番ポートを公開し、それをホスト(EC2)の80番ポートに紐づけています。

 docker run -d --name go-website -p 80:8080 <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com

1-2-4. 動作確認

EC2のパブリックIPアドレスにアクセスし、以下のようなページが表示されれば完了です。

image.png

(各種ツールのインストールがとても面倒くさい、、、)

2. コンテナオーケストレーション (ECS) によるデプロイ

本番運用のデファクトスタンダードともいえるECSを用いる方法です。
以下の2種類の方法をご紹介します。
※Dockerイメージは「1-2-1. Go用のマルチステージビルド Dockerfile の作成」で作成したものと同じもの(8080番ポートを公開)を使用します。

2-1. ECS on Fargate

サーバ管理が特に不要で、コンテナを定義して投げるだけの方法です。
現状のAWSだとまず最初に検討すべき王道ともいえる方法です。
以下の手順で進めます。

  1. ECSクラスターの作成
  2. タスク定義の作成
  3. サービス作成
  4. 動作確認

2-1-1. ECSクラスターの作成

クラスターとはタスク(コンテナの実行単位)が稼働する場所であり、ECSを使うときの実行環境をまとめた箱のようなものです。
「ECS」→「クラスターの作成」からクラスターを作成します。

インフラストラクチャ
コンピューティングリソースの取得方法を選択します。

項目 設定値
コンピューティングキャパシティの取得方法の選択 Fargateのみ

ここでは、Fargateのみを選択します。

image.png

これだけでクラスター作成は完了です。

2-1-2. タスク定義の作成

続いてタスク定義を作成します。
タスクとは1つ以上のDockerコンテナを一括で実行するためのまとまりのことです。
そのタスクの設定を記述するものがタスク定義です。
具体的には、CPUやポート番号、環境変数、ログ設定、IAMロール設定などを記述します。

インフラストラクチャの要件
インフラの設定を行います。

項目 設定値
起動タイプ AWS Fargate
オペレーティングシステム / アーキテクチャ Linux / X86_64
CPU 1 vCPU
メモリ 3 GB
タスク実行ロール ecsTaskExecutionRole

起動タイプにはAWS Fargateを選択します。
また、タスクを実行するためにタスク実行ロールにはecsTaskExecutionRoleが必要です。

image.png

コンテナ
起動するコンテナの設定を行います。

項目 設定値
イメージ URI 「1-2-1. Go用のマルチステージビルド Dockerfile の作成」で作成したイメージ
ポートマッピング コンテナポート: 8080 / プロトコル: TCP / アプリケーションプロトコル: HTTP
CPU 1 vCPU
メモリ(ハード制限) 3 GB
メモリ(ソフト制限) 1 GB

イメージURIには起動するDockerイメージを指定します。
コンテナポートはDockerコンテナが公開しているポート番号を指定します。

image.png

これでタスク定義の作成は完了です。

2-1-3. サービスの作成

最後にサービスを作成します。
サービスとはタスクを指定した数になるように維持する仕組みです。

サービスの詳細
使用するタスク定義を指定します。

項目 設定値
タスク定義ファミリー 「2-1-2. タスク定義の作成」で作成したタスク定義
タスク定義のリビジョン デフォルト値

タスク定義のリビジョンとはタスク定義のバージョンのことです。
タスク定義は一度登録すると不変であり、内容を更新するたびに新しいリビジョンとして保存される仕組みになっています。タスク定義ファミリーを選択するとデフォルトで最新のリビジョンが入力されるようになっていますので理由がない限り変更は必要ありません。

コンピューティング設定
コンテナのタイプやスケールの設定を行います。

項目 設定値
コンピューティングオプション キャパシティープロバイダー戦略
キャパシティープロバイダー戦略 カスタムを使用(アドバンスト)
キャパシティプロバイダー Fargate
ベース 0
ウェイト 1
プラットフォームバージョン LATEST

コンピューティングオプションはキャパシティープロバイダー戦略を選択します。
キャパシティープロバイダー戦略とはECSに自動でタスクをどこに配置するかを選ばせる仕組みです。

Fargateの場合、以下の2つを選択することができます。

名称 特徴 使う場面
FARGATE 通常 安定稼働
FARGATE_SPOT 70%〜90% 料金割引、いつ止まってもOK バッチ処理 / 可用性要らないワーカー

今回はとりあえずFARGATEを選択します。
ベース値は0、ウェイト値は1とします。

ベース値とは最初にこのキャパシティープロバイダーでいくつのタスクを起動するかを決める値です。いわば最低保証するタスク数です。今回はキャパシティープロバイダーを1つしか設定していないので0も1も同じ意味です。

ウェイト値とはベースを置いた後、残りのタスクをどのようにキャパシティープロバイダーに割り振るかを決める重みです。今回はキャパシティープロバイダーを1つしか設定していないのでなんでもOKです。

以下のようにキャパシティープロバイダーを設定している場合は、残りのタスクがFARGATE : SPOT = 1 : 3で配置されます。

FARGATE:       weight 1
FARGATE_SPOT:  weight 3

image.png

デプロイ設定
タスクの数やタスク定義の変更をどう反映させるかを決める設定を行います。

項目 設定値
スケジューリング戦略 レプリカ
必要なタスク 1
アベイラビリティーゾーンの再調整 有効化
ヘルスチェックの猶予期間 0

スケジューリング戦略はレプリカを選択します。
(というかデーモンはFargate未対応なのでそもそも選択できません。)

必要なタスクはそのまま起動するタスクの数を指します。
ここで設定した数を維持するにECSが自動復旧させたりします。
今回は1とします。

image.png

ネットワーキング
VPCやサブネット、セキュリティグループなどのネットワークの設定を行います。

項目 設定値
VPC ご自身のVPC
サブネット ご自身のパブリックサブネット
パブリックIP オン

重要なのはサブネットの選択です。
どのように公開したいかによって選択が変わります。
よくあるパターンとしては以下の3つがあります。

構成 セキュリティ コスト 運用 どんな時に最適?
ALB + ECS(プライベート) 高い NAT料金高い 通常 一般的な本番構成
ALB + ECS(パブリック) ←★これを採用 中程度 安い 簡単 NAT料金を節約したい小規模・検証環境
ECS(パブリック・ALBなし) 低い 最安 最も簡易 固定IPで直接公開する小規模API

(NATは割と使用料が高いので、)今回はALB+ECS(パブリック)を採用します。
サブネットにはパブリックサブネットを2つ以上選択します。

セキュリティグループ
セキュリティグループも重要です。
今回の構成の場合ECSにアクセスしてくるのはALBになりますのでALBのセキュリティグループをソースとするインバウンドを許可する必要があります。

セキュリティグループ 方向 ポート/プロトコル ソース/宛先 備考
ALB-SG Inbound 80 / TCP 0.0.0.0/0 インターネットからのアクセスを許可
ALB-SG Outbound All 0.0.0.0/0 タスクや外部アクセス用
TASK-SG Inbound 8080 / TCP ALB-SG ALB からのアプリアクセスのみ許可
TASK-SG Outbound All 0.0.0.0/0 外部APIやECRアクセス用

パブリックIPはオンにします。
こうしないとECRからイメージを取得できずにエラーとなります。

image.png

ロードバランシング
ロードバランシングのところでALBの作成も一緒にできちゃいます。

項目 設定値
VPC ご自身のVPC
サブネット ご自身のパブリックサブネット
ロードバランサーの種類 Application Load Balancer
Application Load Balancer 新しいロードバランサーを作成
リスナー 新しいリスナーを作成(ポート: 80 / プロトコル: HTTP)
ターゲットグループ 新しいターゲットグループを作成(プロトコル: HTTP / ポート: 8080)

ロードバランサーの種類はALBを選択します。
新しいロードバランサーの作成からALBを新規作成します。

リスナーには実際に公開したいポート番号を指定し、ターゲットグループにはアプリが公開するポートを指定します。

image.png

これで作成します。
タスクが起動し、ALBのリソースマップが正常になっていれば正しく動作しています。

image.png

2-1-4. 動作確認

ALBのDNS名にアクセスすると動作確認ができます。

image.png

image.png

(本格的な構成もFargateを使えば簡単!)

2-2. ECS on EC2

コンテナのホスト(EC2)も自分で管理する方法です。
リザーブドインスタンスでコストを極限まで下げたい時や、特殊なGPUインスタンスを使いたい時に用います。ECS on Fargate同様、公開方法にはいくつかありますが、今回はALBを用いずシンプルにパブリックサブネットにEC2を配置してそのまま公開する方法を解説します。

以下の手順で進めます。

  1. IAMロールの作成
  2. ECSクラスターの作成
  3. タスク定義の作成
  4. サービスの作成
  5. 動作確認

2-2-1. IAMロールの作成

まずはEC2に付与するIAMロールを作成します。
以下2つのポリシーを付与したIAMロールを作成します。

  • AmazonEC2ContainerServiceforEC2Role
  • AmazonSSMManagedInstanceCore

AmazonEC2ContainerServiceforEC2RoleはEC2をECSクラスタの一員として動かすために必要な権限です。これを付与することでEC2がECSクラスタに参加し、ECSからのタスク起動指示を受け取ったり、ECSにリソース情報を共有したり、CloudWatch Logsにログを送ったりできるようになります。

AmazonSSMManagedInstanceCoreは「1-1-4-1.EC2への接続」で解説した通り、SSM Session Managerを用いてEC2に接続するための権限です。なくても動きます。

image.png

2-2-2. ECSクラスターの作成

次にECSクラスタを作成します。
マネージドサービスであるFargateと違ってEC2を用いる場合は少々設定項目が増えます。

インフラストラクチャ
最初にAuto Scalingグループの設定を行います。ここでの設定に基づいて自動的にEC2を作成し、ECSクラスターに登録されます。
※この設定はキャパシティープロバイダー戦略を用いる場合に適用されます。

項目 設定値
コンピューティングキャパシティの取得方法 インスタンスとセルフマネージドインスタンス
Auto Scaling グループ (ASG) 新しい Auto Scaling グループの作成
プロビジョニングモデル オンデマンド
AMI Amazon Linux 2023
EC2 インスタンスタイプ t3.large
EC2 インスタンスロール 手順①で作成したロール
必要な容量 最小:1 / 最大:1(必要に応じて変更してください)
SSH キーペア 事前に作成したキーペアを使用
ルート EBS ボリュームサイズ 30 GiB

必要な容量の最小値を0にすると、コンテナインスタンスが自動で登録されない可能性があります。

image.png

image.png

ネットワーク設定
VPC、サブネット、セキュリティグループなどのネットワークの設定を行います。

項目 設定値
VPC 事前作成済みの VPC
サブネット 事前作成済みのパブリックサブネット
セキュリティグループ すべての IP アドレス(0.0.0.0/0)からの HTTP(ポート 80)許可
パブリック IP 自動割り当て サブネットの設定を使用

パブリック IP 自動割り当てをサブネットの設定を使用とすることでパブリックサブネットの場合はパブリックIPアドレスが付与されます。

image.png

2-2-3. タスク定義の作成

その次にタスク定義を作成します。
インフラストラクチャの要件とコンテナの2つの設定を行います。

インフラストラクチャの要件
使用するEC2のインフラの設定を行います。

項目 設定値
起動タイプ Amazon EC2 インスタンス
オペレーティングシステム / アーキテクチャ Linux / x86_64
ネットワークモード bridge
CPU 1 vCPU
メモリ 3 GB
タスク実行ロール ecsTaskExecutionRole

ネットワークモードとはコンテナのネットワーク接続方式を決める設定です。
bridgeモードはコンテナがホストEC2の内部ネットワークに接続され、ポートマッピングを利用して外部と通信する方式です。その他の方式に関しては以下の記事をご参照ください。

ecsTaskExecutionRoleはECSがタスクを起動したり、実行するために必要な権限です。
これにより、ECRからイメージをpullしたり、CloudWatch Logsにログを送信したりすることができます。

image.png

コンテナ
起動するコンテナの設定を行います。

項目 設定値
イメージ URI 起動したい ECR イメージ
ポートマッピング(ホスト → コンテナ) 80 → 8080
プロトコル TCP
アプリケーションプロトコル HTTP

アプリは8080番ポートでアクセスを待ち受けるように設定しているため、ホスト側の80番ポートをコンテナ側の8080番ポートにマッピングします。

image.png

2-2-4. サービスの作成

最後にサービスの作成を行います。

サービスの詳細
使用するタスク定義を指定します。

項目 設定値
タスク定義ファミリー 「2-2-3. タスク定義の作成」で作成したタスク定義
タスク定義のリビジョン デフォルト値

コンピューティング設定
コンテナのタイプやスケールの設定を行います。

項目 設定値
コンピューティングオプション 起動タイプ
起動タイプ EC2

ECS on Fargateでキャパシティープロバイダー戦略を用いたため、ここでは起動タイプを選択しています。
タスクが一つの場合、キャパシティープロバイダー戦略起動タイプの違いは以下の通りです。

項目 起動タイプ(EC2) キャパシティプロバイダ
タスクの起動可否 ✔ 正常に動く ✔ 正常に動く
EC2 の自動スケール ❌ なし(手動) ✔ あり
コスト最適化 ❌ しにくい ✔ 無駄な EC2 を自動停止可能
大規模運用 ❌ 面倒 ✔ 向いている
設定の複雑さ ✔ 簡単 ❌ やや複雑

将来的にタスクを増やす時にも自動スケーリングが効き、かつEC2の台数調整もECSに任せられるため、基本的にはキャパシティープロバイダー戦略を用いるべきです。EC2台数1台で固定かつスケールしない場合は起動タイプでも問題ありません。

image.png

デプロイ設定
タスクの数やタスク定義の変更をどう反映させるかを決める設定を行います。この辺はECS on Fargateと同じです。

項目 設定値
スケジューリング戦略 レプリカ
必要なタスク 1
アベイラビリティーゾーンの再調整 有効化
ヘルスチェックの猶予期間 0

image.png

2-2-5. 動作確認

作成後、動作確認を行います。「タスク」 > [起動したタスク名] > 「設定」を開き、パブリックIPのオープンアドレスをクリックするとコンテナにアクセスできます。

image.png

image.png

(Fargateより設定項目が多いので大変、インフラのスペックを選べるのはいいね)

3. サーバーレスファンクション (Lambda) によるデプロイ

リクエストに応じてコードを実行する、イベント駆動型の手法です。
Goは起動が速く、サーバレスとも相性がいいです。
以下の2種類の方法をご紹介します。

3-1. API Gateway and Lambda

AWS LambdaのGoランタイムを使用します。
リクエストが来ない時間は課金ゼロにすることができますが、aws-lambda-goライブラリを用いてハンドラー化する必要があります。
以下の手順で進めます。

  1. コードの修正(aws-lambda-goライブラリを使用)
  2. Zip化してアップロード
  3. API Gatewayのトリガー設定
  4. 動作確認

3-1-1. コードの修正(aws-lambda-goライブラリを使用)

Lambdaでデプロイしたい場合、そのままのコードでは動作しません。
Lambdaのお作法に則った形式に書き換える必要があります。

main.go
package main

import (
	"bytes"
	"context"
	"html/template"
	"net/http"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

// Lambdaのエントリーポイント
func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {

	tmpl, err := template.ParseFiles("templates/index.html")
	if err != nil {
		return events.APIGatewayV2HTTPResponse{
			StatusCode: http.StatusInternalServerError,
			Body:       err.Error(),
		}, nil
	}

	// HTMLを文字列として生成
	var body string
	{
		buf := new(bytes.Buffer)
		if err := tmpl.Execute(buf, nil); err != nil {
			return events.APIGatewayV2HTTPResponse{
				StatusCode: http.StatusInternalServerError,
				Body:       err.Error(),
			}, nil
		}
		body = buf.String()
	}

	return events.APIGatewayV2HTTPResponse{
		StatusCode: http.StatusOK,
		Headers: map[string]string{
			"Content-Type": "text/html; charset=utf-8",
		},
		Body: body,
	}, nil
}

func main() {
	lambda.Start(handler)
}

変更箇所は以下3点です。

  1. サーバの起動をlambda.Start(handler)に変更
  2. http.handlerFuncをLambda Handlerへ変更
  3. HTMLテンプレートを文字列として返すように変更

3-1-1-1. サーバの起動をlambda.Start(handler)に変更

元々のコードでは以下のようにサーバを起動していました。

http.ListenAndServe(":"+port, nil)

Lambdaはイベント駆動のサービスであり、ポートをlistenしません。
そのため、この部分を以下のように修正し、リクエストが来るたびにhandlerが1回実行されるようにします。

func main() {
	lambda.Start(handler)
}

3-1-1-2. http.HandlerFuncをLambda Handlerへ変更

元々のhandlerでは、以下のようなシグネチャになっていました。

func helloHandler(w http.ResponseWriter, r *http.Request)

Lambdaでは「HTTP」はJSONに変換されます。
ブラウザからのHTTPリクエストはAPI GatewayやFunction URLによってJSONに変換され、それがLambdaに届くような仕組みになっています。

そのJSONをGoのstructに変換したものがevents.APIGatewayV2HTTPRequestです。
中身は以下のようになっています。

// APIGatewayV2HTTPRequest contains data coming from the new HTTP API Gateway
type APIGatewayV2HTTPRequest struct {
	Version               string                         `json:"version"`
	RouteKey              string                         `json:"routeKey"`
	RawPath               string                         `json:"rawPath"`
	RawQueryString        string                         `json:"rawQueryString"`
	Cookies               []string                       `json:"cookies,omitempty"`
	Headers               map[string]string              `json:"headers"`
	QueryStringParameters map[string]string              `json:"queryStringParameters,omitempty"`
	PathParameters        map[string]string              `json:"pathParameters,omitempty"`
	RequestContext        APIGatewayV2HTTPRequestContext `json:"requestContext"`
	StageVariables        map[string]string              `json:"stageVariables,omitempty"`
	Body                  string                         `json:"body,omitempty"`
	IsBase64Encoded       bool                           `json:"isBase64Encoded"`
}

リクエストに関する情報はこのevents.APIGatewayV2HTTPRequestから取得することができます。

同様にLambdaではレスポンスを書き込むのではなく、戻り値として返します。
そのため、必要な情報をstructにまとめて返す必要があります。

そのためのstructがevents.APIGatewayV2HTTPResponseです。
中身は以下のようになっています。

// APIGatewayV2HTTPResponse configures the response to be returned by API Gateway V2 for the request
type APIGatewayV2HTTPResponse struct {
	StatusCode        int                 `json:"statusCode"`
	Headers           map[string]string   `json:"headers"`
	MultiValueHeaders map[string][]string `json:"multiValueHeaders"`
	Body              string              `json:"body"`
	IsBase64Encoded   bool                `json:"isBase64Encoded,omitempty"`
	Cookies           []string            `json:"cookies"`
}

レスポンスとして返したい情報をこのevents.APIGatewayV2HTTPResponseに詰めて返します。

3-1-1-3. HMTLテンプレートを文字列として返すように変更

元々のコードでは、以下のようにHTTPに直接書き込む形でテンプレートを返していました。

func helloHandler(w http.ResponseWriter, r *http.Request) {
	tmpl.Execute(w, nil)
}

「3-1-1-2. http.handlerFuncをLambda Handlerへ変更」でも記述した通り、Lambdaではhttp.ResponseWriterは存在しません。
つまり 「書き込み先」が存在していない状態 です。
そこでbytes.Bufferを用いて、一旦メモリ上に書き込み、後から文字列で取得します。

	// HTMLを文字列として生成
	var body string
	{
		buf := new(bytes.Buffer) // HTMLを書き込むための空のバッファ
		if err := tmpl.Execute(buf, nil); err != nil { // メモリに書き込み
			return events.APIGatewayV2HTTPResponse{
				StatusCode: http.StatusInternalServerError,
				Body:       err.Error(),
			}, nil
		}
		body = buf.String() // メモリ上のHTMLを文字列に変換、これによりLambdaで返せるようになる
	}
    
    return events.APIGatewayV2HTTPResponse{
		StatusCode: http.StatusOK,
		Headers: map[string]string{
			"Content-Type": "text/html; charset=utf-8",
		},
		Body: body, // 文字列のbodyをBodyに詰めて返す
	}, nil 

3-1-2. Zip化してアップロード

3-1-2-1. ビルドとZip化

続いて、作成したコードやテンプレートをビルドし、Zipに固めます。

ビルドは以下のコマンドで実行できます。

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bootstrap main.go

実行ファイル名は必ずbootstrapにする必要があります。

カスタムランタイムのエンドポイントは、bootstrap という名前の実行可能ファイルです。ブートストラップファイルをランタイムにするか、ランタイムを作成する別のファイルを呼び出す場合があります。デプロイパッケージのルートに bootstrap という名前のファイルが含まれていない場合、Lambda は関数のレイヤーでファイルを検索します。bootstrap ファイルが存在しないか、実行可能でない場合、関数は呼び出し時に Runtime.InvalidEntrypoint エラーを返します。

生成した実行ファイルとテンプレートが入ったディレクトリをZipに固めます。
python3コマンドを用いていますが、zipコマンドなどでもOKです。

python3 -m zipfile -c lambda-deployment.zip bootstrap templates

templatesを入れるのを忘れると動作確認の際にエラーになります。

3-1-2-2. Lambda関数を作成し、Zipをアップロード

「関数を作成」からLambdaを作成していきます。

基本的な情報
作成方法とランタイムを設定します。

項目 設定値
作成方法 一から作成
ランタイム Amazon Linux 2023
アーキテクチャ x86_64

image.png

作成後、「アップロード元」>「Zipファイル」から「3-1-2-1. ビルドとZip化」で作成したZipファイルをアップロードします。

image.png

3-1-3. API Gatewayのトリガー設定

最後にAPI Gatewayを作成します。

今回はシンプルな HTTPリクエスト/レスポンス型の Web アプリですので、APIタイプの選択ではHTTP APIを選択します。

image.png

APIを設定
IPアドレスのタイプやLambda関数を指定します。

項目 設定値
IPアドレスのタイプ IPv4
統合 Lambda
Lambda関数 「3-1-2-2. Lambda関数を作成し、Zipをアップロード」で作成した Lambda 関数

image.png

ルートを設定
公開するルートを設定します。

項目 設定値
メソッド GET
リソースパス /
統合ターゲット 「3-1-2-2. Lambda関数を作成し、Zipをアップロード」で作成した Lambda 関数

今回はルートにアクセスするとページが返ってくるだけのWebアプリです。
そのためルートパスにGETリクエストを投げるとLambda関数が実行されるようにします。

image.png

ステージを定義
自動デプロイを有効化します。

image.png

この内容で作成します。

image.png

3-1-4. 動作確認

作成したAPI Gatewayへ移動し、「URLを呼び出す」のURLへアクセスすると動作確認ができます。
image.png

image.png

(Lambda用にコードを書き換えるのがめんどくさい)

3-2. Lambda Web Adapter

最近流行っているLambda Web Adapterを用いる方法です。

通常のアプリのコードを一行も書き換えずにLambdaで動かすことができます。
以下の手順で進めます。

  1. Dockerfileに COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:v1 /lambda-adapter /opt/extensions/lambda-adapter を追加
  2. Lambdaにコンテナイメージとしてデプロイ
  3. 動作確認

※手順は以下の公式ページを参考にしています。

3-2-1. Dockerfileに COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:v1 /lambda-adapter /opt/extensions/lambda-adapter を追加

「1-2-1. Go用のマルチステージビルド Dockerfile の作成」で作成したDockerfileにLambda Web Adapterの設定を一行だけ加えます。

# ================================
# 1. Build stage
# ================================
FROM golang:1.25 AS builder

WORKDIR /app

# Go module filesを先にコピー(キャッシュ効く)
COPY go.mod ./
RUN go env -w GOPROXY=https://proxy.golang.org && go mod download

# 残りのソースコードコピー
COPY . .

# Linux用の静的バイナリをビルド
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server main.go

# ================================
# 2. Runtime stage
# ================================
FROM alpine:3.20

# Lambda Web Adapter を追加
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.0 /lambda-adapter /opt/extensions/lambda-adapter

WORKDIR /app

# ビルドしたバイナリをコピー
COPY --from=builder /app/server .

# templates ディレクトリをコピー
COPY --from=builder /app/templates ./templates

ENV PORT=8080

# 本番実行コマンド
CMD ["./server"]

これをECRにプッシュします。手順は「1-2-2. ECRへのプッシュ」で記載している通りです。

3-2-2. Lambdaにコンテナイメージとしてデプロイ

「関数を作成」から作成していきます。

基本的な情報
コンテナイメージを選択した場合、使用するイメージを選択できます。

項目 設定値
作成方法 コンテナイメージ
イメージURI 3-2-1.で作成したイメージ
アーキテクチャ x86_64

先程作成したイメージを指定します。

image.png

その他の設定
関数URLの有効化や認証タイプを設定します。

項目 設定値
コンピューティングタイプ Lambda(デフォルト)
関数URL 有効化
認証タイプ NONE
呼び出しモード BUFFERED
VPC 無効

コンピューティングタイプをLambda マネージドインスタンスとすると、EC2上にLambdaを起動することができます。高負荷なアプリやコールドスタートを完全に排除したい場合はこちらを選択します。今回はそんなことないのでLambda(デフォルト)を選択します。

関数URLを有効化にすると、Lambdaに直接HTTPSエンドポイントを生やすことができます。
有効化せずにAPI Gatewayでデプロイすることもできますが、今回は有効化します。

image.png

「関数を作成」を押すとLambdaが作成されます。

3-2-3. 動作確認

Lambda作成完了後、関数URLにアクセスするとコンテナが立ち上がり、アプリにアクセスできます。

image.png

image.png

(便利すぎる、、、革命的だ、、、!!!)

4. フルマネージド PaaS ライクなデプロイ

「ネットワークとかコンテナとかよくわかんらん!」な人におすすめの手法です。

4-1. App Runner

ECSやFargateの設定(VPC、ALB、セキュリティグループ)をしなくても済む方法です。楽さだけならピカイチ。
詳細が知りたい方は以下をご参照ください。

以下の手順で進めます。

  1. 80番ポートで待ち受けるように修正したイメージを作成
  2. サービスの作成

4-1-1. 80番ポートで待ち受けるように修正したイメージを作成

App Runnerを用いる場合、Dockerのポートマッピングができません。
現状の8080番ポートで待ち受けるイメージでは80番ポートでデプロイすることができないため、新しくイメージを作成します。手順は「1-2-1. Go用のマルチステージビルド Dockerfile の作成」、「1-2-2. ECRへのプッシュ」で記載している通りです。

8080番ポートで公開する場合、Dockerfileの修正は必要ありません。

# ================================
# 1. Build stage
# ================================
FROM golang:1.25 AS builder

WORKDIR /app

# Go module filesを先にコピー(キャッシュ効く)
COPY go.mod ./
RUN go env -w GOPROXY=https://proxy.golang.org && go mod download

# 残りのソースコードコピー
COPY . .

# Linux用の静的バイナリをビルド
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server main.go

# ================================
# 2. Runtime stage
# ================================
FROM alpine:3.20

WORKDIR /app

# ビルドしたバイナリをコピー
COPY --from=builder /app/server .

# templates ディレクトリをコピー
COPY --from=builder /app/templates ./templates

# ポート公開
# ★8080→80に修正
ENV PORT=80
EXPOSE 80 

# 本番実行コマンド
CMD ["./server"]

これをビルドしてイメージを作成し、port80というタグをつけてECRにプッシュします。
手順は「②Docker on EC2」と同様のためここでは省略します。

image.png

4-1-2. サービスの作成

あとは少しの項目を設定するだけでデプロイできてしまいます。
「サービスの作成」からいくつか設定します。

ソース及びデプロイ
使用するイメージの設定を行います。

項目 設定値
リポジトリタイプ コンテナレジストリ
プロバイダー Amazon ECR
コンテナイメージのURI 1.で作成したイメージのURL
デプロイトリガー 自動
ECRアクセスロール AppRunnerECRAccessRole

image.png

デプロイトリガーはECRに新しいバージョンが更新されたときに自動で更新するかの設定です。自動にしておくことで更新も自動でおこなってくれます。

サービスを設定
CPUやメモリ、ポートの設定を行います。

項目 設定値
仮想CPU 1 vCPU
仮想メモリ 2 GB
ポート 80

image.png

これだけでOKです。「作成とデプロイ」を押してデプロイします。

image.png

これでデプロイ完了です。

4-1-3. 動作確認

デフォルトドメインからページにアクセスできます。

image.png

image.png

(簡単すぎる!!これで大丈夫なの??と不安になるぐらいに、、、!!)

最後に

本記事で紹介した方法を以下の軸で比較してみます。

  • 簡単さ:セットアップ量 / 運用負荷 / 学習コストを総合評価
  • 柔軟性:OS・ネットワーク・ミドルウェア・実行形態の自由度
  • セキュリティ:AWSマネージド度合い
  • コスト:低トラフィック時の安さ + スケール時のコントロール性
デプロイ方式 手軽さ 柔軟性 セキュリティ コスト
1-1. EC2 + 実行ファイル直配置 ★★☆☆☆ ★★★★★ ★★☆☆☆ ★★★★★
1-2. Docker on EC2 ★★☆☆☆ ★★★★★ ★★★☆☆ ★★★★☆
2-1. ECS on Fargate ★★★★☆ ★★★★☆ ★★★★★ ★★★☆☆
2-2. ECS on EC2 ★★★☆☆ ★★★★★ ★★★★☆ ★★★★☆
3-1. API Gateway + Lambda ★★★☆☆ ★★☆☆☆ ★★★★★ ★★★★★
3-2. Lambda Web Adapter ★★★★★ ★★★☆☆ ★★★★★ ★★★★☆
4-1. App Runner ★★★★★ ★★★☆☆ ★★★★☆ ★★☆☆☆
  • 学習・検証 → EC2直配置
  • 王道の本番構成 → ECS on Fargate
  • 時代はサーバレス → API Gateway + Lambda, Lambda Web Adapter
  • さっさと公開 → App Runner

ってところでしょうか。
特に後半2つ(Lambda Web Adapter, App Runner)の手軽さは感動ものでした。
個人的にはLambda Web Adapterが優勝かなあ。

16
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?