はじめに
当記事では、AWSのECS/Fargateインフラを
Terraformでコードから再現する作業をまとめています。
なお、Terraformのインストールなど事前準備は
別記事でまとめていますので、まずはそちらをご確認ください。
また、Terraformではなく、AWSマネジメントコンソールからGUIで
ECS/Fargateインフラを構築する手順は別記事でまとめていますので
ご興味がございましたら、ご覧ください。
前提として、ECS/FargateにホストしているアプリはRails
となりますので
その点をご理解ください。
使用技術
- Terraform: 1.1.2
- ECS/Fargate: blue/greenデプロイメント
- Rails: 6
- MySQL: 8
インフラ構成図
下記通り、ECS/Fargateに関連するリソース一式をTerraformでコードから生成します。
作業内容
- 基礎部
- ネットワーク
- セキュリティグループ
- EC2/ RDS
- ALB/ Route53/ ACM
- ECS/ SystemsManager
- CloudFront/ S3
1. 基礎部
まず、Terraform/プロバイダーのバージョン指定や変数などの
基盤部分をmain.tfでまとめます。
# -------------------------------------------
# Terraform configuration
# -------------------------------------------
terraform {
required_version = ">=1.1"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>3.0"
}
}
}
# -------------------------------------------
# Provider
# -------------------------------------------
provider "aws" {
profile = "terraform"
region = "ap-northeast-1"
}
provider "aws" {
alias = "virginia"
profile = "terraform"
region = "us-east-1"
}
# -------------------------------------------
# Variables
# -------------------------------------------
variable "tool" {
type = string
}
variable "project" {
type = string
}
variable "environment" {
type = string
}
続いて、terraform.tfvarsに、変数に代入する値を
各自でアプリに合わせて入力してください。
私の場合は、AWSマネジメントコンソールからGUIで作成したECS/Fargateと 新しく作成するTerraformベースのECS/Fargateを区別するために
toolにterraformという値を設定しています。
tool = "terraform"
project = "アプリ名"
environment = "production"
また、modulesには、IAMロールだけ書いています。
(後の実装で役立ってきますが、正直2つのIAMロールしか作成しませんので
活用されている訳ではないです。)
module無しで記述しても労力的には変化ないと思いますが
個人的な可読性とmoduleを使ってみたいという気持ちで
採用してますので、ご了承ください。
variable "name" {}
variable "policy" {}
variable "identifier" {}
resource "aws_iam_role" "default" {
name = var.name
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
data "aws_iam_policy_document" "assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [var.identifier]
}
}
}
resource "aws_iam_policy" "default" {
name = var.name
policy = var.policy
}
resource "aws_iam_role_policy_attachment" "default" {
role = aws_iam_role.default.name
policy_arn = aws_iam_policy.default.arn
}
output "iam_role_arn" {
value = aws_iam_role.default.arn
}
output "iam_role_name" {
value = aws_iam_role.default.name
}
そして、terraformディレクトリに.gitignoreを新たに配置しています。
複数.gitignoreが存在する場合は、最下層の.gitignoreが優先されます。
コード自体は、gitignore ioでterraformを検索して
テンプレートを貼り付けているだけです。
# Created by https://www.toptal.com/developers/gitignore/api/terraform
# Edit at https://www.toptal.com/developers/gitignore?templates=terraform
### Terraform ###
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
# Exclude all .tfvars files, which are likely to contain sentitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
#
*.tfvars
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
# End of https://www.toptal.com/developers/gitignore/api/terraform
以上を踏まえて、最終的なディレクトリ構造は下記の通りです。
(これから各リソースのコードを書いていくと同ディレクトリ構造になります。)
% tree terraform
terraform
├── acm.tf
├── cloudfront.tf
├── ec2.tf
├── ecs.tf
├── elb.tf
├── main.tf
├── modules
│ ├── iam_role
│ └── main.tf
├── network.tf
├── rds.tf
├── route53.tf
├── s3.tf
├── security_group.tf
├── ssm.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── terraform.tfvars
2. ネットワーク
では、各リソースの土台となるネットワーク部分から作成していきましょう。
まず、VPC/SubnetなどのリソースをCidrブロックで構想します。
VPC:ネットワーク全体
VPC名称 | CIDR | アドレス範囲 |
---|---|---|
app-production-vpc | 10.0.0.0/16 | 10.0.0.0 - 10.0.255.255 |
サブネット:リソースごとの領域
サブネット名称 | CIDR | アドレス範囲 | region | 用途 | ルートテーブル |
---|---|---|---|---|---|
ecs-1a-subnet | 10.0.1.0/24 | 10.0.1.0 - 10.0.1.255 | ap-northeast-1a | ECS(ALB/fargate) | public-rt |
ecs-1c-subnet | 10.0.2.0/24 | 10.0.2.0 - 10.0.2.255 | ap-northeast-1c | ECS(ALB/fargate) | public-rt |
db-1a-subnet | 10.0.11.0/24 | 10.0.11.0 - 10.0.11.255 | ap-northeast-1a | DB | private-rt |
db-1c-subnet | 10.0.12.0/24 | 10.0.12.0 - 10.0.12.255 | ap-northeast-1c | DB | private-rt |
management-ec2-1a-subnet | 10.0.21.0/24 | 10.0.21.0 - 10.0.21.255 | ap-northeast-1a | 管理用EC2 | public-rt |
management-ec2-1c-subnet | 10.0.22.0/24 | 10.0.22.0 - 10.0.22.255 | ap-northeast-1c | 管理用EC2 | public-rt |
インターネットゲートウェイ:インターネットに繋がる唯一の出入り口
インターネットゲートウェイ名称 |
---|
igw |
ルートテーブル:サブネットの通信経路を決める
ルートテーブル名称 | 送信先 | ターゲット |
---|---|---|
public-rt | 10.0.0.0/24, 0.0.0.0/0 | local, igw |
private-rt | 10.0.0.0/24 | local |
それでは、構想したものをterraformコードに落とし込んでいきます。
terraformコードは、HCL(HashiCorpLanguage)という文法に基づいています。
HCLについて、全ては解説しませんので、ご了承ください。
resourceブロックでは、resourceに続く、
1つ目の””で囲われた文字列は生成するAWSリソースを
2つ目の””で囲われた文字列はterraform内の識別子を
記述しています。
※terraform内の識別子は、自分の好きな名称を決められます。
なお、resourceブロックで定義したリソースは他のresourceブロックから参照できます。
例えば、ECS用サブネットに着目すると、
vpc_id =
の後にaws_vpc.vpc.id
とVPCのリソースを参照しています。
さらにid
の箇所は属性といい、各リソースによって使用できるものが異なります。
詳細を知りたい場合は、Terraformの公式ページから確認します。
一例ではありますが、VPCを生成するaws_vpcというリソース
であれば
下記のように使える属性を調べます。
では、AWSマネジメントコンソールからGUIで
ネットワークの各リソースを関連づけしていたように
terraformコードで、各リソースを紐付けるように記述していきます。
# -------------------------------------------
# VPC
# -------------------------------------------
resource "aws_vpc" "vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-vpc"
}
}
# -------------------------------------------
# Subnet (マルチAZ)
# -------------------------------------------
### ECS用サブネット
resource "aws_subnet" "ecs_public_subnet_1a" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1a"
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-ecs-public-subnet-1a"
}
}
resource "aws_subnet" "ecs_public_subnet_1c" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1c"
cidr_block = "10.0.2.0/24"
map_public_ip_on_launch = true
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-ecs-public-subnet-1c"
}
}
### DB用サブネット
resource "aws_subnet" "db_private_subnet_1a" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1a"
cidr_block = "10.0.11.0/24"
map_public_ip_on_launch = false
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-db-private-subnet-1a"
}
}
resource "aws_subnet" "db_private_subnet_1c" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1c"
cidr_block = "10.0.12.0/24"
map_public_ip_on_launch = false
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-db-private-subnet-1c"
}
}
### マネジメントEC2用サブネット
resource "aws_subnet" "management_ec2_public_subnet_1a" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1a"
cidr_block = "10.0.21.0/24"
map_public_ip_on_launch = true
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-public-subnet-1a"
}
}
resource "aws_subnet" "management_ec2_public_subnet_1c" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1c"
cidr_block = "10.0.22.0/24"
map_public_ip_on_launch = true
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-public-subnet-1c"
}
}
# -------------------------------------------
# InternetGateway
# -------------------------------------------
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-igw"
}
}
# -------------------------------------------
# RootTable
# -------------------------------------------
### Public
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-public-rt"
}
}
# Publicには、インターネットゲートウェイに繋がるルートを追加する
resource "aws_route" "public_rt_igw_r" {
route_table_id = aws_route_table.public_rt.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
resource "aws_route_table_association" "ecs_public_rt_1a" {
route_table_id = aws_route_table.public_rt.id
subnet_id = aws_subnet.ecs_public_subnet_1a.id
}
resource "aws_route_table_association" "ecs_public_rt_1c" {
route_table_id = aws_route_table.public_rt.id
subnet_id = aws_subnet.ecs_public_subnet_1c.id
}
resource "aws_route_table_association" "management_ec2_public_rt_1a" {
route_table_id = aws_route_table.public_rt.id
subnet_id = aws_subnet.management_ec2_public_subnet_1a.id
}
resource "aws_route_table_association" "management_ec2_public_rt_1c" {
route_table_id = aws_route_table.public_rt.id
subnet_id = aws_subnet.management_ec2_public_subnet_1c.id
}
### Private
resource "aws_route_table" "private_rt" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-private-rt"
}
}
resource "aws_route_table_association" "db_private_rt_1a" {
route_table_id = aws_route_table.private_rt.id
subnet_id = aws_subnet.db_private_subnet_1a.id
}
resource "aws_route_table_association" "db_private_rt_1c" {
route_table_id = aws_route_table.private_rt.id
subnet_id = aws_subnet.db_private_subnet_1c.id
}
ここまで、記述できたらAWSリソース生成のために
ターミナルから下記terraformコマンドを実行します。
(次章以降、当コマンドの実行は重複するため原則省略します。)
# Terraformの初期化
% terraform init
# コードを整列するようフォーマットする
% terraform fmt
# 現コードで生成されるリソースを確認
% terraform plan
# リソース生成の実行
% terraform apply -auto-approve
問題なく処理が完了したら、マネジメントコンソール画面から確認してみましょう。
3. セキュリティグループ
続いて、AWSサービスのファイアーウォールにあたる
セキュリティグループを設定していきます。
セキュリティグループでは、リソースへの接続を制限する
インバウンドルールに注意して設定していきます。
(アウトバウンドルールは制限をかけていません。)
セキュリティグループ名称 | タイプ | ソース | 用途 |
---|---|---|---|
ecs-sg | HTTP, HTTPS, カスタムTCP | 0.0.0.0, 0.0.0.0, management-ec2-sg | ALB/fargate |
db-sg | MySQL | ecs-sg, management-ec2-sg | DB |
management-ec2-sg | SSH | 0.0.0.0 | 管理用EC2 |
ECSは、ユーザーからのアクセスが想定されるために、
HTTP/HTTPSのプロトコルで全IPアドレスを許可する設定にします。
DBは、ECSと管理用EC2のみアクセスできるように
2つのセキュリティグループに限定して許可する設定とします。
管理用EC2には、SSHプロトコルのみ接続が可能とします。
# -------------------------------------------
# SecurtyGroup
# -------------------------------------------
### 1.ECS用 ###
resource "aws_security_group" "ecs_sg" {
name = "ecs-sg"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-ecs-sg"
}
}
resource "aws_security_group_rule" "ecs_ingress_http" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.ecs_sg.id
}
resource "aws_security_group_rule" "ecs_ingress_https" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.ecs_sg.id
}
resource "aws_security_group_rule" "ecs_ingress_custom_tcp" {
type = "ingress"
from_port = 10080
to_port = 10080
protocol = "tcp"
source_security_group_id = aws_security_group.management_ec2_sg.id
security_group_id = aws_security_group.ecs_sg.id
}
resource "aws_security_group_rule" "ecs_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.ecs_sg.id
}
### 2. DB用 ###
resource "aws_security_group" "db_sg" {
name = "db-sg"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-db-sg"
}
}
resource "aws_security_group_rule" "db_ingress_mysql_from_ecs" {
type = "ingress"
from_port = 3306
to_port = 3306
protocol = "tcp"
source_security_group_id = aws_security_group.ecs_sg.id
security_group_id = aws_security_group.db_sg.id
}
resource "aws_security_group_rule" "db_ingress_mysql_from_management_ec2" {
type = "ingress"
from_port = 3306
to_port = 3306
protocol = "tcp"
source_security_group_id = aws_security_group.management_ec2_sg.id
security_group_id = aws_security_group.db_sg.id
}
resource "aws_security_group_rule" "db_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.db_sg.id
}
### 3.マネジメントEC2用 ###
resource "aws_security_group" "management_ec2_sg" {
name = "management-ec2-sg"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-sg"
}
}
resource "aws_security_group_rule" "management_ec2_ingress_ssh" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.management_ec2_sg.id
}
resource "aws_security_group_rule" "management_ec2_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.management_ec2_sg.id
}
では、terraformコマンドを実行して、リソースが追加で
生成できたかをマネジメントコンソールで確認します。
4. EC2/ RDS
次は、EC2とRDSをネットワークに配置していきましょう。
⑴ EC2
では、EC2から着手していきます。
EC2本体のコードを書く前に
- AMI
- key pair
を先に準備していきます。
はじめに**AMI
**から準備します。
ただ、AMIはマネジメントコンソールからAMIを特定するIDをコピーして
直接EC2のamiに貼り付けても問題はありません。
当記事では、dataブロックを用いて
AWS外部データにアクセスして、指定条件からamiを特定し
それをEC2のamiに紐づける方法で実装しています。
最初にamiの詳細情報を調べるためにaws cliを使います。
ターミナルから下記コマンドを実行して詳細情報を確認しましょう。
# acmの詳細情報を表示する
% aws ec2 describe-images --image-ids ami-0218d08a1f9dac831
{
"Images": [
{
----省略----
### nameフィルターで使用
"Description": "Amazon Linux 2 Kernel 5.10 AMI 2.0.20211201.0 x86_64 HVM gp2",
"EnaSupport": true,
"Hypervisor": "xen",
"ImageOwnerAlias": "amazon",
"Name": "amzn2-ami-kernel-5.10-hvm-2.0.20211201.0-x86_64-gp2",
"RootDeviceName": "/dev/xvda",
### root-device-typeフィルターで使用
"RootDeviceType": "ebs",
"SriovNetSupport": "simple",
### virtualization-typeフィルターで使用
"VirtualizationType": "hvm"
}
]
}
では、さきほど調べた3つの情報を各filterに設定していきます。
なお、補足ですが
most_recentは最新版を選択する設定で、
更に3つあるfilterのうち、nameは
*(ワイルドカード)を用いて、どの日付でもヒットするよう条件を緩めています。
これによって、複数ヒットするamiから最新版を選んでくれる
と設定になります。
### AMI ###
data "aws_ami" "management_ec2_ami" {
most_recent = true
owners = ["self", "amazon"]
filter {
name = "name"
values = ["amzn2-ami-kernel-5.10-hvm-2.0.*-x86_64-gp2"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
続いて、**key pair
**です。
マネジメントコンソールからGUI操作で構築していた時
は
最初に、AWS側にキーペア(公開鍵+秘密鍵)を生成し、
そのキーペアのうち、秘密鍵(~.pem)をローカルに渡して
ローカルからAWSリソースへSSHで接続可能としていました。
Terraformから生成する場合
は
ローカルでキーペア(公開鍵+秘密鍵)を作成して
キーペアのうち、公開鍵(~.pub)をAWSに登録します。
いずれも最終的には
- AWS: 公開鍵
- ローカル: 秘密鍵
という状態になります。
では、最初にターミナルからキーペアを作成します。
# 既存のSSHキーが存在するか確認
% ls -al ~/.ssh
# 未使用の無駄なSSHキーがあれば削除しておく
% rm -rf ~/.ssh/未使用のキー
# RSA4096ビット形式のSSHキーを作成する
% ssh-keygen -t rsa -b 4096
↓ 質問に答えていく
# SSHキーの保存先パスとファイル名を決める
# -> そのままEnterと押すと()内のデフォルト値で生成される
# ※ 今回は「keypair_aws」としている
Enter file in which to save the key (/Users/ユーザー名/.ssh/id_rsa): /Users/ユーザー名/.ssh/keypair_aws
# SSHキーのパスフレーズ設定
Enter passphrase (empty for no passphrase): 任意のパスワードを入力
# パスフレーズの再確認
Enter same passphrase again: 再度パスワードを入力
↓ 入力が完了すると、秘密鍵と公開鍵が生成される
Your identification has been saved in /Users/ユーザー名/.ssh/keypair_aws.
Your public key has been saved in /Users/ユーザー名/.ssh/keypair_aws.pub.
# 生成されたSSHキーの確認
% ls -al ~/.ssh
lsコマンドで表示されるファイルのうち
.pubの拡張子がついているファイルは公開鍵を指します。 何もついていない鍵は秘密鍵ですので、拡張子「.pem」を付けておきましょう。
それでは、生成したキーペアの公開鍵(~.pub)を
terraformコードで、AWSに登録していきます。
### key pair ###
resource "aws_key_pair" "keypair" {
key_name = "${var.tool}-${var.project}-${var.environment}-management-ec2-keypair"
public_key = file("~/.ssh/keypair_aws.pub")
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-keypair"
}
}
では、EC2本体を実装していきます。
先ほど設定したAMIとkeypairをEC2に紐づける他に、aws_eipで
ElasticIPをEC2に関連づけてIPアドレスを固定化させています。
最後のoutputは、自動生成されたEC2のパブリックIPアドレスを出力しています。
SSH接続時に必要なIPアドレスをコードからも確認できる仕組みとしています。
# -------------------------------------------
# Management EC2
# -------------------------------------------
### AMI ###
data "aws_ami" "management_ec2_ami" {
most_recent = true
owners = ["self", "amazon"]
filter {
name = "name"
values = ["amzn2-ami-kernel-5.10-hvm-2.0.*-x86_64-gp2"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
### key pair ###
resource "aws_key_pair" "keypair" {
key_name = "${var.tool}-${var.project}-${var.environment}-management-ec2-keypair"
public_key = file("~/.ssh/keypair_aws.pub")
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-keypair"
}
}
### EC2 instance ###
resource "aws_instance" "management_ec2" {
ami = data.aws_ami.management_ec2_ami.id
instance_type = "t2.micro"
subnet_id = aws_subnet.management_ec2_public_subnet_1a.id
associate_public_ip_address = true
vpc_security_group_ids = [aws_security_group.management_ec2_sg.id]
key_name = aws_key_pair.keypair.key_name
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-management-ec2"
}
}
### Elastic IP ###
resource "aws_eip" "management_ec2_eip" {
vpc = true
instance = aws_instance.management_ec2.id
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-eip"
}
}
output "ElasticIP" {
value = aws_eip.management_ec2_eip.public_ip
}
⑵ RDS
続いて、RDSです。
一部のみ解説いたします。
パラメータグループは、MySQLのデフォルトでセットされていない
日本語に対応させるため、文字コードにutf8mb4を指定しています。
サブネットグループは、RDS生成時には必須である為
作成済みのサブネットをまとめる設定をしています。
random_stringは、ランダムな文字列を生成できるTerraformのツールです。
ここでは、記号を使用しない16文字で生成するルールとしています。
パスワードはgitignoreで無視させているterraform.tfstateに出力されます。
**identifier
は、AWSで生成されるDBの名称
**で
**name
は、MySQLで生成されるDBの名称
**です。
(RailsがMySQLに接続する時はnameで定めた文字列をdatabase.ymlに定義しておく必要がある)
deletion_protectionは削除保護になりますので
むやみにDBを削除させない為にはtrue
としておいた方が安全でしょう。
私の場合は、まだポートフォリオが完成していないので
削除しやすいようにfalse
としています。
# -------------------------------------------
# ParameterGroup
# -------------------------------------------
resource "aws_db_parameter_group" "mysql_parametergroup" {
name = "${var.tool}-${var.project}-${var.environment}-mysql-parametergroup"
family = "mysql8.0"
parameter {
name = "character_set_database"
value = "utf8mb4"
}
parameter {
name = "character_set_server"
value = "utf8mb4"
}
}
# -------------------------------------------
# SubnetGroup
# -------------------------------------------
resource "aws_db_subnet_group" "mysql_subnet_group" {
name = "${var.tool}-${var.project}-${var.environment}-mysql-subnetgroup"
subnet_ids = [
aws_subnet.db_private_subnet_1a.id,
aws_subnet.db_private_subnet_1c.id
]
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-mysql-subnetgroup"
}
}
# -------------------------------------------
# RDS instance
# -------------------------------------------
resource "random_string" "db_password" {
length = 16
special = false
}
resource "aws_db_instance" "mysql" {
engine = "mysql"
engine_version = "8.0.27"
# AWSで作成されるデータベース名
identifier = "${var.tool}-${var.project}-${var.environment}-mysql"
username = "admin"
password = random_string.db_password.result
instance_class = "db.t2.micro"
allocated_storage = 20
max_allocated_storage = 50
storage_type = "gp2"
storage_encrypted = false
multi_az = false
availability_zone = "ap-northeast-1a"
db_subnet_group_name = aws_db_subnet_group.mysql_subnet_group.name
vpc_security_group_ids = [aws_security_group.db_sg.id]
publicly_accessible = false
port = 3306
# MySQLで作成されるデータベース名
name = "${var.project}_db"
parameter_group_name = aws_db_parameter_group.mysql_parametergroup.name
backup_window = "04:00-05:00"
backup_retention_period = 7
maintenance_window = "Mon:05:00-Mon:06:00"
auto_minor_version_upgrade = false
deletion_protection = false
skip_final_snapshot = true
apply_immediately = true
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-mysql"
}
}
output "rds_endpoint" {
value = aws_db_instance.mysql.address
}
ここまで完了できたら、これまでと同様にterraformコマンドで
現時点のリソースを反映させましょう。
5. ALB/ Route53/ ACM
続いて、
・通信をリスナーごとに振り分けるロードバランサーのALB
・名前解決を行うDNSサーバーのRoute53
・HTTPS通信で必要になる証明書のACM
を生成していきます。
⑴ ALB
では、まずALBから実装していきましょう。
ここでも一部のみ解説します。
enable_deletion_protectioは削除保護をする設定ですが
今回はコメントアウトで設定していませんので
terraform destroyコマンドで消しやすいようにしています。
access_logsは、後で実装予定のアクセスログ用S3が関わってきますので
現時点はコメントアウトしています。
そして、ポイントとなるリスナーとターゲットグループ
は
以下の通りで作成していきます。
リスナー名称 | ポート番号 | デフォルトアクション | 用途 | 備考 |
---|---|---|---|---|
http | 80 | redirect | リダイレクト専用 | 80ポートを443ポートにリダイレクトさせる |
https | 443 | forward | 本番用リスナー | 443ポートからアクセスしたユーザー |
custom_10080 | 10080 | forward | テスト用リスナー | 10080ポートからアクセスしたテストユーザー |
ターゲットグループ名称 | ポート番号 | 用途 |
---|---|---|
blue_tg | 443 | blue/greenデプロイメントの転送先 |
green_tg | 10080 | blue/greenデプロイメントの転送先 |
通信の流れを理解する為に
リスナー: 一般/テストユーザーなどの外から通信を受け付ける対象
ターゲットグループ: 受け取った通信を流すネットワーク内の場所
でイメージを捉えておくと良いと思います。
なお、health_checkのpathは、アプリ構成によって異なる箇所です。
ご自身のヘルスチェックを行うパスに合わせましょう。
# -------------------------------------------
# ALB
# -------------------------------------------
### ALB本体 ###
resource "aws_lb" "alb" {
name = "${var.tool}-${var.project}-${var.environment}-alb"
load_balancer_type = "application"
internal = false
# 削除保護は開発時には設定しない
# enable_deletion_protection = true
subnets = [
aws_subnet.ecs_public_subnet_1a.id,
aws_subnet.ecs_public_subnet_1c.id,
]
### アクセスログ用のS3
#access_logs {
# bucket = aws_s3_bucket.alb_accesslogs_bucket.id
# enabled = true
#}
security_groups = [
aws_security_group.ecs_sg.id
]
}
output "alb_dns_name" {
value = aws_lb.alb.dns_name
}
### リスナー ###
## 1.HTTPリスナー ##
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
## 2.プロダクションリスナー(HTTPS) ##
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.alb.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = aws_acm_certificate.tokyo_cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.blue_tg.arn
}
}
## 3.テストリスナー(カスタムTCP:10080) ##
resource "aws_lb_listener" "custom_10080" {
load_balancer_arn = aws_lb.alb.arn
port = "10080"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.green_tg.arn
}
}
# -------------------------------------------
# TargetGroup
# -------------------------------------------
### 1.blue target group ###
resource "aws_lb_target_group" "blue_tg" {
name = "${var.tool}-${var.project}-${var.environment}-blue-tg"
target_type = "ip"
vpc_id = aws_vpc.vpc.id
port = 80
protocol = "HTTP"
deregistration_delay = 300
health_check {
path = "/api/healthcheck"
healthy_threshold = 3
unhealthy_threshold = 2
timeout = 5
interval = 15
matcher = 200
port = "traffic-port"
protocol = "HTTP"
}
depends_on = [aws_lb.alb]
}
### 2.green target group ###
resource "aws_lb_target_group" "green_tg" {
name = "${var.tool}-${var.project}-${var.environment}-green-tg"
target_type = "ip"
vpc_id = aws_vpc.vpc.id
port = 10080
protocol = "HTTP"
deregistration_delay = 300
health_check {
path = "/api/healthcheck"
healthy_threshold = 3
unhealthy_threshold = 2
timeout = 5
interval = 15
matcher = 200
port = "traffic-port"
protocol = "HTTP"
}
depends_on = [aws_lb.alb]
}
⑵ Route53
次はRoute53の実装です。
私は、AWSのRoute53からドメインを購入しています
。
もし異なるレジストラからドメインを取得している方は ホストゾーンなど少しだけ実装が違うかもしれません。
ここでは、
DNSサーバーを担うAWSサービスのRoute53
に
ドメイン名をもとにアクセスしてきた通信をALBに向ける
という
名前解決
の設定を行います。
なお、a_record_cloudfrontは、後の実装で関わってくる
CloudFront用のレコードなので、現時点ではコメントアウトしています。
表でまとめると下記となります。
レコード名称 | ドメイン | 対象リソース |
---|---|---|
a_record_alb | Route53で取得したドメイン名 | ALB |
a_record_cloudfront | ホスト名.Route53で取得したドメイン名 | CloudFront |
また、route53_acm_dns_resolveは
証明書ACMで使うDNS検証用のレコード設定です。
次章で説明しますが、terraform公式コードを参考にしています。
# -------------------------------------------
# Route53
# -------------------------------------------
### ドメインの取得(レジストリ:Route53) ###
data "aws_route53_zone" "hostzone" {
name = "Route53で取得したドメイン名"
}
### Aレコード:ALB ###
resource "aws_route53_record" "a_record_alb" {
zone_id = data.aws_route53_zone.hostzone.zone_id
name = data.aws_route53_zone.hostzone.name
type = "A"
alias {
name = aws_lb.alb.dns_name
zone_id = aws_lb.alb.zone_id
evaluate_target_health = true
}
}
output "domain_name" {
value = aws_route53_record.a_record_alb.name
}
### 後の実装
### Aレコード:CloudFront ###
#resource "aws_route53_record" "a_record_cloudfront" {
# zone_id = data.aws_route53_zone.hostzone.zone_id
# name = "ホスト名.Route53で取得したドメイン名"
# type = "A"
# alias {
# name = aws_cloudfront_distribution.cf.domain_name
# zone_id = aws_cloudfront_distribution.cf.hosted_zone_id
# evaluate_target_health = true
# }
#}
### 証明書のDNS検証用レコード ###
resource "aws_route53_record" "route53_acm_dns_resolve" {
for_each = {
for dvo in aws_acm_certificate.tokyo_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
type = dvo.resource_record_type
record = dvo.resource_record_value
}
}
allow_overwrite = true
zone_id = data.aws_route53_zone.hostzone.zone_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
⑶ ACM
続いて、ACMの作成です。
ACM証明書は、以下の通りで2つ発行します。
ACM名称 | リージョン | 用途 | 備考 |
---|---|---|---|
tokyo_cert | ap-northeast-1 | ALB | デフォルトのリージョン |
virginia_cert | us-east-1 | CloudFront | providerのエイリアスを用いてリージョン切り替え |
CloudFront用のvirginia_certは、東京リージョンでは作成できません。
そこでmain.tfに記述しているproviderのaliasで工夫し、
リージョンを切替しています。
なお、ACM証明書のDNS検証については
Terraform公式ページにコードが記述されていますので、ご確認ください。
----省略----
# -------------------------------------------
# Provider
# -------------------------------------------
provider "aws" {
profile = "terraform"
region = "ap-northeast-1"
}
provider "aws" {
alias = "virginia"
profile = "terraform"
region = "us-east-1"
}
----省略----
# -------------------------------------------
# AWS Certificate Manager
# -------------------------------------------
### 1、ALB用のHTTPS証明書 ###
resource "aws_acm_certificate" "tokyo_cert" {
domain_name = data.aws_route53_zone.hostzone.name
validation_method = "DNS"
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-alb-sslcert"
}
lifecycle {
create_before_destroy = true
}
}
### 証明書のDNS検証 ###
resource "aws_acm_certificate_validation" "cert_valid" {
certificate_arn = aws_acm_certificate.tokyo_cert.arn
validation_record_fqdns = [for record in aws_route53_record.route53_acm_dns_resolve : record.fqdn]
}
### 2、CloudFront用のHTTPS証明書 ###
resource "aws_acm_certificate" "virginia_cert" {
provider = aws.virginia
domain_name = "*.取得したドメイン名"
validation_method = "DNS"
tags = {
Name = "${var.tool}-${var.project}-${var.environment}-cloudfront-sslcert"
}
lifecycle {
create_before_destroy = true
}
}
ここまでできたら、terraformコマンドを実行してリソースを生成します。
参考まで:(検証作業)
なお、リソース生成後、リスナー側を検証したい場合
defaullt_actionをfixed-responseにして
プレーンテキストが返されるか見てみるのも良いと思います。
検証は、ターミナルからcurlコマンドを実行すると出来ます。
### リスナー ###
## 2.プロダクションリスナー(HTTPS) ##
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.alb.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = aws_acm_certificate.tokyo_cert.arn
## 一時的にコメントアウト
#default_action {
# type = "forward"
# target_group_arn = aws_lb_target_group.blue_tg.arn
#}
## 検証: プレーンテキストがレスポンスされるか
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "HTTPのテスト"
status_code = "200"
}
}
}
% curl https://ドメイン名
6. SystemsManager/ ECS
それでは、
・秘匿情報を管理するSystemsManager
・コンテナを制御するECS(cluster, service, task-def)
を作成していきましょう。
⑴ SystemsManager
まず、SystemsManagerから実装します。
ここでは、RailsからMySQLに接続するに必要な3つの情報を
SystemsManagerに登録しています。
名称 | Key値 | Value値 | 備考 |
---|---|---|---|
db_username | "/container-param/db-username" | admin | そのまま使用 |
db_password | "/container-param/db-password" | uninitialized | aws cliで書き換え |
db_host | "/container-param/db-host" | uninitialized | aws cliで書き換え |
全てタイプはSecureString
です。
Value値について補足ですが、
db_username
は「admin」をそのままの値として利用し、
db_password, db_host
は仮の値として「uninitialized」とし、 SystemsManager登録後に、ターミナルからaws cliで値を上書きします。
# -------------------------------------------
# Systems Manager
# -------------------------------------------
resource "aws_ssm_parameter" "db_username" {
name = "/container-param/db-username"
value = "admin"
type = "SecureString"
description = "MySQLのユーザー名"
lifecycle {
ignore_changes = [value]
}
}
resource "aws_ssm_parameter" "db_password" {
name = "/container-param/db-password"
value = "uninitialized"
type = "SecureString"
description = "AWSCLIを用いて初期値から変更"
lifecycle {
ignore_changes = [value]
}
}
resource "aws_ssm_parameter" "db_host" {
name = "/container-param/db-host"
value = "uninitialized"
type = "SecureString"
description = "AWSCLIを用いて初期値から変更"
lifecycle {
ignore_changes = [value]
}
}
ここまでで、terraformコマンドを実行してリソースを生成しましょう。
リソース生成後に、ターミナルからaws cliを実行して SystemsManagerのパラメーターストアに登録している2つ値を上書きします。
なお、RDSパスワードはrandom_stringが自動生成しています。
その為、VScode
でterraform.tfstate
ファイル内を
password
で検索をかければ、パスワードが表示できるはずです。
RDSエンドポイントは、マネジメントコンソールでもterraform.tfstateでも どちらでもいいのでコピペしてください。
% aws ssm put-parameter --name '/container-param/db-password' --type SecureString --value 'RDSパスワードの値' --overwrite
% aws ssm put-parameter --name '/container-param/db-host' --type SecureString --value 'RDSエンドポイントの値' --overwrite
それでは、値が更新できているかをマネジメントコンソールの
SystemsManagerのパラメータストアから確認しましょう。
db-host, db-passwordをクリックして、保存されている値が
先ほど、ターミナルから入力した値を合致していればOKです。
⑵ ECS
ここから、ECSの設定に入ります。
コードがかなり膨大なので、一部だけ解説します。
desired_countは、ECSで起動させるタスク数です。
当記事では1としておりますが、このままでは障害が発生した時に
再起動まで時間が掛かる為、可用性を意識するなら2以上が望ましいでしょう。
deployment_controllerは、blue/greenデプロイメントを実現する為に
AWSのCodeDeployを指定しています。
Terraform公式ページにコードが載っていますので、ご確認ください。
container_definitionsは、
file参照ではなく、jsonencodeで各項目を直接記述しています。
secretsは、
先ほどSystemsManagerに登録した情報を読み取り、 コンテナ(Rails)の環境変数に埋め込んでいます。
ecs_task_execution_role と ecs_code_deploy_role は
IAMロールで、モジュールを用いて、それぞれ作成しています。
1つ目のdataブロックでAWS公式のIAMロールを参照し、
2つ目のdataブロックでポリシードキュメントとして定義しておき
最後にmoduleで同ポリシードキュメントを紐づけています。
(タスク実行ロールは、statementでSystemsManagerへのアクセス権限を付けています。)
# -------------------------------------------
# ECS
# -------------------------------------------
### クラスター ###
resource "aws_ecs_cluster" "cluster" {
name = "${var.tool}-${var.project}-${var.environment}-ecs-backend-rails-cluster"
}
### サービス ###
resource "aws_ecs_service" "service" {
name = "${var.tool}-${var.project}-${var.environment}-ecs-backend-rails-service"
cluster = aws_ecs_cluster.cluster.arn
task_definition = aws_ecs_task_definition.task_def.arn
# 最終的に可用性を上げるために2に変更する
desired_count = 1
launch_type = "FARGATE"
platform_version = "1.4.0"
health_check_grace_period_seconds = 180
network_configuration {
assign_public_ip = true
security_groups = [aws_security_group.ecs_sg.id]
subnets = [
aws_subnet.ecs_public_subnet_1a.id,
aws_subnet.ecs_public_subnet_1c.id
]
}
load_balancer {
target_group_arn = aws_lb_target_group.blue_tg.arn
container_name = "自分のコンテナ名"
container_port = 80
}
deployment_controller {
type = "CODE_DEPLOY"
}
lifecycle {
ignore_changes = [task_definition, load_balancer]
}
depends_on = [aws_db_instance.mysql]
}
### タスク定義 ###
resource "aws_ecs_task_definition" "task_def" {
family = "${var.tool}-${var.project}-${var.environment}-ecs-backend-rails-task-def"
cpu = 256
memory = 512
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
execution_role_arn = module.ecs_task_execution_role.iam_role_arn
container_definitions = jsonencode([
{
name = "自分のコンテナ名"
image = aws_ecr_repository.backend.repository_url
essential = true
portMappings = [
{
containerPort = 80
hostPort = 80
}
]
logConfiguration = {
logDriver : "awslogs",
options : {
awslogs-region : "ap-northeast-1",
awslogs-stream-prefix : "backend",
awslogs-group : aws_cloudwatch_log_group.ecs_backend.name
}
},
secrets = [
{
name = "DB_USERNAME",
valueFrom = "/container-param/db-username"
},
{
name = "DB_PASSWORD",
valueFrom = "/container-param/db-password"
},
{
name = "DB_HOST",
valueFrom = "/container-param/db-host"
}
]
}
])
}
### 1.IAMロール:タスク実行ロール ###
data "aws_iam_policy" "ecs_task_execution_role_policy" {
arn = "arn:aws:iam::aws:policy/service-role/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"]
resources = ["*"]
}
}
module "ecs_task_execution_role" {
source = "./modules/iam_role"
name = "ecs-task-execution"
identifier = "ecs-tasks.amazonaws.com"
policy = data.aws_iam_policy_document.ecs_task_execution.json
}
### 2.IAMロール:CodeDeployロール ###
data "aws_iam_policy" "ecs_code_deploy_role_policy" {
arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}
data "aws_iam_policy_document" "ecs_code_deploy" {
source_json = data.aws_iam_policy.ecs_code_deploy_role_policy.policy
}
module "ecs_code_deploy_role" {
source = "./modules/iam_role"
name = "ecs-code-deploy"
identifier = "codedeploy.amazonaws.com"
policy = data.aws_iam_policy_document.ecs_code_deploy.json
}
### CodeDeploy ###
resource "aws_codedeploy_app" "codedeploy_app" {
compute_platform = "ECS"
name = "${var.tool}-${var.project}-${var.environment}-codedeploy-app"
}
resource "aws_codedeploy_deployment_group" "codedeploy_dg" {
app_name = aws_codedeploy_app.codedeploy_app.name
deployment_group_name = "${var.tool}-${var.project}-${var.environment}-codedeploy-dg"
service_role_arn = module.ecs_code_deploy_role.iam_role_arn
deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE"]
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "CONTINUE_DEPLOYMENT"
}
terminate_blue_instances_on_deployment_success {
action = "TERMINATE"
termination_wait_time_in_minutes = 5
}
}
deployment_style {
deployment_option = "WITH_TRAFFIC_CONTROL"
deployment_type = "BLUE_GREEN"
}
ecs_service {
cluster_name = aws_ecs_cluster.cluster.name
service_name = aws_ecs_service.service.name
}
load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = [aws_lb_listener.https.arn]
}
target_group {
name = aws_lb_target_group.blue_tg.name
}
target_group {
name = aws_lb_target_group.green_tg.name
}
}
}
}
### ECR ###
resource "aws_ecr_repository" "backend" {
name = "${var.tool}-${var.project}-${var.environment}-backend-rails-repo"
}
### CloudWatch Logs ###
resource "aws_cloudwatch_log_group" "ecs_backend" {
name = "/${var.tool}-${var.project}-${var.environment}/ecs/backend"
retention_in_days = 180
}
ここまでできたら、同じようにterraformコマンドを実行してみましょう。
リソース生成後の動作検証は**ECS起動の確認作業**を実施してください。
(番外) ECS Exec
ECS/Fargate構成で生成したコンテナをローカルから操作する為の
追加実装なので、こちらは必要であれば実装してください。
私はコンテナからRaisのマイグレーション作業など試したかったので
実装しました。
それでは、最初はECSのサービスで
enable_execute_commandをtrueとして
execute_commandコマンドが使用できるように有効化させます。
続いて、ECSのタスク定義で
task_role_arnに新しく作成するIAMロールを紐付けます。
最後にECSのタスク定義に紐付けるIAMロールを作成します。
ここではstatementでjsonコードを直接記述し、ポリシーを定めています。
(dataブロックでベースとなるポリシーは取得せずにコードで書いています。)
■ポリシー
### サービス ###
resource "aws_ecs_service" "service" {
----省略----
enable_execute_command = true
----省略----
}
### タスク定義 ###
resource "aws_ecs_task_definition" "task_def" {
----省略----
task_role_arn = module.ecs_task_role.iam_role_arn
----省略----
}
### 1.IAMロール:タスク実行ロール ###
----省略----
### 2.IAMロール:タスクロール ###
data "aws_iam_policy_document" "ecs_task" {
version = "2012-10-17"
statement {
effect = "Allow"
actions = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
]
resources = ["*"]
}
}
module "ecs_task_role" {
source = "./modules/iam_role"
name = "ecs-task"
identifier = "ecs-tasks.amazonaws.com"
policy = data.aws_iam_policy_document.ecs_task.json
}
### 3.IAMロール:CodeDeployロール ###
----省略----
7. CloudFront/ S3
続いて、
・コンテンツを高速配信するCloudFront
・画像やログを保管するS3
を作成します。
⑴ CloudFront
まずは、CloudFrontから実装します。
ここでも、一部のみの解説となります。
originのdomain_nameとorigin_idでは、接続先となる
パブリックアクセス用S3バケットを紐づけています。
origin_access_identityはS3にアクセスする為に設定が必要なものです。
別リソースaws_cloudfront_origin_access_identityで
定義しているものを参照しています。
default_cache_behaviorでは、
**どういうURLを受け付けて、どこに振り分けるか
**を設定する項目です。
ここでは、S3による静的コンテンツの表示を目的としている為 query_string, headers, cookies の情報は転送しないようにしており、 min_ttl以降でキャッシュの設定をしています。
restrictionsでは、ロケーションを指定してアクセスを制限できます。
ですが、ここでは制限をかけずに明記だけしています。
aliasesでは、取得したドメインにホスト名を付して定義しています。
viewer_certificateは、証明書の設定です。
以前にACMで作成した証明書virginia_certを関連付けしています。
# -------------------------------------------
# CloudFront
# -------------------------------------------
resource "aws_cloudfront_distribution" "cf" {
enabled = true
price_class = "PriceClass_All"
origin {
domain_name = aws_s3_bucket.public_access_bucket.bucket_regional_domain_name
origin_id = aws_s3_bucket.public_access_bucket.id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.cf_s3_origin_access_identity.cloudfront_access_identity_path
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = aws_s3_bucket.public_access_bucket.id
forwarded_values {
query_string = false
headers = []
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
compress = true
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
aliases = ["ホスト名.ドメイン名"]
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.virginia_cert.arn
minimum_protocol_version = "TLSv1.2_2019"
ssl_support_method = "sni-only"
}
}
resource "aws_cloudfront_origin_access_identity" "cf_s3_origin_access_identity" {
comment = "public access bucket identity"
}
⑵ S3
最後に、S3バケットを作成します。
作成するS3は次の2つです。
- Railsからの画像投稿を保存するS3
- ALBのアクセスログを保管するS3
1.は、Railsでcarrier_waveを使っている前提となりますが、
config.fog_directoryで、パブリックアクセス用のS3名
config.asset_hostで、保存先のURL名
を設定します。
if Rails.env.production?
CarrierWave.configure do |config|
config.fog_credentials = {
provider: 'AWS',
aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
region: 'ap-northeast-1'
}
config.fog_directory = 'パブリックアクセス用のS3名'
config.asset_host = 'https://ホスト名.ドメイン名'
end
end
2.は、elb.tfでコメントアウトしていたものをコメントインさせて
ALBにS3を紐付けして、アクセスログ用のS3も設定します。
# -------------------------------------------
# ALB
# -------------------------------------------
----省略----
### コメントイン
## アクセスログ用のS3
access_logs {
bucket = aws_s3_bucket.alb_accesslogs_bucket.id
enabled = true
}
}
S3の設定は以下のように行います。
# -------------------------------------------
# Public Access Bucket
# -------------------------------------------
resource "aws_s3_bucket" "public_access_bucket" {
bucket = "${var.tool}-${var.project}-${var.environment}-public-access-bucket"
force_destroy = true
acl = "public-read"
cors_rule {
allowed_origins = ["https://ドメイン名"]
allowed_methods = ["GET"]
allowed_headers = ["*"]
max_age_seconds = 3600
}
}
# -------------------------------------------
# ALB AccessLogs Bucket
# -------------------------------------------
resource "aws_s3_bucket" "alb_accesslogs_bucket" {
bucket = "${var.tool}-${var.project}-${var.environment}-alb-accesslogs-bucket"
force_destroy = true
lifecycle_rule {
enabled = true
expiration {
days = "180"
}
}
}
resource "aws_s3_bucket_policy" "alb_log" {
bucket = aws_s3_bucket.alb_accesslogs_bucket.id
policy = data.aws_iam_policy_document.alb_log.json
}
data "aws_iam_policy_document" "alb_log" {
statement {
effect = "Allow"
actions = ["s3:PutObject"]
resources = ["arn:aws:s3:::${aws_s3_bucket.alb_accesslogs_bucket.id}/*"]
principals {
type = "AWS"
identifiers = ["582318560864"]
}
}
}
ここまで実装できたら、今までと同様でterraformコマンドを実行して
リソースを生成します。
これでRailsの画像投稿も機能しますので完成です!!
ECS起動の確認作業
Terraformの実装は完了しましたが、
現状はECRにイメージが存在しない状態です。
その為、ECRのタスクでコンテナ生成が上手くいきません。
そこで、当記事ではターミナルから手動で
ローカルからECRにイメージをpushします。
マネジメントコンソールのECR画面から
対象リポジトリにチェックを入れて
プッシュコマンドの表示
をクリックすると
イメージpushに関わる実行コマンドを表示されます。
<手動パターン>
# 1.Dockerコマンドが使用できるようにAWSに認証させる
% aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com
# 2.本番用のDockerfile.productionをもとにイメージを生成する
% docker build -f Dockerfile.production -t ECRレポジトリ名 .
# 3.ECRに保存するためにAWS指定のタグ付けを行う
% docker tag ECRレポジトリ名:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ECRレポジトリ名:latest
# 4.指定タグをつけたイメージをECRに保存する
% docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ECRレポジトリ名:latest
もし、CircleCIのOrbsを用いて、自動デプロイを実装していれば
ECR/ECSに関わるAWSリソース名を全て変更すれば機能します。
参考までにCircleCIの設定ファイルを載せておきます。
<CircleCIで自動パターン>
version: 2.1
jobs:
build-and-test:
docker:
- image: circleci/ruby:2.7.3-node-browsers
environment:
RAILS_ENV: 'test'
- image: circleci/mysql:8.0
command: mysqld --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
MYSQL_ROOT_HOST: '%'
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "Gemfile.lock" }}
- v1-dependencies-
- run:
name: install dependencies
command: |
bundle install --jobs=4 --retry=3 --path vendor/bundle
- save_cache:
paths:
- ./vendor/bundle
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
- run: yarn add @fortawesome/fontawesome-free
- run: mv config/database.yml.ci config/database.yml
- run: bundle exec rake db:create
- run: bundle exec rake db:schema:load
# rubocopのコード解析は一旦保留にする
# - run:
# name: Rubocop
# command: bundle exec rubocop
- run:
name: RSpec
command: |
mkdir /tmp/test-results
TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
circleci tests split --split-by=timings)"
bundle exec rspec \
--format progress --format RspecJunitFormatter \
--out /tmp/test-results/rspec.xml \
$TEST_FILES
- store_test_results:
path: /tmp/test-results
- store_artifacts:
path: tmp/screenshots
destination: test-screenshots
orbs:
aws-ecr: circleci/aws-ecr@7.3.0
aws-ecs: circleci/aws-ecs@2.2.1
workflows:
build-and-deploy:
jobs:
- build-and-test
- aws-ecr/build-and-push-image:
filters:
branches:
only: master
extra-build-args: '--build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY}'
### terraformで作成したECR名称に変更
repo: ECRレポジトリ名
dockerfile: Dockerfile.production
- aws-ecs/deploy-service-update:
filters:
branches:
only: master
requires:
- aws-ecr/build-and-push-image
### terraformで作成したリソース名に全て変更
cluster-name: 'クラスター名'
service-name: 'サービス名'
family: 'タスク定義名'
deployment-controller: 'CODE_DEPLOY'
codedeploy-application-name: 'CodeDeployのアプリケーション名'
codedeploy-deployment-group-name: 'CodeDeployのデプロイメントグループ名'
codedeploy-load-balanced-container-name: 'コンテナ名'
上記作業を実施後に
マネジメントコンソールのサービス内のイベントタブで
「service サービス名 has reached a steady state.」
のメッセージが確認できれば成功です。
参考動画/書籍
参考記事
終わりに
一度、Terraformでインフラが構築できれば、後から
部分的に構成を変更したり、削除したり、追加したりも容易になるし
terraform最高!
ただ、正直なところ、細かい部分は設定できていないし、
これから加筆修正も加えないとな…
長い記事となりましたが、最後までお読み頂き
ありがとうございました。