こんにちは、カバー株式会社エンジニアのAです。
本記事では表題の通り、Unityが実行可能なUbuntu環境のAMIをビルドした際のPackerコードをご紹介します。
はじめに
私はカバー株式会社で、タレントの皆さんが日々の配信で使う「ホロライブアプリ」の開発に携わっています。
ホロライブアプリはUnityで開発しており、いままでこのアプリのテスト・ビルドはオンプレマシンをself-hosted runnerとして登録したgithub actionsで行っていました。しかし、開発の規模の拡大に伴い、オンプレマシンの管理のしにくさや柔軟な運用ができないなどの問題点が浮上しました。
これに対して、AWS上にスケール可能なCI環境を構築しました。EC2インスタンスを管理するOSSとして、こちらを採用しました。
これを使用することで、actions workflowが実行されるごとにEC2 spot instanceが立ち上がり、インスタンスをrunnerとして登録してworkflowの処理を行い、処理が終了すると自動的にインスタンスも終了されるシステムを構築することが可能です。
さて、このシステムでインスタンスを立ち上げる際、Unityを事前にインストールした環境のAMIを作成し、そのAMIをもとにUnity実行環境のインスタンスを立ち上げる必要があります。今回は上記リポジトリのterraform-aws-github-runner/images/ubuntu-focal/github_agent.ubuntu.pkr.hcl
で提供されていたpackerコードサンプルをもとに、Unity実行可能なAMIをビルドしました。
できたもの
packer {
required_plugins {
amazon = {
version = ">= 0.0.2"
source = "github.com/hashicorp/amazon"
}
}
}
variable "runner_version" {
description = "The version (no v prefix) of the runner software to install https://github.com/actions/runner/releases. The latest release will be fetched from GitHub if not provided."
default = null
}
variable "region" {
description = "The region to build the image in"
type = string
default = "eu-west-1"
}
variable "security_group_id" {
description = "The ID of the security group Packer will associate with the builder to enable access"
type = string
default = null
}
variable "subnet_id" {
description = "If using VPC, the ID of the subnet, such as subnet-12345def, where Packer will launch the EC2 instance. This field is required if you are using an non-default VPC"
type = string
default = null
}
variable "associate_public_ip_address" {
description = "If using a non-default VPC, there is no public IP address assigned to the EC2 instance. If you specified a public subnet, you probably want to set this to true. Otherwise the EC2 instance won't have access to the internet"
type = string
default = null
}
variable "instance_type" {
description = "The instance type Packer will use for the builder"
type = string
default = "t3.medium"
}
variable "root_volume_size_gb" {
type = number
default = 8
}
variable "ebs_delete_on_termination" {
description = "Indicates whether the EBS volume is deleted on instance termination."
type = bool
default = true
}
variable "global_tags" {
description = "Tags to apply to everything"
type = map(string)
default = {}
}
variable "ami_tags" {
description = "Tags to apply to the AMI"
type = map(string)
default = {}
}
variable "snapshot_tags" {
description = "Tags to apply to the snapshot"
type = map(string)
default = {}
}
variable "custom_shell_commands" {
description = "Additional commands to run on the EC2 instance, to customize the instance, like installing packages"
type = list(string)
default = []
}
variable "temporary_security_group_source_public_ip" {
description = "When enabled, use public IP of the host (obtained from https://checkip.amazonaws.com) as CIDR block to be authorized access to the instance, when packer is creating a temporary security group. Note: If you specify `security_group_id` then this input is ignored."
type = bool
default = false
}
data "http" github_runner_release_json {
url = "https://api.github.com/repos/actions/runner/releases/latest"
request_headers = {
Accept = "application/vnd.github+json"
X-GitHub-Api-Version = "2022-11-28"
}
}
locals {
runner_version = coalesce(var.runner_version, trimprefix(jsondecode(data.http.github_runner_release_json.body).tag_name, "v"))
}
source "amazon-ebs" "githubrunner" {
ami_name = "github-runner-ubuntu-focal-amd64-${formatdate("YYYYMMDDhhmm", timestamp())}"
instance_type = var.instance_type
region = var.region
security_group_id = var.security_group_id
subnet_id = var.subnet_id
associate_public_ip_address = var.associate_public_ip_address
temporary_security_group_source_public_ip = var.temporary_security_group_source_public_ip
source_ami_filter {
filters = {
name = "*ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
ssh_username = "ubuntu"
tags = merge(
var.global_tags,
var.ami_tags,
{
OS_Version = "ubuntu-focal"
Release = "Latest"
Base_AMI_Name = "{{ .SourceAMIName }}"
})
snapshot_tags = merge(
var.global_tags,
var.snapshot_tags,
)
launch_block_device_mappings {
device_name = "/dev/sda1"
volume_type = "gp3"
volume_size = "${var.root_volume_size_gb}"
delete_on_termination = "${var.ebs_delete_on_termination}"
}
}
build {
name = "githubactions-runner"
sources = [
"source.amazon-ebs.githubrunner"
]
provisioner "shell" {
environment_vars = [
"DEBIAN_FRONTEND=noninteractive"
]
inline = concat([
"sudo cloud-init status --wait",
"sudo apt-get -y update",
"sudo apt-get -y install ca-certificates curl gnupg lsb-release",
"sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg",
"echo deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null",
"sudo apt-get -y update",
"sudo apt-get -y install docker-ce docker-ce-cli containerd.io jq git unzip",
"sudo systemctl enable containerd.service",
"sudo service docker start",
"sudo usermod -a -G docker ubuntu",
"sudo curl -f https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb -o amazon-cloudwatch-agent.deb",
"sudo dpkg -i amazon-cloudwatch-agent.deb",
"sudo systemctl restart amazon-cloudwatch-agent",
"sudo curl -f https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o awscliv2.zip",
"unzip awscliv2.zip",
"sudo ./aws/install",
], var.custom_shell_commands)
}
provisioner "file" {
content = templatefile("../../submodules/terraform-aws-github-runner/images/install-runner.sh", {
install_runner = templatefile("../../submodules/terraform-aws-github-runner/modules/runners/templates/install-runner.sh", {
ARM_PATCH = ""
S3_LOCATION_RUNNER_DISTRIBUTION = ""
RUNNER_ARCHITECTURE = "x64"
})
})
destination = "/tmp/install-runner.sh"
}
provisioner "shell" {
env = {
RUNNER_TARBALL_URL="https://github.com/actions/runner/releases/download/v${local.runner_version}/actions-runner-linux-x64-${local.runner_version}.tar.gz"
}
inline = [
"sudo chmod +x /tmp/install-runner.sh",
"echo ubuntu | tee -a /tmp/install-user.txt",
"sudo RUNNER_ARCHITECTURE=x64 RUNNER_TARBALL_URL=$RUNNER_TARBALL_URL /tmp/install-runner.sh",
"echo ImageOS=ubuntu20 | tee -a /opt/actions-runner/.env"
]
}
provisioner "file" {
content = templatefile("../../submodules/terraform-aws-github-runner/images/start-runner.sh", {
start_runner = templatefile("../../submodules/terraform-aws-github-runner/modules/runners/templates/start-runner.sh", { metadata_tags = "enabled" })
})
destination = "/tmp/start-runner.sh"
}
provisioner "shell" {
inline = [
"sudo mv /tmp/start-runner.sh /var/lib/cloud/scripts/per-boot/start-runner.sh",
"sudo chmod +x /var/lib/cloud/scripts/per-boot/start-runner.sh",
]
}
# ec2-userを追加
provisioner "file" {
source = "./defaults.cfg"
destination = "/tmp/defaults.cfg"
}
provisioner "shell" {
inline = [
"sudo mv /tmp/defaults.cfg /etc/cloud/cloud.cfg.d/defaults.cfg",
"sudo adduser ec2-user",
"sudo usermod -aG sudo ec2-user",
]
}
# runnerの権限変更
provisioner "shell" {
inline = [
"sudo chown -R ec2-user:ec2-user /opt/actions-runner/",
"sudo chown -R ec2-user:ec2-user /opt/hostedtoolcache/",
]
}
# git-lfsのインストール
provisioner "shell" {
inline = [
"sudo apt-get update && sudo apt-get install -y git-lfs",
]
}
# Unity Hubのインストール
provisioner "shell" {
inline = [
"wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | sudo tee /usr/share/keyrings/Unity_Technologies_ApS.gpg > /dev/null",
"sudo sh -c 'echo \"deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main\" > /etc/apt/sources.list.d/unityhub.list'",
"sudo apt update && sudo apt install -y unityhub",
]
}
# Unityのインストール
provisioner "shell" {
inline = [
"sudo apt-get -y install libgconf-2-4 libgtk-3-0 libgl1-mesa-dev xvfb",
"sudo mkdir -p /opt/Unity/Hub/Editor",
"sudo chown -R ubuntu:ubuntu /opt/Unity",
"xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unityhub --headless install-path --set /opt/Unity/Hub/Editor",
"xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unityhub --headless install --version 2020.3.33f1 --changeset 915a7af8b0d5 --module linux-il2cpp windows-mono ios",
"sudo chown -R ec2-user:ec2-user /opt/Unity",
]
}
# Unity floating licenseの設定
provisioner "file" {
source = "../services-config.json"
destination = "/tmp/services-config.json"
}
provisioner "shell" {
inline = [
"sudo mkdir -p /usr/share/unity3d/config",
"sudo mv /tmp/services-config.json /usr/share/unity3d/config/services-config.json",
"sudo chown -R root:root /usr/share/unity3d",
]
}
post-processor "manifest" {
output = "manifest.json"
strip_path = true
}
}
解説
前半は元リポジトリからほぼ書き換わっていません、ざっくりactions-runnerやcloudwatch agentのインストールをしてます。Unityを動作可能にするために行った変更点のみ解説します。
OSの選定
EC2 spot instanceはAWSの余剰リソースを安価に使えるオプションです。
安価に使えることのトレードオフとして、spotインスタンスはEC2が容量を必要としたときに中断される可能性があります。そのため、今回のCI環境のような
- 必要な時のみ起動する
- 途中停止しても影響が少ない
ワークロードには適しています。
Linuxインスタンスはon-demandから70% offくらいの料金で使用でき、windows/macOSと比較して安価に利用できることを確認したため、Linuxで実行可能なものはできる限りLinuxで実行する方針にしました。
現在の開発環境であるUnity2020.3.33f1の動作要件で、扱い慣れているUbuntu20.04をsource AMIとしました(amazon linux 2でUnityが実行可能かは検証していません…)。
Unity Hubのインストール
CLIからUnity Editorをインストールするには、Unity Hub経由でインストールする方法と、DLしたインストーラーからインストールする方法があります。後者の方法は公式のドキュメントで説明されています。
しかし後者の方法では、Unityのプラットフォーム依存コンパイルに必要なモジュールを追加することができません。そのため今回は、まずUnity Hubをインストールし、Hub経由で必要モジュールを含めたEditorのインストールを行いました。
# Unity Hubのインストール
provisioner "shell" {
inline = [
"wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | sudo tee /usr/share/keyrings/Unity_Technologies_ApS.gpg > /dev/null",
"sudo sh -c 'echo \"deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main\" > /etc/apt/sources.list.d/unityhub.list'",
"sudo apt update && sudo apt install -y unityhub",
]
}
こちらは公式のドキュメント通りです。
Unity Editorのインストール
# Unityのインストール
provisioner "shell" {
inline = [
"sudo apt-get -y install libgconf-2-4 libgtk-3-0 libgl1-mesa-dev xvfb",
"sudo mkdir -p /opt/Unity/Hub/Editor",
"sudo chown -R ubuntu:ubuntu /opt/Unity",
"xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unityhub --headless install-path --set /opt/Unity/Hub/Editor",
"xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unityhub --headless install --version 2020.3.33f1 --changeset 915a7af8b0d5 --module linux-il2cpp windows-mono ios",
"sudo chown -R ec2-user:ec2-user /opt/Unity",
]
}
一行ずつ解説します。
sudo apt-get -y install libgconf-2-4 libgtk-3-0 libgl1-mesa-dev xvfb
Unityの実行に必要なライブラリをインストールしています。
sudo mkdir -p /opt/Unity/Hub/Editor
sudo chown -R ubuntu:ubuntu /opt/Unity
今回は諸所の都合により、AMIをビルドする際のユーザーとrunnerを実行するユーザーが異なるという状況のため、/opt
下にUnityをインストールしてます。
上コマンドでインストール先のディレクトリを作成し、オーナーをAMIビルド時のユーザーであるubuntu
に変更しています。
EC2インスタンスはheadless環境ですが、素直に--headless
とつけてunityhubを実行しても以下のエラーが出ます。
Illegal instruction (core dumped)
そこでxvfbを使って仮想ディスプレイを作り、unityhubを実行しました(GPUまわりのエラーを吐かれましたが無視してます)。
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unityhub --headless install-path --set /opt/Unity/Hub/Editor
Unityのインストール先を先に作成したディレクトリに指定します。
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unityhub --headless install --version 2020.3.33f1 --changeset 915a7af8b0d5 --module linux-il2cpp windows-mono ios
Unity Editorをインストールします。この際、--version
で指定するインストール可能なバージョンは
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unityhub --headless editors --releases
で確認できます。
2023.3.0a15
2023.2.1f1
2022.3.14f1
2021.3.32f1
インストールしたいEditorのバージョンがここで表示されない場合、--changeset
を指定することでインストール可能になります。バージョンとchangesetの対応はUnityのRelease notesから確認可能です。
--module
からインストール可能なモジュールは
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unityhub --headless help
で確認できます。
Commands:
editors
description: list the releases and installed editors
alias: e
example: unityhub --headless editors -r
options:
[default] list of available releases and installed editors on your machine combined
--releases|-r only list of available releases promoted by Unity
--installed|-i only list of installed editors on your machine
--add <path> locating and associating an editor from a stipulated path
install-path
description: set/get the path where the Unity editors will be installed
alias: ip
example: unityhub --headless ip -s ~/Unity/Hub/Editor/
options:
--set|-s <path> set the install path to the given path
--get|-g returns the install path
install
description:
installs a new editor either from the releases list or archive
alias: i
example: unityhub --headless install --version 2019.1.11f1 --changeset 9b001d489a54
options:
--version|-v <version> editor version to be installed (e.g. 2019.1.11f1) - required
--changeset|-c <changeset> changeset of the editor if it is not in the release list (e.g. 9b001d489a54)
- required if the version is not in the releases. see editors -r
--module|-m <moduleid> see install-modules for more information
--childModules|--cm automatically installs all child modules of selected modules
install-modules
description:
download and install a module (e.g. build support) to an installed editor
alias: im
example: unityhub --headless install-modules --version 2019.1.11f1 -m ios android
options:
--version|-v <version> version of the editor to add the module to - required
--module|-m <moduleid> the module id. The followings are the available values depending on version. You can specify multiple values, separated by spaces.
For a full list, please visit https://docs.unity3d.com/hub/manual/HubCLI.html
Android Build Support: android
Android SDK & NDK Tools: android-sdk-ndk-tools
OpenJDK: android-open-jdk
iOS Build Support: ios
Linux Build Support (IL2CPP): linux-il2cpp
Mac Build Support (Mono): mac-mono
Windows Build Support (Mono): windows-mono
WebGL Build Support: webgl
Facebook Gameroom Build Support: facebook-games
Documentation: documentation
Language packs: language-ja, language-ko, language-zh-cn, language-zh-hant, language-zh-hans
--childModules|--cm automatically installs all child modules of selected modules
Unity floating licenseの設定
Unity Editor実行時のライセンスには、Unity Build Serverのfloating licenseを用いました。
こちらを参考に、Unity実行インスタンスを立ち上げるVPCとは別の、ライセンスサーバー用VPCを構築し、Peering接続によりVPC間で通信可能にしました。
# Unity floating licenseの設定
provisioner "file" {
source = "../services-config.json"
destination = "/tmp/services-config.json"
}
provisioner "shell" {
inline = [
"sudo mkdir -p /usr/share/unity3d/config",
"sudo mv /tmp/services-config.json /usr/share/unity3d/config/services-config.json",
"sudo chown -R root:root /usr/share/unity3d",
]
}
floating licenseの設定は、configファイルを指定のディレクトリに置くだけです。
おわりに
今回のCI環境構築は、こちらの記事シリーズを大いに参考にさせていただきました。
本記事で紹介したのは今回構築したシステム全体のほんの一要素です。今後機会があれば、他の部分や全体についての記事も書くかもしれません(書かないかもしれません)。
本記事が誰かの参考になれば幸いです、最後までお読みいただきありがとうございました。
参考
- https://synamon.hatenablog.com/entry/2022/09/22/153523
- https://github.com/philips-labs/terraform-aws-github-runner
- https://aws.amazon.com/jp/ec2/spot/
- https://docs.unity3d.com/ja/2020.3/Manual/InstallingUnity.html
- https://docs.unity3d.com/hub/manual/InstallHub.html#install-hub-linux
- https://forum.unity.com/threads/unable-run-the-editor-without-a-desktop-environment-in-batchmode-with-nographics.609058/
- https://docs.unity3d.com/licensing/manual/ClientConfig.html