Go x Next.js(SPA) な環境をTerraformでAWSに構築してみます!
バックエンドのGoには、ECS Fargateで。
フロントエンドのSPAには、CloudFront・S3を使用して静的ホスティングをしてます。
また、System エージェントをインストールすることでFargateコンテナ内に接続できるようにしています。
はじめに
連載記事でこの環境を構築していきます。
本記事では、ECSクラスター・SSMアクティベーション・ECSサービスの作成を行います!
フォルダ構成とか書き方、モージュル化などは、【ネットワーク環境構築】terraform AWS環境構築 第1回この記事とほぼ同じなので気になる方は御覧ください!
全体のソースコード:github
環境は以下です。
| OS | Cataline 10.15.6 |
| Terraform | 0.14.4 |
| Go | 1.16.3 |
| React | 17.0.2 |
基本構文などこちらにまとめてますので、よかったらみてください!
AWS Terraform 基本コード まとめ
連載一覧
- Go x Next.js(SPA) をTerraformでさっさと構築 1/3
- Go x Next.js(SPA) をTerraformでさっさと構築 2/3
- Go x Next.js(SPA) をTerraformでさっさと構築 3/3 ←ここ
やること
以下の定義と作成をします。
ECSクラスター
【ECS Fargate(nginx)実行】terraform AWS環境構築 第4回
ここのECSクラスターの作成とほぼ同じです。
./main.tf
module "network" {
source = "./network"
app_name = var.app_name
}
module "acm" {
source = "./acm"
domain = var.domain
}
module "spa" {
source = "./spa"
app_name = var.app_name
domain = var.domain
acm_id = module.acm.acm_id
}
module "subdomain_acm" {
source = "./subdomain_acm"
domain = var.domain
}
module "elb" {
source = "./elb"
app_name = var.app_name
vpc_id = module.network.vpc_id
public_subnet_ids = module.network.public_subnet_ids
sub_acm_id = module.subdomain_acm.sub_acm_id
domain = var.domain
}
module "rds" {
source = "./rds"
app_name = var.app_name
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
vpc_id = module.network.vpc_id
alb_security_group_id = module.elb.alb_security_group_id
private_subnet_ids = module.network.private_subnet_ids
}
# 追記
module "ecs_cluster" {
source = "./ecs_cluster"
app_name = var.app_name
}
./ecs_cluster/main.tf
resource "aws_ecs_cluster" "this" {
name = "${var.app_name}-cluster"
}
./ecs_cluster/variables.tf
variable "app_name" {}
./ecs_cluster/outputs.tf
output "cluster_name" {
value = aws_ecs_cluster.this.name
}
SSMアクティベーション
Fargateコンテナ内にSession Managerでログインできるようにアクティベーションを作成します。※Fargateは外部サーバーとしてみなすので、アクティベーションを作成する必要があります。
簡単に説明すると、以前インスタンスやコンテナにセキュアに接続するにはSSHポートなど開けて対応するのですがSession Managerを使用すればポート開けずに接続できるため、SSH接続するよりセキュアにできる。というものです。
以下のサイトが参考になりました。
https://tech.smartcamp.co.jp/entry/fargate-with-session-manager
https://qiita.com/pocari/items/3f3d77c80893f9f1e132
https://enokawa.hatenablog.jp/entry/2019/09/05/104545
./main.tf
module "network" {
source = "./network"
app_name = var.app_name
}
module "acm" {
source = "./acm"
domain = var.domain
}
module "spa" {
source = "./spa"
app_name = var.app_name
domain = var.domain
acm_id = module.acm.acm_id
}
module "subdomain_acm" {
source = "./subdomain_acm"
domain = var.domain
}
module "elb" {
source = "./elb"
app_name = var.app_name
vpc_id = module.network.vpc_id
public_subnet_ids = module.network.public_subnet_ids
sub_acm_id = module.subdomain_acm.sub_acm_id
domain = var.domain
}
module "rds" {
source = "./rds"
app_name = var.app_name
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
vpc_id = module.network.vpc_id
alb_security_group_id = module.elb.alb_security_group_id
private_subnet_ids = module.network.private_subnet_ids
}
module "ecs_cluster" {
source = "./ecs_cluster"
app_name = var.app_name
}
# 追記
module "ssm_activation" {
source = "./ssm_activation"
app_name = var.app_name
}
IAMロールの作成
SSM アクティベーションの作成前に、IAMロールの作成をしておきます。
./ssm_activation/main.tf
module "ssm_ec2_run_role" {
source = "../iam_role"
name = "ssm-ec2-run-role"
identifier = "ssm.amazonaws.com"
policy_arn = [
data.aws_iam_policy.ssm_managed_instance_core.arn,
data.aws_iam_policy.ssm_directory_service_access.arn,
data.aws_iam_policy.cloud_watch_agent_server_policy.arn,
aws_iam_policy.update_ssm_service_policy.arn,
]
}
locals {
account_id = data.aws_caller_identity.user.account_id
}
resource "aws_iam_policy" "update_ssm_service_policy" {
name = "update-ssm-service"
policy = data.template_file.update_ssm_service_policy.rendered
}
./ssm_activation/variables.tf
variable "app_name" {}
./ssm_activation/data.tf
data "aws_iam_policy" "ssm_managed_instance_core" {
arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
data "aws_iam_policy" "ssm_directory_service_access" {
arn = "arn:aws:iam::aws:policy/AmazonSSMDirectoryServiceAccess"
}
data "aws_iam_policy" "cloud_watch_agent_server_policy" {
arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}
data "aws_caller_identity" "user" {}
data "template_file" "update_ssm_service_policy" {
template = file("ssm_activation/update_ssm_service_policy.json")
vars = {
account_id = local.account_id
}
}
./ssm_activation/update_ssm_service_policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetServiceSetting"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ssm:ResetServiceSetting",
"ssm:UpdateServiceSetting"
],
"Resource": "arn:aws:ssm:ap-northeast-1:${account_id}:servicesetting/ssm/managed-instance/activation-tier"
}
]
}
- AmazonSSMManagedInstanceCore:マネージドインスタンスで AWS Systems Manager サービスの主要機能を使用できるようにします。
- AmazonSSMDirectoryServiceAccess:マネージドインスタンスによるドメインへの結合リクエストに対して、代わりに SSM エージェント が AWS Directory Service にアクセスできるようにします。
- CloudWatchAgentServerPolicy:CloudWatch エージェント がマネージドインスタンスで実行できるようにします。このコマンドでは、インスタンスの情報を読み込み、CloudWatchに書き込めるようにします。
- update_ssm_service_policy:アドバンストインスタンス層の有効化をできるようします。
SSM アクティベーションの作成
コンテナログインのためのアクティベーションを作成しましょう。
./ssm_activation/main.tf
module "ssm_ec2_run_role" {
source = "../iam_role"
name = "ssm-ec2-run-role"
identifier = "ssm.amazonaws.com"
policy_arn = [
data.aws_iam_policy.ssm_managed_instance_core.arn,
data.aws_iam_policy.ssm_directory_service_access.arn,
data.aws_iam_policy.cloud_watch_agent_server_policy.arn,
aws_iam_policy.update_ssm_service_policy.arn,
]
}
locals {
account_id = data.aws_caller_identity.user.account_id
}
resource "aws_iam_policy" "update_ssm_service_policy" {
name = "update-ssm-service"
policy = data.template_file.update_ssm_service_policy.rendered
}
# 追記
resource "aws_ssm_activation" "this" {
name = "${var.app_name}-ssm"
iam_role = module.ssm_ec2_run_role.iam_role_name
registration_limit = "5"
}
- aws_ssm_activation ;SSM アクティベーション
- name :Name
- iam_role:IAMロール
- registration_limit:コンテナ・インスタンス登録制限 (max: 1000)
./ssm_activation/outputs.tf
output "ssm_agent_code" {
value = aws_ssm_activation.this.activation_code
}
output "ssm_agent_id" {
value = aws_ssm_activation.this.id
}
SSMにコンテナを登録するために、Agent CodeとAgent IDをアウトプット変数として定義しておきます。
[terraform] $ terraform plan
作成されるリソースの確認。
[terraform] $ terraform apply
リソースの作成。

「ハイブリッドアクティベーション」を選択すると、アクティベーションが作成されていると思います。
ECSサービス
amazon-ssm-agentをインストールしたGoコンテナを作成し、ECS Fargate デプロイします。
【ECS Fargate(rails + nginx)実行】terraform AWS環境構築 第7回
こことほぼ同じです。
./main.tf
module "network" {
source = "./network"
app_name = var.app_name
}
module "acm" {
source = "./acm"
domain = var.domain
}
module "spa" {
source = "./spa"
app_name = var.app_name
domain = var.domain
acm_id = module.acm.acm_id
}
module "subdomain_acm" {
source = "./subdomain_acm"
domain = var.domain
}
module "elb" {
source = "./elb"
app_name = var.app_name
vpc_id = module.network.vpc_id
public_subnet_ids = module.network.public_subnet_ids
sub_acm_id = module.subdomain_acm.sub_acm_id
domain = var.domain
}
module "rds" {
source = "./rds"
app_name = var.app_name
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
vpc_id = module.network.vpc_id
alb_security_group_id = module.elb.alb_security_group_id
private_subnet_ids = module.network.private_subnet_ids
}
module "ecs_cluster" {
source = "./ecs_cluster"
app_name = var.app_name
}
module "ssm_activation" {
source = "./ssm_activation"
app_name = var.app_name
}
# 追記
module "ecs_go" {
source = "./ecs_go"
app_name = var.app_name
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
db_host = module.rds.db_address
ssm_agent_code = module.ssm_activation.ssm_agent_code # SSM Agent Code
ssm_agent_id = module.ssm_activation.ssm_agent_id # SSM Agent ID
vpc_id = module.network.vpc_id
http_listener_arn = module.elb.http_listener_arn
https_listener_arn = module.elb.https_listener_arn
alb_security_group_id = module.elb.alb_security_group_id
cluster_name = module.ecs_cluster.cluster_name
public_subnet_ids = module.network.public_subnet_ids
}
./terraform.tfvars
domain = "<your domain>" db_name = "go-next_db" db_user = "root" db_password = "password"
./variables.tf
variable "aws_region" {
type = string
default = "ap-northeast-1"
}
variable "aws_profile" {
type = string
default = "default"
description = "AWS CLI's profile"
}
variable "app_name" {
type = string
default = "go-next"
}
variable "domain" {}
variable "db_name" {}
variable "db_user" {}
variable "db_password" {}
./ecs_go/main.tf
locals {
account_id = data.aws_caller_identity.user.account_id
}
resource "aws_iam_policy" "ecs_task_execution" {
name = "ecs-task-execution"
policy = data.aws_iam_policy_document.ecs_task_execution.json
}
module "ecs_task_execution_role" {
source = "../iam_role"
name = "ecs-task-execution"
identifier = "ecs-tasks.amazonaws.com"
policy_arn = [aws_iam_policy.ecs_task_execution.arn]
}
resource "aws_ecs_task_definition" "this" {
family = "${var.app_name}-task"
cpu = 256
memory = 2048
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
container_definitions = data.template_file.container_definitions.rendered
execution_role_arn = module.ecs_task_execution_role.iam_role_arn
}
resource "aws_cloudwatch_log_group" "this" {
count = length(var.app_names)
name = "/ecs/go-next/${var.app_names[count.index]}"
}
resource "aws_lb_target_group" "this" {
name = "${var.app_name}-tg"
vpc_id = var.vpc_id
port = 80
protocol = "HTTP"
target_type = "ip"
health_check {
port = 80
path = "/health"
}
}
resource "aws_lb_listener_rule" "http_rule" {
listener_arn = var.http_listener_arn
action {
type = "forward"
target_group_arn = aws_lb_target_group.this.id
}
condition {
path_pattern {
values = ["*"]
}
}
}
resource "aws_lb_listener_rule" "https_rule" {
listener_arn = var.https_listener_arn
action {
type = "forward"
target_group_arn = aws_lb_target_group.this.id
}
condition {
path_pattern {
values = ["*"]
}
}
}
resource "aws_ecs_service" "this" {
name = "${var.app_name}-service"
launch_type = "FARGATE"
desired_count = 1
cluster = var.cluster_name
task_definition = aws_ecs_task_definition.this.arn
network_configuration {
security_groups = [var.alb_security_group_id]
subnets = var.public_subnet_ids
assign_public_ip = true
}
load_balancer {
target_group_arn = aws_lb_target_group.this.arn
container_name = "nginx"
container_port = 80
}
}
./ecs_go/variables.tf
variable "app_name" {}
variable "vpc_id" {}
variable "db_user" {}
variable "db_password" {}
variable "db_host" {}
variable "db_name" {}
variable "app_names" {
default = ["nginx", "go-next_api"]
}
variable "http_listener_arn" {}
variable "https_listener_arn" {}
variable "alb_security_group_id" {}
variable "cluster_name" {}
variable "public_subnet_ids" {}
variable "ssm_agent_code" {}
variable "ssm_agent_id" {}
./ecs_go/data.tf
data "aws_caller_identity" "user" {}
data "aws_iam_policy" "ecs_task_execution_role_policy" {
arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
#「AmazonECSTaskExecutionRolePolicy」ロールを継承したポリシードキュメントの定義
data "aws_iam_policy_document" "ecs_task_execution" {
source_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy
statement {
effect = "Allow"
actions = ["ssm:GetParameters", "kms:Decrypt"]
resources = ["*"]
}
}
data "template_file" "container_definitions" {
template = file("./ecs_go/container_definitions.json")
vars = {
account_id = local.account_id
db_host = var.db_host
db_user = var.db_user
db_password = var.db_password
db_name = var.db_name
ssm_agent_code = var.ssm_agent_code
ssm_agent_id = var.ssm_agent_id
}
}
./ecs_go/container_definitions.json
[
{
"name": "nginx",
"image": "${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
"essential": true,
"memory": 128,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "nginx",
"awslogs-group": "/ecs/go-next/nginx"
}
},
"portMappings": [
{
"containerPort": 80,
"hostPort": 80
}
]
},
{
"name": "go-next_api",
"image":"${account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/go-next_api:latest",
"essential": true,
"memory": 1536,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "go-next_api",
"awslogs-group": "/ecs/go-next/go-next_api"
}
},
"command": ["go","run","main.go"],
"environment": [
{
"name": "RDS_HOST",
"value": "${db_host}"
},
{
"name": "RDS_USER",
"value": "${db_user}"
},
{
"name": "RDS_PASSWORD",
"value": "${db_password}"
},
{
"name": "RDS_PORT",
"value": "3306"
},
{
"name": "RDS_DB_NAME",
"value": "${db_name}"
},
{
"name": "SSM_AGENT_CODE",
"value": "${ssm_agent_code}"
},
{
"name": "SSM_AGENT_ID",
"value": "${ssm_agent_id}"
},
{
"name": "FRONT_URL",
"value": "<your domain>"
}
],
"portMappings": [
{
"containerPort": 8080,
"hostPort": 8080
}
]
}
]
そして、以下がGoコンテナのDockerfileです。
FROM golang:1.16.3 RUN apt-get update && apt-get install -y sudo # amazon-ssm-agent インストール RUN mkdir /tmp/ssm RUN wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb -O /tmp/ssm/amazon-ssm-agent.deb \ && sudo dpkg -i /tmp/ssm/amazon-ssm-agent.deb \ && cp /etc/amazon/ssm/seelog.xml.template /etc/amazon/ssm/seelog.xml # 以下任意 WORKDIR /go/src/app RUN go get github.com/rubenv/sql-migrate/... COPY go.mod go.sum ./ RUN go mod download COPY . . COPY docker-entrypoint.sh / ENTRYPOINT [ "/docker-entrypoint.sh" ]
docker-entrypoint.sh
#!/bin/bash
set -e
# SSMにコンテナを登録
amazon-ssm-agent -register -code "${SSM_AGENT_CODE}" -id "${SSM_AGENT_ID}" -region "ap-northeast-1"
# バックグラウンド実行
amazon-ssm-agent &
exec "$@"
これで、コンテナが起動する際にSSMに登録されSession Managerでログインできるようになります。
今回はGoのDockerイメージを使用しているためDebianでのインストール方法を参考にしています。他のOSによってインストール方法は変わるので注意してください。
公式はこちら
[terraform] $ terraform plan
作成されるリソースの確認。
[terraform] $ terraform apply
リソースの作成。
これでECSサービス立ち上げてSSMにコンテナが登録されると思いますので見てみましょう。

「フリートマネージャー」にインスタンスが1つ増えていますね。
このインスタンスをクリックして「インスタンスアクション」から「セッションを開始」でコンテナにログインできます。

セッション開始。

ここでコマンド操作が可能になります。
セッション開始からアドバンスインスタンスティアになり従量制で料金が発生しますので注意が必要です。
料金について
正常にGoコンテナが動いているか見てみましょう。
おわり
これで、Go x Next.js(SPA)は完成です!お疲れさまでした。
SSM エージェントを使用することでコンテナの運用をやりやすくできますね!
alpineベースで作成できないのが気になりますが。。仕方がないのですかね??
また、SSM アクティベーションには登録制限がありコンテナが落ちただけではこの制限から数が減ることはありません。そのため、登録制限までコンテナの更新をしてまったらアクティベーションの再作成をする必要ができます。
その1つの対策として、アクティベーションはTerraformで作成せず、docker-entrypoint.shでアクティベーションを作成するコマンドを入力して作成する方法があります。
set -e SSM_ACTIVATION=$(aws ssm create-activation --default-instance-name "go-next-ssm" --iam-role "ssm-ec2-run-role" --registration-limit 1 --region "ap-northeast-1") export SSM_ACTIVATION_CODE=$(echo $SSM_ACTIVATION | jq -r .ActivationCode) export SSM_ACTIVATION_ID=$(echo $SSM_ACTIVATION | jq -r .ActivationId) amazon-ssm-agent -register -code $SSM_ACTIVATION_CODE -id $SSM_ACTIVATION_ID -region ap-northeast-1 exec "$@"
その他には、CloudFormationとかで対策できそうですね。(やり方はまだ調べてない。。)
とりあえず、、最後までご覧いただきありがとうございます!
何か疑問に思うことがあれば、何でもいいのでコメントくれれば精一杯答えさせていただきます。
Twitterとかフォローしてくれると嬉しいです。では、次回お会いしましょう!

コメント