はじめに
完璧の「璧」って、「壁」じゃなくて「璧」だって知ってました?
僕は知らなくて、最近会社の先輩に言われて初めて気づきました、、、
スマホとかPCで入力するときは勝手に変換してくれているのでいいんですけど、手書きの時は絶対まちがっていただろうなーと思います
はい、では、前回までで、API の作成と GithubActions で CI の構築をしました
せっかくなので、今回は作った API を動かすための AWS 環境を作っていきます
実際に動かす方法について
以下の手順を見てください
https://github.com/Ixy-194/go-api-sample-todo-terraform/blob/main/README.md
docker で動くようにしているので、terraform インストールしなくても、docker さえ入ってれば動かせます👍
今回のゴール
以下構成の AWS 環境を作ります。terraform で
よくある構成な、シングルリージョン、マルチ AZ 、アプリは ECS 、RDS は Aurora です
API は ECS Fargate で動かす予定で、今回 API のデプロイまではしないので、とりあえずコンテナで nginx を動かしてアクセスできるようにするとこまでやります
Terraform とは?
HashiCorp 社によって開発されたオープンソースの IaC ツールです
AWS, GCP, Azure 等のクラウドサービスのインフラをコードによって構築することができます
Terraformとは、 HashiCorp社によって開発された オープンソースのサービス で、 開発環境を効率的に構築できるIaC(Infrastructure as Code)ツールの一種です。クラウドサーバーなどシステム開発に必要なインフラを、コードを記述することにより自動で構築できます。また、インフラの構成管理などもTerraformのコードによる宣言が可能な事も特徴です
Terraform のメリット
Terraformを導入する一つ目のメリットは、誰が構築しても同一のインフラ構成となることです。複数の開発者が携わるプロジェクトであっても、開発環境の一貫性を保てます
二つ目のメリットは、工数削減効果です。Terraformでは、一度構築したインフラをもとに設計ができるため、同様の環境を構築する際の工数を削減できます。三つ目のメリットは、Gitによるバージョン管理に対応していることです。GitOpsやDevOpsなどさまざまな管理システムを用いて、テスト環境や本番環境を管理できます
詳細はこちらを参照
AWS 環境の構築
ECS Fargate で API の実行環境を作るにあたり、以下の AWS リソースを作成します
- VPC
- RDS(Aurora)
- ALB
- ECS
- ECR
- Bastion(RDSへの踏み台用サーバ)
最終的に出来上がるコードの構成はこんな感じです
.
├── docker-compose.yml
├── env
│ └── dev
│ ├── main.tf
│ └── provider.tf
└── modules
├── alb
│ ├── main.tf
│ ├── output.tf
│ └── variables.tf
├── bastion
│ ├── main.tf
│ └── variables.tf
├── ecr
│ ├── main.tf
│ ├── output.tf
│ └── variables.tf
├── ecs
│ ├── main.tf
│ └── variables.tf
├── rds
│ ├── main.tf
│ └── variables.tf
└── vpc
├── main.tf
├── outputs.tf
└── variables.tf
では、順々にコードを書いて環境を作っていきます
VPC
まずは VPC の作成です
VPC とは Amazon Virtual Private Cloud のことで、AWS 上に構築する仮想ネットワーク環境のことです
ここでは、VPC リソースの作成に「Terraform-aws-modules/vpc/aws」を使い、以下赤枠内の環境を構築します
実際のコード
# 共通的に使用する値を変数として定義
locals {
env = "dev"
cidr = "192.168.1.0/24"
public_subnets = ["192.168.1.64/28", "192.168.1.80/28"]
private_subnets = ["192.168.1.32/28", "192.168.1.48/28"]
rds_subnets = ["192.168.1.0/28", "192.168.1.16/28"]
azs = ["ap-northeast-1a", "ap-northeast-1c"]
service_name = "go-api-sample-todo"
}
module "vpc" {
source = "../../modules/vpc"
cidr = local.cidr
public_subnets = local.public_subnets
private_subnets = local.private_subnets
rds_subnets = local.rds_subnets
azs = local.azs
env = local.env
}
locals
に共通的に使用する値(CIDRとか使用する AZとか)を変数として定義し、/modules/vpc
を呼び出します
modules/vpc
には、以下の3ファイルがあります
- main.tf
- 作成する AWS リソースを定義するファイル
- outputs.tf
- 戻り値を定義するファイル
⇨ここで作成したリソースの情報を他モジュールでも参照・利用できるように変数に格納する
- 戻り値を定義するファイル
- variables.tf
- 引数を定義するファイル
variables.tf
では以下のように、env/dev/main.tf
から値を受け取るための引数を定義しています
variable "cidr" {}
variable "public_subnets" {}
variable "private_subnets" {}
variable "rds_subnets" {}
variable "azs" {}
variable "env" {}
main.tf
#################################################################################
# Network
# Ref. https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/5.0.0
#################################################################################
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "${var.env}-vpc"
cidr = var.cidr
azs = var.azs
public_subnets = var.public_subnets
private_subnets = var.private_subnets
database_subnets = var.rds_subnets
enable_nat_gateway = true
enable_vpn_gateway = true
tags = {
Terraform = "true"
Environment = var.env
}
}
「Terraform-aws-modules/vpc/aws」を使うと、これだけの記述で VPC 周りの NW 環境を作れるので便利です
outputs.tf
VPC リソースの情報を他のモジュールでも使えるように変数に格納しておきます。
output "vpc" {
value = module.vpc
}
これでベースとなるネットワーク環境ができました。
ALB
ALB とは Application Load Balancer のことで、いわゆる ロードバランサーです
リクエストを受けて、背後にあるサービス(今回なら ECS)にいい感じにリクエストを振り分けてルーティングしてくれます
main.tf
# ALB
# ALB
resource "aws_lb" "this" {
name = "${var.env}-alb"
internal = false
load_balancer_type = "application"
subnets = var.subnets
security_groups = [aws_security_group.this.id]
tags = {
Name = "${var.env}-alb"
Terraform = "true"
Environment = var.env
}
}
# SecurityGroup
resource "aws_security_group" "this" {
name = "${var.env}-sg-alb"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.env}-sg-alb"
Terraform = "true"
Environment = var.env
}
}
これで terraform apply
をすると、コンソールから ALB が作成されているのがわかります
ただし、現時点では listener(ALBの振り分け先)の指定をしていないので、アクセスすることはできません
次の ECS で listener の作成を行います
ECS
ECS とは Amazon Elastic Container Service (Amazon ECS)のことで、AWS が作成したフルマネージドなコンテナオーケストレーションサービスです
k8s の AWS 版と思っていればいいと思います
ECS は、大きく3つの要素によって構成されています
- クラスタ
- タスクとサービスをグループ化するもの
- サービス
- タスク定義を元にタスク(コンテナ)を立ち上げて、ロードバランサとの紐付けをするもの
- タスク
- 実際に起動するコンテナ
- タスク定義(task_definition)の内容を元に生成されたコンテナのこと
ここでは、以下のリソースを作成します
実際のコードはこんな感じ
# クラスター定義
resource "aws_ecs_cluster" "this" {
name = "${var.env}-ecs-cluster-${var.service_name}"
tags = {
Name = "${var.env}-ecs-cluster-${var.service_name}"
Terraform = "true"
Environment = var.env
}
}
# サービス定義
resource "aws_ecs_service" "this" {
name = "${var.env}-ecs-service-${var.service_name}"
cluster = aws_ecs_cluster.this.id
task_definition = aws_ecs_task_definition.this.arn
desired_count = 1
launch_type = "FARGATE"
platform_version = "1.4.0"
network_configuration {
subnets = var.subnets
security_groups = [aws_security_group.this.id]
}
# ALB との紐付け
load_balancer {
target_group_arn = aws_lb_target_group.this.arn
container_name = var.service_name
container_port = "80"
}
lifecycle {
ignore_changes = [desired_count, task_definition]
}
tags = {
Name = "${var.env}-ecs-service-${var.service_name}"
Terraform = "true"
Environment = var.env
}
}
# タスク定義
resource "aws_ecs_task_definition" "this" {
family = "${var.env}-task-definition-${var.service_name}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
container_definitions = jsonencode([
{
name = var.service_name
# 暫定で nginx を立てる
# 別途 CD でイメージを上書きする
image = "nginx:latest"
logConfiguration : {
logDriver : "awslogs",
options : {
awslogs-region : "ap-northeast-1",
awslogs-stream-prefix : var.service_name,
awslogs-group : "/ecs/${var.service_name}/${var.env}"
}
}
portMappings = [
{
containerPort = 80
}
]
}
])
task_role_arn = aws_iam_role.this.arn
execution_role_arn = aws_iam_role.this.arn
tags = {
Name = "${var.env}-task-definition-${var.service_name}"
Terraform = "true"
Environment = var.env
}
}
# ターゲットグループの作成
resource "aws_lb_target_group" "this" {
name = "${var.env}-alb-tg-${var.service_name}"
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = var.vpc_id
tags = {
Name = "${var.env}-alb-tg-${var.service_name}"
Terraform = "true"
Environment = var.env
}
}
# listener の作成
resource "aws_lb_listener" "http" {
port = "80"
protocol = "HTTP"
load_balancer_arn = var.lb.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.this.arn
}
tags = {
Name = "${var.env}-lb-http-listener-${var.service_name}"
Terraform = "true"
Environment = var.env
}
}
resource "aws_security_group" "this" {
name = "${var.env}-sg-${var.service_name}"
description = "${var.env}-sg-${var.service_name}"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [var.lb_security_group_id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.env}-sg-${var.service_name}"
Terraform = "true"
Environment = var.env
}
}
resource "aws_cloudwatch_log_group" "this" {
name = "/ecs/${var.service_name}/${var.env}"
}
# 実行ロール
resource "aws_iam_role" "this" {
name = "${var.env}-ecs-execution-role-${var.service_name}"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_iam_role_policy" "this" {
name = "${var.env}-ecs-execution-role-policy-${var.service_name}"
role = aws_iam_role.this.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"ssm:GetParameters",
"secretsmanager:GetSecretValue",
]
Effect = "Allow"
Resource = "*"
},
]
})
}
resource "aws_iam_role_policy_attachment" "this" {
role = aws_iam_role.this.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
これで terraform apply
をして、 ALB の URL にアクセスすると nginx の画面が表示されるはずです
ALB の URL はコンソール画面から確認できます
ALB の URL にアクセスしてこんな画面が表示されれば OK
RDS(Aurora)
続いて、Amazon Aurora で DB を作ります
Amazon Aurora は、AWS が開発したフルマネージドなリレーショナルデータベースのことです
terraform:env/dev/main.tf
からの呼び出し部分はこんな感じ
module "rds" {
source = "../../modules/rds"
env = local.env
vpc_id = module.network.vpc.vpc_id
azs = local.azs
db_subnet_group_name = module.network.vpc.database_subnet_group
access_allow_cidr_blocks = module.network.vpc.private_subnets_cidr_blocks
}
配置する VPC、AZ、サブネットグループの指定と、アクセスを許可する cider(今回は private subnet からのみアクセス許可する)を指定します
# 使用する Aurora DB のエンジン、バージョン情報を定義
locals {
master_username = "admin"
engine = "aurora-mysql"
engine_version = "8.0.mysql_aurora.3.02.0"
instance_class = "db.t4g.medium"
database_name = "todo"
}
# RDS
resource "aws_rds_cluster" "this" {
cluster_identifier = "${var.env}-cluster-${local.database_name}"
database_name = local.database_name
master_username = local.master_username
master_password = random_password.this.result
availability_zones = var.azs
port = 3306
vpc_security_group_ids = [aws_security_group.this.id]
db_subnet_group_name = var.db_subnet_group_name
db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.this.id
engine = local.engine
engine_version = local.engine_version
final_snapshot_identifier = "${var.env}-cluster-final-snapshot-${local.database_name}"
skip_final_snapshot = true
apply_immediately = true
tags = {
Name = "${var.env}-cluster-${local.database_name}"
Terraform = "true"
Environment = var.env
}
lifecycle {
ignore_changes = [
availability_zones,
]
}
}
resource "aws_rds_cluster_instance" "this" {
count = 1
identifier = "${var.env}-${local.database_name}-${count.index}"
engine = local.engine
engine_version = local.engine_version
cluster_identifier = aws_rds_cluster.this.id
instance_class = local.instance_class
tags = {
Name = "${var.env}-${local.database_name}-${count.index}"
Terraform = "true"
Environment = var.env
}
}
# master_username のパスワードを自動生成
resource "random_password" "this" {
length = 12
special = true
override_special = "!#&,:;_"
lifecycle {
ignore_changes = [
override_special
]
}
}
resource "aws_rds_cluster_parameter_group" "this" {
name = "${var.env}-rds-cluster-parameter-group-${local.database_name}"
family = "aurora-mysql8.0"
parameter {
name = "time_zone"
value = "Asia/Tokyo"
}
}
# Security Group
resource "aws_security_group" "this" {
name = "${var.env}-sg-rds-${local.database_name}"
vpc_id = var.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.env}-sg-rds-${local.database_name}"
Terraform = "true"
Environment = var.env
}
}
resource "aws_security_group_rule" "this" {
type = "ingress"
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = var.access_allow_cidr_blocks
security_group_id = aws_security_group.this.id
}
# 各種パラメータを AWS Systems Manager Parameter Store へ保存
resource "aws_ssm_parameter" "master_username" {
name = "/${var.env}/rds/${local.database_name}/master_username"
type = "SecureString"
value = aws_rds_cluster.this.master_username
tags = {
Terraform = "true"
environment = var.env
}
}
resource "aws_ssm_parameter" "master_password" {
name = "/${var.env}/rds/${local.database_name}/master_password"
type = "SecureString"
value = aws_rds_cluster.this.master_password
tags = {
Terraform = "true"
environment = var.env
}
}
resource "aws_ssm_parameter" "cluster_endpoint" {
name = "/${var.env}/rds/${local.database_name}/endpoint_w"
type = "SecureString"
value = aws_rds_cluster.this.endpoint
tags = {
Terraform = "true"
environment = var.env
}
}
resource "aws_ssm_parameter" "cluster_reader_endpoint" {
name = "/${var.env}/rds/${local.database_name}/endpoint_r"
type = "SecureString"
value = aws_rds_cluster.this.reader_endpoint
tags = {
Terraform = "true"
environment = var.env
}
}
aws_ssm_parameter
では、生成されたパスワードや RDS のエンドポイントをアプリケーション側から取得できるように、AWS System Manager のParameter Storeに格納しています
これで、terraform apply
すると、コンソール画面から クラスターとインスタンスが作成され、Parameter Store にも以下で指定したパラメータが保存されています
# 各種パラメータを AWS Systems Manager Parameter Store へ保存
resource "aws_ssm_parameter" "master_username" {
name = "/${var.env}/rds/${local.database_name}/master_username"
type = "SecureString"
value = aws_rds_cluster.this.master_username
tags = {
Terraform = "true"
environment = var.env
}
}
resource "aws_ssm_parameter" "master_password" {
name = "/${var.env}/rds/${local.database_name}/master_password"
type = "SecureString"
value = aws_rds_cluster.this.master_password
tags = {
Terraform = "true"
environment = var.env
}
}
resource "aws_ssm_parameter" "cluster_endpoint" {
name = "/${var.env}/rds/${local.database_name}/endpoint_w"
type = "SecureString"
value = aws_rds_cluster.this.endpoint
tags = {
Terraform = "true"
environment = var.env
}
}
resource "aws_ssm_parameter" "cluster_reader_endpoint" {
name = "/${var.env}/rds/${local.database_name}/endpoint_r"
type = "SecureString"
value = aws_rds_cluster.this.reader_endpoint
tags = {
Terraform = "true"
environment = var.env
}
}
画面から確認するとこんな感じ
Bastion
EC2 を使って踏み台サーバを構築します
ここでの用途としては、 DB にアクセスするための踏み台サーバです
踏み台といっても外部に公開するのは嫌なので、private subnet に配置し、ssh ではなく、コンソール画面からのみ接続できるようにします
実際のソースコードはこんな感じ
# EC2
## 最新の AmazonLinux2 の AMI の ID を取得
data "aws_ssm_parameter" "this" {
name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}
resource "aws_instance" "centos" {
ami = data.aws_ssm_parameter.this.value
instance_type = "t2.nano"
subnet_id = var.subnet_id
iam_instance_profile = aws_iam_instance_profile.this.name
vpc_security_group_ids = [aws_security_group.this.id]
key_name = "centos-bastion"
tags = {
Name = "${var.env}-bastion"
Terraform = "true"
Environment = var.env
}
}
resource "aws_instance" "this" {
ami = data.aws_ssm_parameter.this.value
instance_type = "t2.nano"
subnet_id = var.subnet_id
iam_instance_profile = aws_iam_instance_profile.this.name
vpc_security_group_ids = [aws_security_group.this.id]
tags = {
Name = "${var.env}-aws-bastion"
Terraform = "true"
Environment = var.env
}
}
# Security Group
## コンソール画面からしかアクセスしないので、 ingress は設定しない
resource "aws_security_group" "this" {
name = "${var.env}-sg-bastion"
vpc_id = var.vpc_id
ingress {
description = "from rivate"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.env}-sg-bastion"
Terraform = "true"
Environment = var.env
}
}
# IAM
## コンソールからセッションマネージャーでアクセスできるように、IAMロールとIAMポリシーを設定
resource "aws_iam_role" "this" {
name = "${var.env}-iam-role-bastion"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
}
resource "aws_iam_instance_profile" "this" {
name = "${var.env}-iam-instance-profile-bastion"
role = aws_iam_role.this.name
}
data "aws_iam_policy" "this" {
arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_role_policy_attachment" "this" {
role = aws_iam_role.this.name
policy_arn = data.aws_iam_policy.this.arn
}
これでわざわざローカルから SSH で接続しなくても、コンソール画面から接続できるので便利ですね
ECR
最後に ECR の作成です
ECR とは Amazon Elastic Container Registry のことで、AWS が提供するフルマネージドなコンテナイメージレジストリです
docker のコンテナイメージの管理に使います
実際のコードはこんな感じ
resource "aws_ecr_repository" "this" {
name = var.service_name
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
tags = {
Name = "${var.env}-ecr-${var.service_name}"
Terraform = "true"
Environment = var.env
}
}
これで terraform apply
するだけでリポジトリが作れます
簡単ですね
最後に
こんな感じで AWS 環境を作ってみました
実際のソースコードはこちら
正直、コンソール画面からぽちぽち設定するよりも、 terraform とかで環境をコード化できるっていうのは素晴らしいと思う
作り直しが簡単だし、どこかの手順をが抜けてた!なんてこともないですし
わざわざエクセルとかで手順書なんて作りたくないですからねー
次回は、 この GithubActions でこの terraform のコードを CI/CD してみようと思います
ではでは