はじめに
自分の学習記録用にQiita:Team の オープンソースクローン 「Knowledge」を単一のEC2の中で
使用していたのですが、学習も兼ねてTerraformでECS環境へ移行してみました。
移行後の構成イメージ
参考サイト
みんな大好きDevelopers.ioさんの以下のサイトを参考にしました。
こちらのサイトだと手動で作成していたのですが、
ちょうどTerraformの勉強をしていたので、Terraformを使用してみました。
Amazon ECS + RDS(PostgreSQL) で Qiita:Team の オープンソースクローン Knowledge(Docker版) を構築する
おおまかな手順の流れ
-
Cloud9実行環境構築
-
Terraformで環境作成
-
ECRにイメージをアップロード
-
タスク定義作成
-
Terraformでサービス作成
-
DBデータ移行
1.Cloud9実行環境構築
前提として別の記事でCloud9実行環境構築は作成しているので、
そちらの記事を参照ください。
(1) Homebrewインストール
homebrewについては以下を参照
homebrewとは何者か。仕組みについて調べてみた
$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/Linuxbrew/install/master/install.sh)"
(2) HomebrewにPATHを通す
1を実行すると、Terminalの最後に、PATHが通っていないと警告が表示されます。
そこで、以下コマンドを順に実行してPATHを通します。
$ test -d ~/.linuxbrew && PATH="$HOME/.linuxbrew/bin:$HOME/.linuxbrew/sbin:$PATH"
$ test -d /home/linuxbrew/.linuxbrew && PATH="/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:$PATH"
$ test -r ~/.bash_profile && echo "export PATH='$(brew --prefix)/bin:$(brew --prefix)/sbin'":'"$PATH"' >>~/.bash_profile
$ echo "export PATH='$(brew --prefix)/bin:$(brew --prefix)/sbin'":'"$PATH"' >>~/.profile
(3) 動作確認
新たにTerminalを開いて確認します。
$ brew
Example usage:
brew search [TEXT|/REGEX/]
brew info [FORMULA...]
brew install FORMULA...
brew update
brew upgrade [FORMULA...]
brew uninstall FORMULA...
brew list [FORMULA...]
Troubleshooting:
brew config
brew doctor
brew install --verbose --debug FORMULA
Contributing:
brew create [URL [--no-fetch]]
brew edit [FORMULA...]
Further help:
brew commands
brew help [COMMAND]
man brew
https://docs.brew.sh
簡単な手順でCloud9でもHomebrewを実行できるようになりました。
(4) tfenvをインストール
Terraformのバージョンを切り替えることができるtfenvをインストールします。
tfenvをインストールしていれば、新しいTerraformのバージョンがリリースされても、Cloud9上で旧バージョンのインストールを維持したまま、新バージョンを試すことができます。
$ brew install tfenv
(5) tfenvで利用できるTerraformのバージョンを確認
以下のコマンドで、tfenvを使ってインストールできるTerraformのバージョンを確認します。
$ tfenv list-remote
(6) tfenvでTerraformをインストール
最新のバージョンでインストールするには、以下のコマンドを実行します。
$ tfenv install latest
バージョンを指定する場合は、以下のコマンドに倣います。
$ tfenv install 0.11.13
(7) tfenvでTerraformのバージョンを選択
tfenvでバージョンを指定すると、terraformコマンドを実行した時に、指定したバージョンが使われるようになります。1つしかバージョンをインストールしていなくても、コマンドを実行する必要があります。
$ tfenv use latest
バージョンを指定する場合は、以下のコマンドに倣います。
$ tfenv use 0.11.13
(8) 動作確認
Terraformのバージョン確認コマンドを実行して、バージョンを表す文字列が返ることを確認します。
$ terraform --version
Terraform v0.12.0-beta2
これでCloud9でもTerraformが使えるようになりました。Cloud9をEC2モードで動かしている場合は、EC2にIAM Roleをアタッチすれば、TerraformでAWSのリソースを構築できるようになります。
2.Terraformで環境作成
(1) 今回作成する予定のディレクトリ構成
サンプルで作成したgithubをクローンして使ってみてください
https://github.com/sentail-yasu/knowledge-ecs-sample
開発環境でしか作らない想定だったので、以下の構成で作りました。
適宜変更してください
├── envs
│ ├── dev
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ └── provider.tf
│ │ └── variables.t
├── acm.tf
├── ec2.tf
├── ecs.tf ←ここだけはこの章でで作らないように
├── lb.tf
├── network.tf
├── rds.tf
├── s3.tf
├── security_group.tf
└── variables.tf
(2) 定義作成
envs/dev/backend.tf
terraform {
required_version = ">= 0.12"
backend "s3" {
bucket = "tfstate-bucket" ←自分用のS3バケットを作成して変更
region = "ap-northeast-1"
key = "dev/terraform.tfstate" ←任意で変更
encrypt = true
}
}
envs/dev/main.tf
module "dev" {
source = "../../"
vpc_cidr = var.vpc_cidr
subnet_cidr = var.subnet_cidr
name = var.name
rds_instance_class = var.rds_instance_class
database_name = var.database_name
db_username = var.db_username
db_password = var.db_password
region = var.region
ami_id = var.ami_id
instance_count = var.instance_count
public_subnets = var.public_subnets
private_subnets = var.private_subnets
instance_type = var.instance_type
key_pair = var.key_pair
app_name = var.app_name
bastion_name = var.bastion_name
}
envs/dev/provider.tf
provider "aws" {
region = "ap-northeast-1"
}
envs/dev/variables.tf
variable "vpc_cidr"{
default = "10.1.0.0/16"
}
variable "subnet_cidr" {
type = "map"
default = {
public-a = "10.1.10.0/24"
public-c = "10.1.20.0/24"
private-a = "10.1.100.0/24"
private-c = "10.1.200.0/24"
}
}
variable "name" {
default = "knowledge"
}
variable "rds_instance_class" {
default = "db.t2.small"
}
variable "database_name" {
default = "knowledge"
}
variable "db_username" {
default = "postgres"
}
variable "db_password" {}
variable "region" {
default = "ap-northeast-1"
}
variable "ami_id" {
default = "ami-0f310fced6141e627" ←AmazonLinux2 x86-64
}
variable "instance_count" {
default = 2
}
variable "public_subnets" {
default = {
"0" = "subnet-xxxx" ←一回実行してサブネット作成後に手動で入れてください
"1" = "subnet-xxxx" ←一回実行してサブネット作成後に手動で入れてください
}
}
variable "private_subnets" {
default = {
"0" = "subnet-xxxx" ←一回実行してサブネット作成後に手動で入れてください
"1" = "subnet-xxxx" ←一回実行してサブネット作成後に手動で入れてください
}
}
variable "instance_type" {
default = "t3.micro"
}
variable "key_pair" {
default = "your key pair"
}
variable "app_name" {
default = "web"
}
variable "bastion_name" {
default = "bastion"
}
acm.tf
data aws_route53_zone route53-zone {
name = "your domain." ←自分で取得したドメインを入力
private_zone = false
}
resource aws_acm_certificate cert {
domain_name = "www.your domain."
validation_method = "DNS"
}
resource aws_route53_record cert_validation {
zone_id = data.aws_route53_zone.route53-zone.zone_id
name = aws_acm_certificate.cert.domain_validation_options.0.resource_record_name
type = aws_acm_certificate.cert.domain_validation_options.0.resource_record_type
records = [aws_acm_certificate.cert.domain_validation_options.0.resource_record_value]
ttl = 60
}
resource aws_acm_certificate_validation cert {
certificate_arn = aws_acm_certificate.cert.arn
validation_record_fqdns = [aws_route53_record.cert_validation.fqdn]
}
ec2.tf
resource "aws_instance" "bastion_ec2" {
count = 1
ami = var.ami_id
instance_type = var.instance_type
key_name = var.key_pair
subnet_id = lookup(var.public_subnets, count.index % 2)
vpc_security_group_ids = ["${aws_security_group.bastion_security_group.id}"]
tags = {
Name = "${var.name}_${var.bastion_name}_${count.index + 1}"
}
}
lb.tf
// albを作成。
resource "aws_alb" "alb" {
name = "${var.name}-alb"
security_groups = ["${aws_security_group.alb.id}"]
subnets = [
"${aws_subnet.public-a.id}",
"${aws_subnet.public-c.id}",
]
internal = false
enable_deletion_protection = false
access_logs {
bucket = "${var.name}-alb-logs-01"
}
}
// albのターゲットグループ
resource "aws_alb_target_group" "alb" {
name = "${var.name}-tg"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.vpc.id
target_type = "ip"
health_check {
interval = 60
path = "/"
// NOTE: defaultはtraffic-port
//port = 80
protocol = "HTTP"
timeout = 20
unhealthy_threshold = 4
matcher = 302
}
}
// 443ポートの設定。今回は事前にAWS Certificate Managerで作成済みの証明書を設定。
resource "aws_alb_listener" "alb_443" {
load_balancer_arn = "${aws_alb.alb.arn}"
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2015-05"
certificate_arn = "${aws_acm_certificate.cert.arn}"
default_action {
target_group_arn = "${aws_alb_target_group.alb.arn}"
type = "forward"
}
}
resource "aws_alb_listener" "alb" {
load_balancer_arn = "${aws_alb.alb.arn}"
port = "80"
protocol = "HTTP"
default_action {
target_group_arn = "${aws_alb_target_group.alb.arn}"
type = "forward"
}
}
output "alb" {
value = {
dns_name = "${aws_alb.alb.dns_name}"
arn = "${aws_alb.alb.arn}"
target_group_arn = "${aws_alb_target_group.alb.arn}"
}
}
network.tf
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
instance_tenancy = "default"
enable_dns_support = true
tags = {
Name = "${var.name}-vpc"
}
}
# EIP
resource "aws_eip" "nat" {
vpc = true
}
# NatGateway
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public-a.id
}
# Subnet
resource "aws_subnet" "public-a" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.subnet_cidr["public-a"]
map_public_ip_on_launch = true
availability_zone = "ap-northeast-1a"
tags = {
Name = "public-subnet-1a"
}
}
resource "aws_subnet" "public-c" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.subnet_cidr["public-c"]
map_public_ip_on_launch = true
availability_zone = "ap-northeast-1c"
tags = {
Name = "public-subnet-1c"
}
}
resource "aws_subnet" "private-a" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.subnet_cidr["private-a"]
availability_zone = "ap-northeast-1a"
tags = {
Name = "private-subunet-1a"
}
}
resource "aws_subnet" "private-c" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.subnet_cidr["private-c"]
availability_zone = "ap-northeast-1c"
tags = {
Name = "private-subunet-1c"
}
}
# db_subnet_group
resource "aws_db_subnet_group" "db-subnet" {
name = "db-subnet"
description = "test db subnet"
subnet_ids = ["${aws_subnet.private-a.id}", "${aws_subnet.private-c.id}"]
}
# Internet Gateway
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.name}-igw"
}
}
# public Route Table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.name}-rt"
}
}
# public Route
resource "aws_route" "public_route" {
gateway_id = aws_internet_gateway.igw.id
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
}
# パブリックルートテーブルとpublic-a関連付け
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public-a.id
route_table_id = aws_route_table.public.id
}
# パブリックルートテーブルとpublic-c関連付け
resource "aws_route_table_association" "public_c" {
subnet_id = aws_subnet.public-c.id
route_table_id = aws_route_table.public.id
}
# private Route Table
resource "aws_route_table" "private" {
vpc_id = aws_vpc.vpc.id
}
# private Route
resource "aws_route" "private_route" {
nat_gateway_id = aws_nat_gateway.nat.id
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
}
# パブリックルートテーブルとprivate-a関連付け
resource "aws_route_table_association" "private-a" {
subnet_id = aws_subnet.private-a.id
route_table_id = aws_route_table.private.id
}
# パブリックルートテーブルとprivate-c関連付け
resource "aws_route_table_association" "private-c" {
subnet_id = aws_subnet.private-c.id
route_table_id = aws_route_table.private.id
}
output "vpc_id" {
value = aws_vpc.vpc.id
}
output "subnet_public_a_id" {
value = aws_subnet.public-a.id
}
output "subnet_public_c_id" {
value = aws_subnet.public-c.id
}
output "subnet_private_a_id" {
value = aws_subnet.private-a.id
}
output "subnet_private_c_id" {
value = aws_subnet.private-c.id
}
rds.tf
resource "aws_db_parameter_group" "db-pg" {
name = "knowledge"
family = "postgres11.5"
description = "knowledge"
parameter {
name = "log_min_duration_statement"
value = "100"
}
}
resource "aws_db_instance" "posgre" {
identifier = "knowledge"
allocated_storage = 10
engine = "postgres"
engine_version = "11.5"
instance_class = var.rds_instance_class
name = var.database_name
username = var.db_username
password = var.db_password
db_subnet_group_name = "${aws_db_subnet_group.db-subnet.name}"
vpc_security_group_ids = ["${aws_security_group.posgre_security_group.id}"]
parameter_group_name = "${aws_db_parameter_group.db-pg.name}"
multi_az = false
backup_retention_period = "7"
backup_window = "19:00-19:30"
apply_immediately = "true"
auto_minor_version_upgrade = false
}
output "rds_endpoint" {
value = "${aws_db_instance.posgre.address}"
}
s3.tf
# プライベートバケットの定義
resource "aws_s3_bucket" "alb-logs" {
# バケット名は世界で1意にしなければならない
bucket = "${var.name}-alb-logs-01"
versioning {
enabled = true
}
# 暗号化を有効
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
}
# ブロックパブリックアクセス
resource "aws_s3_bucket_public_access_block" "alb-logs" {
bucket = aws_s3_bucket.alb-logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
security_group.tf
# RDSに設定するセキュリティグループ
resource "aws_security_group" "posgre_security_group" {
name = "${var.name}-posgre-sg"
vpc_id = aws_vpc.vpc.id
ingress {
from_port = "5432"
to_port = "5432"
protocol = "tcp"
cidr_blocks = ["10.1.0.0/16"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name}-posgre-sg"
}
}
resource "aws_security_group" "alb" {
name = "${var.name}-alb-sg"
vpc_id = aws_vpc.vpc.id
ingress {
from_port = "443"
to_port = "443"
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.name}-alb-sg"
}
}
output "db_sg_id" {
value = aws_security_group.posgre_security_group.id
}
# bastionに設定するセキュリティグループ
resource "aws_security_group" "bastion_security_group" {
name = "${var.name}-${var.bastion_name}-sg"
vpc_id = aws_vpc.vpc.id
ingress {
from_port = "22"
to_port = "22"
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.name}-${var.bastion_name}-sg"
}
}
output "bastion_sg_id" {
value = aws_security_group.bastion_security_group.id
}
# ecsに設定するセキュリティグループ
resource "aws_security_group" "ecs" {
name = "${var.name}-ecs-sg"
vpc_id = aws_vpc.vpc.id
ingress {
from_port = "8080"
to_port = "8080"
protocol = "tcp"
cidr_blocks = ["10.1.0.0/16"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name}-ecs-sg"
}
}
variables.tf
variable "vpc_cidr"{}
variable "subnet_cidr" {}
variable "name" {}
variable "rds_instance_class" {}
variable "database_name" {}
variable "db_username" {}
variable "db_password" {}
variable "region" {}
variable "ami_id" {}
variable "instance_count" {}
variable "public_subnets" {}
variable "private_subnets" {}
variable "instance_type" {}
variable "key_pair" {}
variable "app_name" {}
variable "bastion_name" {}
(3) Terraform実行
実行するディレクトリに移動後、Terraformで環境構築
※なぜかACMのところで1回目は失敗してしまうのですが、
2回目の実行で成功します。
以下の記事の方も同じ事象みたいです
https://qiita.com/pokotyan/items/6a00b5cdfe8811b4c832
$ cd terraform/envs/dev
$ terraform init
$ terraform plan
$ terraform apply
3.ECRにイメージをアップロード
(1) 公式DockerからDockerイメージをダウンロード
以下のリポジトリをクローンする
https://github.com/support-project/docker-knowledge
(2) データベース接続先の変更
volumes/knowledge/custom_connection.xmlでDB接続先設定をカスタマイズできるようですので、作成したPostgreSQLの情報を使ってこのファイルを編集します。
変更後のイメージ
<connectionConfig>
<name>custom</name>
<driverClass>org.postgresql.Driver</driverClass>
<URL>jdbc:postgresql://RDSのエンドポイント:ポート番号/DB名</URL> ←ここを変更
<user>postgres</user>
<password>mysql123</password>
<schema>public</schema>
<maxConn>0</maxConn>
<autocommit>false</autocommit>
</connectionConfig>
(3) Dockerfile の編集
さきほど編集した custom_connection.xml が コンテナ起動時に適用されるようにします。
VOLUME [ "/root/.knowledge" ]
COPY ./volumes/knowledge/custom_connection.xml /root/.knowledge/ ←ここを追加
EXPOSE 8080
(4) ECRのリポジトリ作成
前提としてAWSコンソールにログインしてECRリポジトリを作成しておく
(5) ECRへログイン
aws ecr get-login --no-include-email --region ap-northeast-1 > login.sh
bash login.sh
(6) Docker Image の作成
docker build -t knowledge .
(7) タグ付け
docker tag knowledge:latest ECRのリポジトリ名/knowledge:latest
(8) プッシュ
docker push ECRのリポジトリ名/knowledge:latest
4.タスク定義作成
(1) task_definition.jsonを作成
$ vi task_definition.json
以下の内容を環境に置き換えて作成
{
"family": "sample",
"executionRoleArn": "arn:aws:iam::自分の環境に変更:role/ecsTaskExecutionRole",
"networkMode": "awsvpc",
"containerDefinitions": [
{
"name": "knowledge",
"image": "自分のECRのリポジトリ/knowledge:latest",
"ulimits": [
{
"name": "nofile",
"softLimit": 65536,
"hardLimit": 65536
}
],
"portMappings": [
{
"protocol": "tcp",
"containerPort": 8080
}
],
"memoryReservation": 256,
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/knowledge",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "knowledge"
}
}
}
],
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "512",
"memory": "2048"
}
(2) タスク定義作成
$ aws ecs register-task-definition --cli-input-json file://$PWD/task-definitions.json
5.Terraformでサービス作成
(1) 定義作成
ecs.tf
## cluster設定
resource "aws_ecs_cluster" "ecs_cluster" {
name = "${var.name}-cluster"
}
resource "aws_ecs_service" "ecs" {
name = "${var.name}-01"
cluster = "${aws_ecs_cluster.ecs_cluster.id}"
task_definition = "knowledge-fagate:1"
desired_count = 2
launch_type = "FARGATE"
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
network_configuration {
subnets = [
"${aws_subnet.private-a.id}",
"${aws_subnet.private-c.id}"
]
security_groups = [
"${aws_security_group.ecs.id}"
]
assign_public_ip = "false"
}
health_check_grace_period_seconds = 0
load_balancer {
target_group_arn = "${aws_alb_target_group.alb.arn}"
container_name = "knowledge"
container_port = 8080
}
scheduling_strategy = "REPLICA"
deployment_controller {
type = "CODE_DEPLOY"
}
// deployやautoscaleで動的に変化する値を差分だしたくないので無視する
lifecycle {
ignore_changes = [
"desired_count",
"task_definition",
"load_balancer",
]
}
propagate_tags = "TASK_DEFINITION"
}
(2) オプトイン設定を定義
どうやらタスク定義はARNがオプトインの設定を変更しない場合、ARNが長すぎてエラーになってしまうため、以下の公式ドキュメントに従い、自分が使用しているIAMだけオプトイン設定を変更する。
https://aws.amazon.com/jp/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2/
(3) Terraform実行
$ cd terraform/envs/dev
$ terraform init
$ terraform plan
$ terraform apply
(4) 実行後確認
ECSのサービスが無事出来ていれば完璧
**
6.DBデータ移行
(1) 移行前のRDSにログインできるEC2にログイン
※割愛してますが、組み込みのKnowledgeのposgreにログインできなかったため、
わざわざRDSを建て、GUIからDBの移行しました...
(2) postgresql11のインストール
こちらのpostgres環境はバージョン11であったため、
以下のコマンドでインストール実施
$ sudo rpm -ivh --nodeps https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
$ sed -i "s/\$releasever/7/g" "/etc/yum.repos.d/pgdg-redhat-all.repo"
$ sudo yum install -y postgresql11
(3) 以下のコマンドでdump取得
pg_dump -U ユーザ名 -h ホスト名 -p 5432 DB名 -f ファイル名.dump
(4) 移行後のRDSにログインできるEC2にログイン
(5) postgresql11のインストール
こちらのpostgres環境はバージョン11であったため、
以下のコマンドでインストール実施
$ sudo rpm -ivh --nodeps https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
$ sed -i "s/\$releasever/7/g" "/etc/yum.repos.d/pgdg-redhat-all.repo"
$ sudo yum install -y postgresql11
(6) 以下のコマンドでdumpから復元
なんか色々エラーが起きてたけど、無視しました...
そのせいなのかユーザが復元されませんでした
psql -U ユーザ名 -h ホスト名 -d データベース名 -p 5432 -f ファイル名.dump
(7) 画像を読み込めるように再度修正
なぜか画像が読み込めなくてなっていたので、
DBに直接ログインして以下のSQL文を発行する。
※正直ここは環境特有かも
UPDATE knowledges
SET content = REPLACE(content, '/knowledge/open.file/download?', '/open.file/download?');
以上で、移行完了です。
今後の課題
Terraformの実行もECRにイメージをアップするところも
手動でやってしまったので、CI/CDツールをもっと活かして自動化していきたいと思います。