Bamboo remote agent の実行環境に関する改善 part2

Bamboo remote agent の実行環境を改善する

oginoNovember 22, 2024

はじめに

この記事は、AWS EC2 インスタンス上で稼働する Bamboo remote agent (以下 remote agent) に関する改善活動の第二回目の記事です。

前回の記事では、remote agent の実行環境に対して以下の改善を実施しました。

  • サーバーのセットアップ処理の自動化
  • remote agent が実行される EC2 インスタンスの分散

今回の記事では、以下の課題に対する取り組みについてご紹介します。

  • サーバーを再構築した場合に、キャッシュ等の資材を持ち越せない
  • サーバーの費用が最適化されていない

改善前の実行環境について

前回の記事の改善実施後の remote agent の実行環境は、以下のような構成になっています。

  • remote agent が実行される環境として EC2 インスタンスが 2 台起動している
  • remote agent は、EC2 インスタンス内で 2 つ起動している
  • EBS ボリュームは EC2 インスタンスのブロックデバイスマッピングで定義している

上記の環境に対して、キャッシュ資材の永続化とサーバー費用の最適化を実施します。

改善の詳細

はじめに、キャッシュ資材の永続化から実施します。

前回の記事でもご紹介しましたが、改善前の環境は EC2 のブロックデバイスマッピングで、ふたつの EBS ボリュームを定義していました。

module "agent_server" {
  source = "terraform-aws-modules/ec2-instance/aws"
  root_block_device = [
    {
      volume_type = "gp3"
      volume_size = 1000
    }
  ]
  ebs_block_device = [
    {
      device_name = "/dev/sdf"
      volume_type = "gp3"
      volume_size = 1000
    }
  ]

上記のうち /dev/sdf の EBS ボリュームを /data/ にマウントしており、この領域を永続化用途で利用したいと考えていました。しかしながら、上記の状態では、EC2 インスタンスを削除した場合に EBS ボリュームも削除されてしまう、EBS ボリュームを残せたとしても、手動のアタッチが必要となる、という状況でした。そのため、/dev/sdf に相当する EBS ボリュームを別途作成し、当該 EBS ボリュームに必要なキャッシュ資材を保存し、永続化する方針としました。

まず、EBS ボリュームを作成します。以下は aws_ebs_volume リソースを使用した EBS ボリュームの作成例です。

resource "aws_ebs_volume" "remote_agent_vol" {
  availability_zone = element(module.vpc.azs, 0)
  type              = "gp3"
  size              = 100
  iops              = 3000
}

上記を terraform apply することで、EBS ボリュームが作成されます。

次に、上記で作成した EBS ボリュームを、EC2 インスタンスから自動的にマウントできるようにしていきます。方法としては、前回の記事でも利用したユーザーデータを選択しました。bamboo-agent-userdata.tpl を以下の内容に更新します。前回の記事と同じく、実際のファイルの内容とは異なるため、注意してください。

#!/bin/bash
 
_ebs_device_name=${EBS_DEVICE_NAME}
_ebs_volume_id=${EBS_VOLUME_ID}
_bamboo_agent_token=${BAMBOO_AGENT_TOKEN}
 
token=$(curl -s --request PUT "http://169.254.169.254/latest/api/token" --header "X-aws-ec2-metadata-token-ttl-seconds: 600")
instance_id=$(curl -s "http://169.254.169.254/latest/meta-data/instance-id" --header "X-aws-ec2-metadata-token: $${token}") 
 
aws ec2 attach-volume \
  --device "$${_ebs_device_name}" \
  --instance-id "$${instance_id}" \
  --volume-id "$${_ebs_volume_id}"
 
sleep 15
 
until lsblk /dev/nvme1n1; do
  state=$(aws ec2 describe-volumes \
    --volume-ids "$${_ebs_volume_id}" \
    --query "Volumes[].Attachments[].State" \
    --output text)
  echo "ebs state: $${state}"
  if [ "$${state}" = "attached" ]; then
    echo "ebs attach completed."
    break
  fi
 
  echo "not detected nvme1n1 device, sleeping and retrying..."
  sleep 10
done
 
aws ec2 modify-instance-attribute \
  --instance-id "$${instance_id}" \
  --block-device-mappings "[{
    \"DeviceName\": \"$${_ebs_device_name}\",
    \"Ebs\": {
      \"DeleteOnTermination\": false
    }
  }]"
 
# mount
device_name=$(nvme list | grep $${_ebs_volume_id/-/} | awk '{ print $1 }')
filesystem=$(blkid -o value -s TYPE $${device_name})
until ls -l /dev/disk/by-uuid | grep $${device_name##*/}; do
  echo "not detected uuid of the $${device_name##*/} device, sleeping and retrying..."
  sleep 10s
done
disk_uuid=$(lsblk -f | grep $${device_name##*/} | awk '{ print $3 }')
mkdir /data
tee -a /etc/fstab <<EOF
UUID=$${disk_uuid}   /data  xfs  defaults,nofail  0  2
EOF
mount -a
 
# package
# https://github.com/amazonlinux/amazon-linux-2023/issues/397
for package in git docker java-17-amazon-corretto; do
  until dnf install -y "$${package}"; do
    echo "dnf $${package} install failed, sleeping and retrying..."
    sleep 10s
  done
done
systemctl enable docker
systemctl start docker
 
# remote-agent
data_home="/data/home"
bamboo_home_dir="$${data_home}/bamboo"
useradd -s /sbin/nologin -G docker -d "$${bamboo_home_dir}" bamboo
chown -R bamboo:bamboo "$${data_home}"
 
bamboo_version="n.n.n"
base_url="https://packages.atlassian.com/content/groups/public/com/atlassian/bamboo/atlassian-bamboo-agent-installer"
filename="atlassian-bamboo-agent-installer-$${bamboo_version}.jar"
sudo -u bamboo wget \
  -O "$${bamboo_home_dir}/atlassian-bamboo-agent-installer.jar" \
  "$${base_url}/$${bamboo_version}/$${filename}"
 
agent_number=${AGENT_NUMBERS_PER_INSTANCE}
for i in $(seq 1 $${agent_number}); do
  agent_name=$(printf bamboo-remote-agent-%02d "$${i}")
  agent_home="/data/$${agent_name}"
  mkdir -p "$${agent_home}"
  chown -R bamboo:bamboo "$${agent_home}"
  sudo -u bamboo /usr/bin/java \
    -Dbamboo.home="$${agent_home}" \
    -jar "$${bamboo_home_dir}/atlassian-bamboo-agent-installer.jar" \
    https://<your-bamboo-server-domain>/agentServer/ \
    install \
    -t "$${_bamboo_agent_token}"
  tee -a "/etc/systemd/system/$${agent_name}.service"<<EOF
[Unit]
Description=Bamboo Remote Agent $${i}
After=network.target
 
[Service]
User=bamboo
ExecStart=/usr/bin/java \
-Dbamboo.home="$${agent_home}" \
-jar "$${bamboo_home_dir}/atlassian-bamboo-agent-installer.jar" \
https://<your-bamboo-server-domain>/agentServer/
Restart=always
 
[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable "$${agent_name}"
  systemctl start "$${agent_name}"
done

前回との変更点は、EC2 インスタンスに対する EBS ボリュームのアタッチ処理の追加と、EBS ボリュームに対する削除保護の追加です。

上記のファイルをテンプレートとして、ユーザーデータを設定します。

module "agent_server" {
  source = "terraform-aws-modules/ec2-instance/aws"
  root_block_device = [
    {
      volume_type = "gp3"
      volume_size = 1000
    }
  ]
  user_data_base64 = base64encode(templatefile("${path.module}/bamboo-agent-userdata.tpl", {
    AGENT_NUMBERS_PER_INSTANCE = 2
    BAMBOO_AGENT_TOKEN         = var.bamboo_agent_token
    EBS_DEVICE_NAME            = "/dev/sdf"
    EBS_VOLUME_ID              = aws_ebs_volume.remote_agent_vol.id
  }))

上記の agent_serverterraform apply することで起動される EC2 インスタンスは、ユーザーデータが実行される処理の過程で aws_ebs_volume.remote_agent_vol として作成した EBS ボリュームを自動的にアタッチ、マウントします。

上記までの変更で /data/ にマウントする EBS ボリュームは EC2 インスタンスのライフサイクルとは切り離され、永続的に利用ができるようになりました。後は適宜設定を修正して、キャッシュとして残したいデータを /data/ に保存するように調整します。当社の環境では、remote agent のホームディレクリや Maven リポジトリのローカルキャッシュをはじめとしたデータを対象に保存しています。

次に、サーバー費用の最適化に取り組みます。当社では、以下のふたつの方法により費用の最適化を図りました。

  1. 全ての EC2 インスタンスにスポットインスタンスを適用する
  2. 特定時間帯において、スポットインスタンスのうち 1 台を停止する

以下は、autoscaling モジュールを使用してスポットインスタンスを定義する例となります。vpc_zone_identifier には、作成した EBS ボリュームと同一の Availability Zone を持つサブネットを指定する点に注意してください。

module "remote_agent_asg" {
  source                = "terraform-aws-modules/autoscaling/aws"
  min_size              = 1
  max_size              = 1
  desired_capacity      = 1
  vpc_zone_identifier   = var.subnet_ids
  user_data             = base64encode(templatefile("${path.module}/bamboo-agent-userdata.tpl", {
    AGENT_NUMBERS_PER_INSTANCE = 2
    BAMBOO_AGENT_TOKEN         = var.bamboo_agent_token
    EBS_DEVICE_NAME            = "/dev/sdf"
    EBS_VOLUME_ID              = aws_ebs_volume.remote_agent_vol.id
  }))
  block_device_mappings = [
    {
      device_name = "/dev/xvda"
      no_device   = 0
      ebs = {
        volume_type = "gp3"
        volume_size = 1000
      }
    }
  ]
  use_mixed_instances_policy = true
  mixed_instances_policy = {
    instances_distribution = {
      on_demand_base_capacity                  = 0
      on_demand_percentage_above_base_capacity = 0
      spot_allocation_strategy                 = "capacity-optimized"
    }

上記を terraform apply することで Autoscaling Group が作成されます。当該 Autoscaling Group によって起動される EC2 インスタンスは、スポットインスタンスが適用されます。当社の環境では、2 台あったオンデマンドインスタンスを、全てスポットインスタンスへ変更しています。

次に、スケジュールによる自動実行、自動停止を設定します。これは autoscaling モジュールの schedules input から設定が可能です。以下は平日の 09:00 - 21:00 の期間、EC2 インスタンスを起動させたい場合の定義例です。

  schedules = {
    // 平日 21:00 に停止する
    night = {
      min_size         = 0
      max_size         = 0
      desired_capacity = 0
      recurrence       = "0 21 * * 1-5" # Mon-Fri in the evening
      time_zone        = "Asia/Tokyo"
    }
    // 平日 09:00 に起動する
    morning = {
      min_size         = 1
      max_size         = 1
      desired_capacity = 1
      recurrence       = "0 9 * * 1-5" # Mon-Fri in the morning
      time_zone        = "Asia/Tokyo"
    }
  }

当社の環境では、上記を利用して特定時間帯は 2 台の EC2 インスタンスのうち、1 台を停止する運用としています。

得られた効果

EBS ボリュームの永続化を行うことで、以下の効果が得られました。

  1. キャッシュデータが永続化されることによる CI ジョブの高速化

また、スポットインスタンスの適用、スケジュール起動・スケジュール停止の対応を行うことで、以下の効果が得られました。

  1. EC2 インスタンスに関する費用の 50 % 程度の削減

おわりに

今回の記事では、Bamboo remote agent の実行環境となる EC2 インスタンスにアタッチされる EBS ボリュームの永続化と、サーバー費用の最適化に関する取り組みについてご紹介しました。

スポットインスタンスの活用については安定性とのトレードオフにもなるため、実際に導入する場合にはスポットインスタンスの割合などについても十分に検討していただければと思います。