この記事はTerraform Advent Calendar 2021の24日目です
タイトルながああああああ!!!!!
どうも、@Tocyukiこと、としゆきです。
実は今年の8月に神奈川県は川崎市から長野県は佐久市に移住したんですよ。
で、ですよ、川崎に住んでいた頃に足繁く通った二郎系インスパイアの雄、麺でる川崎808ismが食べたくて食べたくてしょうがなかったんですけど、最近通販を始めたみたいで長野でもおうち麺でる出来ることが判明してぶち上がってます
ちょっ!!!麺でる川崎 @mendel_kawasaki 通販始めてるじゃないか!!!長野でも食べれるの嬉しすぎるんだが!!!二郎好きは是非試してみて🍜https://t.co/INLoHRngL6
— Tocyuki@SRE (@Tocyuki) December 21, 2021
ニンニクいれますか?開発環境足りてますか?
最近はやれECSだ、やれk8sだ、などとコンテナ環境がだいぶ浸透してきてますが、EC2などのVM環境で動いているシステムもまだまだ多いのではないでしょうか?
コード化などもしていないと多くの場合、Nginx等のVirtualHostでVM1台に複数環境を建てられている、などということも日常茶飯事かと思います。
そして開発が活発になると、環境が足りなくなり、VirtualHostによる環境追加が繰り返されて、VMのリソースが枯渇し無事全環境死亡・・・、なんてこともあったりなかったりしてもう困っちゃいますよね?
というわけで(?)、TerraformとJenkinsを使った夢の専用開発環境自動構築CI/CDパイプラインを作ったのでどのように実装したかを書いていきたいと思いますー!
今回お話しすること
以下赤枠部分について掻い摘んでお話します。
仕組みの簡単な説明としてはPRをOpen&Closeした際に、ブランチ名をサブドメインとした専用の開発環境の自動構築&削除をTerraformとJenkinsを使って実現します。
今回お話ししないこと
- TerraformおよびTerraformのディレクトリ設計についての説明
- Jenkins、
Jenkinsfile
についての説明 - 各コードの説明
前提
- AMI、Database、NWリソース、ACM、tfstate用S3バケット等は別途用意されている
- PR毎に構築される環境の名前は
feature
環境
Terraformの実装
すでにある開発環境のリソースとは別に以下のようなAWSリソースを作成するtfファイルを用意します。
ちなみにDNSにはCloudflareを利用しています。
terraform
├── data_source.tf
├── main.tf
├── modules
│ ├── aws
│ │ └── app
│ │ ├── alb.tf
│ │ ├── ami.tf
│ │ ├── codedeploy.tf
│ │ ├── ec2.tf
│ │ ├── iam.tf
│ │ ├── s3.tf
│ │ ├── scripts
│ │ │ └── user_data.sh
│ │ ├── security_group.tf
│ │ └── variables.tf
│ └── cloudflare
│ └── domain
│ ├── dns.tf
│ ├── provider.tf
│ └── variables.tf
└── variables.tf
data_source.tf
feature
環境で利用するNWリソース、DB、ACM等の既存開発環境リソースを定義します。
こういう取り組みをしようとした場合、命名規則がきちんと設計されて運用できていると実装が楽になるのでAWSリソースの命名規則はしっかりしておきましょう!
data "aws_vpc" "vpc" {
filter {
name = "tag:Name"
values = ["${var.name}-dev-vpc"]
}
}
data "aws_subnet" "public" {
for_each = var.azs
filter {
name = "tag:Name"
values = ["${var.name}-dev-public-subnet-${each.key}"]
}
}
data "aws_subnet" "private_app" {
for_each = var.azs
filter {
name = "tag:Name"
values = ["${var.name}-dev-private-app-subnet-${each.key}"]
}
}
data "aws_security_group" "db" {
filter {
name = "tag:Name"
values = ["${var.name}-dev-db-sg"]
}
}
data "aws_acm_certificate" "app" {
domain = "*.${var.domain[var.service]}"
}
main.tf
Jenkinsから実行するときにtfstate
ファイル名を-backend-config
で渡すため、S3バックエンドのtfstate
定義は記載しないようにします。
前述の通り、すでにあるtfstate
用のS3バケットを利用します。
terraform {
required_version = "~>0.15"
backend "s3" {
bucket = "example-dev-tfstate-XXXXXXXXXXXX"
region = "ap-northeast-1"
}
required_providers {
aws = {
version = ">=3.44.0"
source = "hashicorp/aws"
}
cloudflare = {
version = ">=2.21.0"
source = "cloudflare/cloudflare"
}
}
}
module "dns_cloudflare" {
source = "./modules/cloudflare/domain"
name = var.name
env = replace(split("/", lower(var.branch))[1], "_", "-")
service = var.service
zone_apex = var.domain
alb_dns_name = module.app.alb_dns_name
}
module "app" {
source = "./modules/aws/app"
name = var.name
env = replace(split("/", lower(var.branch))[1], "_", "-")
service = var.service
domain = var.domain[var.service]
common_tags = local.common_tags
vpc_id = data.aws_vpc.vpc.id
az = "aza"
public_subnets = data.aws_subnet.public
private_subnets_app = data.aws_subnet.private_app
acm_arn = data.aws_acm_certificate.app.arn
aws_account_id = data.aws_caller_identity.current.id
aws_elb_service_account_arn = data.aws_elb_service_account.alb.arn
instance_type = "t3a.micro"
instance_volume_size = 30
db_sg = data.aws_security_group.db
}
以下の部分ですが、ブランチ毎に環境が立ち上がるため、Jenkinsから引数で渡ってくるブランチがfeature/create_typo
であれば各リソース定義に利用される命名規則のenv
部分が、create-typo
となります。
env = replace(split("/", lower(var.branch))[1], "_", "-")
variables.tf
service
とbranch
を未定義にして、Jenkinsで実行する時の引数として渡せるようにしておきます。
data "aws_caller_identity" "current" {}
data "aws_canonical_user_id" "current_user" {}
data "aws_region" "current" {}
data "aws_elb_service_account" "alb" {}
variable "service" {}
variable "branch" {}
locals {
common_tags = {
ServiceName = "my-service-name"
Env = "feature"
}
}
variable "name" {
type = string
default = "example"
}
variable "azs" {
type = map(string)
default = {
aza = "ap-northeast-1a"
azd = "ap-northeast-1d"
}
}
variable "domain" {
type = map(string)
default = {
system1 = "system1-example.com"
system2 = "system2-example.com"
}
}
app module
alb.tf
resource "aws_alb" "app" {
name = "${var.env}-${var.service}"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = values(var.public_subnets)[*].id
ip_address_type = "ipv4"
enable_http2 = true
tags = merge(var.common_tags, {
SystemName = var.service
Name = "${var.name}-${var.env}-${var.service}-app-alb"
Role = "ALB"
})
}
resource "aws_alb_target_group" "app_http" {
name = "${var.env}-${var.service}"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "instance"
deregistration_delay = 0
lifecycle {
create_before_destroy = true
}
health_check {
interval = 30
path = "/"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
matcher = 200
}
tags = merge(var.common_tags, {
SystemName = var.service
Name = "${var.name}-${var.env}-${var.service}-app-alb-tg"
Role = "ALB"
})
}
resource "aws_alb_listener" "app_http" {
load_balancer_arn = aws_alb.app.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_alb_listener" "app_https" {
load_balancer_arn = aws_alb.app.arn
port = 443
protocol = "HTTPS"
certificate_arn = var.acm_arn
default_action {
type = "forward"
target_group_arn = aws_alb_target_group.app_http.arn
}
}
ami.tf
前提としてAMIは作成されているので、そのAMIを使う。
data "aws_ami" "app" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["${var.name}-dev-${var.service}-ami-*"]
}
filter {
name = "state"
values = ["available"]
}
}
ec2.tf
data "template_file" "user_data" {
template = file("${path.module}/scripts/user_data.sh")
vars = {
name = var.name
env = var.env
service = var.service
domain = var.domain
shared_account_id = var.shared_account_id
}
}
resource "aws_instance" "app" {
ami = data.aws_ami.app.id
instance_type = var.instance_type
iam_instance_profile = aws_iam_instance_profile.app.name
user_data = base64encode(data.template_file.user_data.rendered)
vpc_security_group_ids = [aws_security_group.app.id]
subnet_id = var.private_subnets_app[var.az].id
root_block_device {
volume_size = var.instance_volume_size
tags = merge(var.common_tags, {
SystemName = var.service
Name = "${var.name}-${var.env}-${var.service}-app"
Role = "EBS"
})
}
tags = merge(var.common_tags, {
SystemName = var.service
Name = "${var.name}-${var.env}-${var.service}-app"
Role = "EC2"
})
}
resource "aws_alb_target_group_attachment" "app" {
target_group_arn = aws_alb_target_group.app_http.arn
target_id = aws_instance.app.id
}
user_data.sh
起動時に実行される処理を書いたスクリプト。
今回はホスト名をユニークにするための処理しか入れてないが、ここで色々しようと思えばできるので夢膨らみます。
#!/usr/bin/env bash
set -xe
exec >>(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
IP=$(hostname -I |awk '{print $1}'|awk -F. '{OFS="-"}{print $3,$4}')
BASE_NAME="${name}-${env}-${service}"
HOSTNAME="$BASE_NAME-app-$IP"
hostnamectl set-hostname "$HOSTNAME"
codedeploy.tf
Terraformを使えば使い捨てのCodeDeploy定義も楽勝です。
resource "aws_codedeploy_app" "app" {
compute_platform = "Server"
name = "${var.name}-${var.env}-${var.service}-app"
}
resource "aws_codedeploy_deployment_group" "app" {
app_name = aws_codedeploy_app.app.name
deployment_group_name = "${var.name}-${var.env}-${var.service}-app-deployment-group"
deployment_config_name = "CodeDeployDefault.OneAtATime"
service_role_arn = aws_iam_role.codedeploy.arn
ec2_tag_set {
ec2_tag_filter {
key = "Name"
type = "KEY_AND_VALUE"
value = "${var.name}-${var.env}-${var.service}-app"
}
}
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE"]
}
alarm_configuration {
alarms = ["my-alarm-name"]
enabled = true
}
}
iam.tf
EC2、SSM、CodeDeployのIAMを定義します。
EC2に鍵は置かずにSessionManagerでのみ接続できるようにします。
resource "aws_iam_instance_profile" "app" {
name = "${var.name}-${var.env}-${var.service}-app-iam-instance-profile"
role = aws_iam_role.app.name
}
data "aws_iam_policy_document" "app" {
statement {
actions = ["sts:AssumeRole"]
principals {
identifiers = ["ec2.amazonaws.com"]
type = "Service"
}
}
}
resource "aws_iam_role" "app" {
name = "${var.name}-${var.env}-${var.service}-app-iam-role"
path = "/system/"
assume_role_policy = data.aws_iam_policy_document.app.json
}
resource "aws_iam_role_policy_attachment" "ssm_for_app" {
role = aws_iam_role.app.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
}
resource "aws_iam_role_policy_attachment" "s3_for_app" {
role = aws_iam_role.app.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
data "aws_iam_policy_document" "codedeploy" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
identifiers = ["codedeploy.amazonaws.com"]
type = "Service"
}
}
}
resource "aws_iam_role" "codedeploy" {
name = "${var.name}-${var.env}-${var.service}-app-codedeploy"
assume_role_policy = data.aws_iam_policy_document.codedeploy.json
}
resource "aws_iam_role_policy_attachment" "codedeploy" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole"
role = aws_iam_role.codedeploy.name
}
security_group.tf
resource "aws_security_group" "alb" {
name = "${var.name}-${var.env}-${var.service}-app-alb-sg"
description = "Controls access to the ALB for ${var.name}-${var.env}"
vpc_id = var.vpc_id
tags = merge(var.common_tags, {
Name = "${var.name}-${var.env}-${var.service}-app-alb-sg"
Role = "Security Group"
})
}
resource "aws_security_group_rule" "alb_http_ingress" {
from_port = 80
protocol = "tcp"
security_group_id = aws_security_group.alb.id
to_port = 80
type = "ingress"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "alb_https_ingress" {
from_port = 443
protocol = "tcp"
security_group_id = aws_security_group.alb.id
to_port = 443
type = "ingress"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "alb_all_egress" {
from_port = 0
protocol = "-1"
security_group_id = aws_security_group.alb.id
to_port = 0
type = "egress"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group" "app" {
name = "${var.name}-${var.env}-${var.service}-app-sg"
description = "Controls access to the App for ${var.name}-${var.env}"
vpc_id = var.vpc_id
tags = merge(var.common_tags, {
Name = "${var.name}-${var.env}-${var.service}-app-sg"
Role = "Security Group"
})
}
resource "aws_security_group_rule" "app_ingress_http" {
from_port = 80
protocol = "tcp"
security_group_id = aws_security_group.app.id
to_port = 80
type = "ingress"
source_security_group_id = aws_security_group.alb.id
}
resource "aws_security_group_rule" "app_egress_all" {
from_port = 0
protocol = "-1"
security_group_id = aws_security_group.app.id
to_port = 0
type = "egress"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "db_ingress_from_app" {
from_port = 3306
protocol = "tcp"
security_group_id = var.db_sg.id
to_port = 3306
type = "ingress"
source_security_group_id = aws_security_group.app.id
}
s3.tf
CodeDeploy用の成果物置き場のS3バケットを作ります。
resource "aws_s3_bucket" "artifact" {
bucket = "${var.name}-${var.env}-${var.service}-app-artifact-${var.aws_account_id}"
acl = "private"
force_destroy = true
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
tags = merge(var.common_tags, {
SystemName = var.service
Name = "${var.name}-${var.env}-${var.service}-app-artifact-bucket"
Role = "S3"
})
}
data "aws_iam_policy_document" "artifact" {
statement {
effect = "Allow"
actions = ["s3:PutObject"]
resources = ["${aws_s3_bucket.artifact.arn}/*"]
principals {
identifiers = ["codedeploy.amazonaws.com"]
type = "Service"
}
}
statement {
effect = "Allow"
actions = ["s3:GetBucketAcl"]
resources = [aws_s3_bucket.artifact.arn]
principals {
identifiers = ["codedeploy.amazonaws.com"]
type = "Service"
}
}
}
resource "aws_s3_bucket_policy" "artifact" {
bucket = aws_s3_bucket.artifact.id
policy = data.aws_iam_policy_document.artifact.json
depends_on = [aws_s3_bucket_public_access_block.artifact]
}
resource "aws_s3_bucket_public_access_block" "artifact" {
bucket = aws_s3_bucket.artifact.id
block_public_acls = true
ignore_public_acls = true
block_public_policy = true
restrict_public_buckets = true
}
```
#### variables.tf
aws/app moduleで利用する`variables`を定義。
```terraform:modules/aws/app/variables.tf
variable "name" {}
variable "env" {}
variable "service" {}
variable "domain" {}
variable "common_tags" {}
variable "az" {}
variable "vpc_id" {}
variable "public_subnets" {}
variable "private_subnets_app" {}
variable "aws_account_id" {}
variable "aws_elb_service_account_arn" {}
variable "instance_type" {}
variable "instance_volume_size" {}
variable "db_sg" {}
variable "acm_arn" {}
```
### cloudflare module
構築した環境へすぐにアクセスしたいので、ドメイン設定もTerraformで定義します。
今回はDNSにCloudflareを利用します。
#### dns.tf
aws/app moduleで作成したALBのFQDNをCNAMEとしてJenkinsの引数で渡されるシステムのドメインを登録する。
```terraform:modules/cloudflare/domain/dns.tf
data "cloudflare_zones" "apex_zone" {
for_each = var.zone_apex
filter {
name = var.zone_apex[each.key]
status = "active"
}
}
resource "cloudflare_record" "app" {
name = "${var.name}-${var.env}"
type = "CNAME"
proxied = false
value = var.alb_dns_name
zone_id = data.cloudflare_zones.apex_zone[var.service].zones[0].id
}
```
#### provider.tf
今回のディレクトリ構成だとここにこれを定義しないとエラーになる。
```terraform:modules/cloudflare/domain/provider.tf
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
}
}
}
```
#### variables.tf
cloudflare/domain moduleで利用する`variables`を定義。
```terraform:modules/cloudflare/domain.variables.tf
variable "name" {}
variable "env" {}
variable "service" {}
variable "zone_apex" {}
variable "alb_dns_name" {}
```
## Jenkinsの実装
Jenkinsでは以下の処理を実行するビルドジョブを作成し、それらをBlue Oceanプラグインを利用してパイプラインを構築します。
- 前述のTerraformで`apply`および`destroy`ができるジョブ
- CodeDeployでアプリケーションをデプロイするジョブ
- コスト削減のための日次での環境削除バッチジョブ
- 今回実装については割愛します:pray:
### ビルドジョブ
#### Terraformで`apply`および`destroy`ができるジョブ
仮に`feature-dev`というビルドジョブ名としておきます。
実装のポイントとしては以下です。
- Jenkinsのソースコード管理で前述のTerraformリポジトリを登録
- ビルドではシェルの実行を選択し、下記スクリプトを実行させる
- `SERVICE`, `BRANCH`, `MOTION`をビルドのパラメータとして可変可能にしておく
- 既存環境の`tfstate-lock`用のDynamoDBを利用する
- コミットを連続で実施された場合などに排他制御が入り、環境が意図せず壊れるということがなくなります。
- `destroy`後は`aws cli`でDynamoDBのlockファイルと、`tfstate`ファイルを削除しておく
```bash
#!/usr/bin/env bash
set -e
export AWS_DEFAULT_REGION='ap-northeast-1'
export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
export CLOUDFLARE_EMAIL=$CLOUDFLARE_EMAIL
export CLOUDFLARE_API_KEY=$CLOUDFLARE_API_KEY
TFSTATE_BUCKET="example-dev-tfstate-XXXXXXXXXXXX"
cd ./terraform
terraform init -reconfigure -var service=$SERVICE -var branch=$BRANCH -backend-config="key=$SERVICE/$BRANCH.tfstate" -backend-config="dynamodb_table=example-dev-tfstate-lock"
if [ $MOTION = "create" ]; then
terraform apply -var service=$SERVICE -var branch=$BRANCH --auto-approve
elif [ $MOTION = "destroy" ]; then
terraform destroy -var service=$SERVICE -var branch=$BRANCH --auto-approve
aws dynamodb delete-item --table-name example-dev-tfstate-lock --key '{ "LockID": { "S": "'$TFSTATE_BUCKET'/'$SERVICE'/'$BRANCH'.tfstate-md5" }}'
aws s3 rm s3://$TFSTATE_BUCKET/$SERVICE/$BRANCH.tfstate
fi
```
#### CodeDeployでアプリケーションをデプロイするジョブ
仮に`deploy-application`というビルドジョブ名としておきます。
ポイントとしては以下です。
ビルド処理が必要な場合はスクリプト内の適当な場所に書けばOKです:ok_hand:
- Jenkinsのソースコード管理でデプロイしたいアプリケーションのリポジトリを登録
- ビルドではシェルの実行を選択し、下記スクリプトを実行させる
- Terraform同様、ブランチ名を`ENV`として定義しサブドメインとして利用するためちょっといじる
- 例として`system1`というアプリケーションのビルドジョブとしているが、同様の内容で`system2`などのアプリケーションのビルドジョブを作成することで、複数アプリケーションに対応可能
```bash
#!/usr/bin/env bash
set -e
export AWS_DEFAULT_REGION='ap-northeast-1'
export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
aws_account_id="XXXXXXXXXXXX"
ENV=$(echo $BRANCH | awk -F/ '{print $2}' | tr 'A-Z' 'a-z' | tr '[/_]' '-')
base_name="example-${ENV}-system1"
aws deploy push \
--application-name ${base_name}-app \
--description "This is a revision for the ${base_name}-app" \
--ignore-hidden-files \
--s3-location s3://${base_name}-app-artifact-${aws_account_id}/${base_name}-app.zip \
--source .
deployment_id=$(aws deploy create-deployment \
--application-name ${base_name}-app \
--deployment-group-name ${base_name}-app-deployment-group \
--file-exists-behavior OVERWRITE \
--s3-location bucket=${base_name}-app-artifact-${aws_account_id},key=${base_name}-app.zip,bundleType=zip \
--query "deploymentId" --output text)
while :
do
status=$(aws deploy get-deployment --deployment-id ${deployment_id} --query "deploymentInfo.[status, creator]" --output text | awk '{print $1}')
if [ ${status} = "Failed" ]; then
echo "Deployment ${deployment_id} is ${status} !!!!!"
exit 1
break
elif [ ${status} = "Succeeded" ]; then
echo "Deployment ${deployment_id} is ${status} !!!!!"
break
else
echo "Deployment ${deployment_id} is ${status} ....."
sleep 10
fi
done
```
### Blue Oceanプラグインを利用してマルチパイプラインを構築
Jenkinsに[Blue Oceanプラグイン](https://www.jenkins.io/projects/blueocean/)を導入したら、`Jenkinsfile`をアプリケーションリポジトリに配置します!
ちなみにマルチパイプラインは同じSREチームの[@butadora](https://qiita.com/butadora)が実装してくれて、無事Jenkinsおじさんへジョブチェンジしました!
実装のポイントとしては以下です。
(今回は記載していないですが、実際の運用ではPRに構築された環境のアクセスURLをコメントしてあげるようにしています。)
- PRオープン、コミット追加時に、`terraform apply`とCodeDeployによるアプリケーションデプロイを実行する。
- PRマージ時に`terraform destroy`を実行する
```groovy:Jenkinsfile
pipeline {
agent any
environment {
// Branch pattern regexp
FEATURE_BRANCH_REGEX = "^feature/"
RELEASE_BRANCH_REGEX = "^release/"
HOTFIX_BRANCH_REGEX = "^hotfix/"
DESTROY_BRANCH_PATTERN = "master|^(feature|release)/"
}
stages {
stage('Create resouces') {
when {
anyOf {
changeRequest comparator: 'REGEXP', // feature -> any
branch: env.FEATURE_BRANCH_REGEX
changeRequest comparator: 'REGEXP', // hotfix or release -> any
branch: "(${env.HOTFIX_BRANCH_REGEX}|${env.RELEASE_BRANCH_REGEX})"
}
}
steps {
build job: 'feature-dev', parameters: [
string(name: 'SERVICE', value: '{実際のサービス名}'),
string(name: 'BRANCH', value: env.CHANGE_BRANCH),
string(name: 'MOTION', value: 'create')
]
}
}
stage('Deploy application') {
when {
anyOf {
changeRequest comparator: 'REGEXP', // feature -> any
branch: env.FEATURE_BRANCH_REGEX
changeRequest comparator: 'REGEXP', // hotfix or release -> any
branch: "(${env.HOTFIX_BRANCH_REGEX}|${env.RELEASE_BRANCH_REGEX})"
}
}
steps {
build job: 'deploy-application', parameters: [
string(name: 'ENV', value: 'feature'),
string(name: 'BRANCH', value: env.CHANGE_BRANCH),
booleanParam(name: 'CHECK', value: true)
]
}
}
stage('Destroy feature dev resources') {
when {
branch pattern: env.DESTROY_BRANCH_PATTERN, comparator: 'REGEXP'
}
steps {
script {
// HEADコミットメッセージを取得
def commit_message = sh(returnStdout: true, script: 'git log -1 --pretty="%s"').trim()
// feature, release, hotfixブランチからのPRマージだった場合に、マージ元のブランチ名をパース
def parse_cmd = $/eval "git log -1 --pretty=%s | sed -re 's/^Merge pull request #[0-9]* from (org名)\/(feature|release|hotfix)(\/.*)/\1\2/'"/$
env.DESTROY_TARGET_BRANCH = sh(script: "${parse_cmd}", returnStdout: true).trim()
// HEADがPRマージコミットの場合のみdestroy
if (env.DESTROY_TARGET_BRANCH == commit_message) {
echo 'HEAD is not PR merge commit.'
} else if ((env.BRANCH_NAME ==~ /^release\/.*/ && env.DESTROY_TARGET_BRANCH ==~ /^feature\/.*/)
|| (env.BRANCH_NAME ==~ /^feature\/.*/ && env.DESTROY_TARGET_BRANCH ==~ /^${env.BRANCH_NAME}.+/)
|| env.BRANCH_NAME == 'master') {
build job: 'feature-dev', parameters: [
string(name: 'SERVICE', value: '{実際のサービス名}'),
string(name: 'BRANCH', value: env.DESTROY_TARGET_BRANCH),
string(name: 'MOTION', value: 'destroy')
]
} else {
echo 'Do not need destroy.'
}
}
}
}
}
post {
always {
// 終わったらworkspaceをclean
cleanWs()
}
}
}
```
ちなみに、実際の運用ではコストの問題や長期間オープンされているPRもあるのでJenkinsの日次バッチで環境を`destroy`する処理を入れています:ok_hand:
翌日以降に再度環境を立ち上げたい場合はJenkinsのバッチを実行するかコミットを追加すればOKです!
## おわりに
こういう仕組みはどうしてもピタゴラスイッチちっくになってしまうのは仕方がない部分もあるかもしれないですが、命名規則などをしっかり決めて運用し、全体での設計に矛盾がないか確認することで大きな問題を発生させることなく運用できるのかなと思います。
弊社でこの仕組を導入して数ヶ月立ちますが、大きな問題が起こることなく運用されていて、デプロイと開発環境の改善がされて評判は上々です!
今後は、`Jenkinsfile`で実装している部分をGitHub Actionsへ変えたり、基盤もECSに変えたりとまだまだ改善したいことがたくさんあるので引き続き頑張っていきたいと思います−\(^o^)/
会社のアドベントカレンダーで本記事の内容も含むこの1年SREとしてやってきたことを書いたので是非見てみてくださいー!
https://qiita.com/Tocyuki/items/d1ae0dafcdc3f4ca9cf7