Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions terragrunt/accounts/rustc-perf-prod/collector/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions terragrunt/accounts/rustc-perf-prod/collector/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
terraform {
source = "../../../modules//rustc-perf-collector"
}

include {
path = find_in_parent_folders()
merge_strategy = "deep"
}

dependency "quota" {
config_path = "../ec2-quota"
}

inputs = {
# Reading an output creates a Terragrunt dependency on the separately
# applied quota request. The module also checks the account's *current*
# quota, so a requested-but-not-yet-approved increase cannot allocate the
# Dedicated Host and start billing it.
required_dedicated_hosts = dependency.quota.outputs.requested_value
}
10 changes: 10 additions & 0 deletions terragrunt/modules/rustc-perf-collector/_terraform.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = "~> 1.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.27"
}
}
}
52 changes: 52 additions & 0 deletions terragrunt/modules/rustc-perf-collector/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
locals {
// The project requested the M9g (Graviton 5) family on a Dedicated Host.
// Start with a 12xlarge partition: it provides 48 vCPUs and 192 GiB while
// using one of the eight 12xlarge slots listed for an empty M9g host, leaving
// capacity available for future M9g collectors.
instance_family = "m9g"
instance_type = "m9g.12xlarge"

// New instance families are not necessarily offered in every AZ. Select a
// stable AZ ID from AWS's live offerings instead of hard-coding an AZ name
// that can map differently between accounts.
availability_zone_id = try(sort(data.aws_ec2_instance_type_offerings.collector.locations)[0], null)
}

// Use Canonical's official arm64 image: Graviton cannot boot the repository's
// usual amd64 AMIs. most_recent is safe here because instance.tf ignores later
// AMI changes to avoid silently replacing a benchmark machine.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical

filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-arm64-server-*"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we switch to 26.04 from the start?

}

filter {
name = "architecture"
values = ["arm64"]
}

filter {
name = "virtualization-type"
values = ["hvm"]
}
}

data "aws_ec2_instance_type_offerings" "collector" {
filter {
name = "instance-type"
values = [local.instance_type]
}

location_type = "availability-zone-id"
}

// The quota request records the desired value; this data source reads the
// actually approved value used by the host-allocation precondition.
data "aws_servicequotas_service_quota" "m9g_hosts" {
service_code = "ec2"
quota_code = "L-9F9F275C"
}
42 changes: 42 additions & 0 deletions terragrunt/modules/rustc-perf-collector/dedicated-host.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// A Dedicated Host is an entire physical EC2 server allocated to this AWS
// account. Selecting the family rather than one instance type lets us divide
// its capacity among supported M9g sizes instead of fixing the host layout at
// allocation time. aws_ec2_host allocates an On-Demand host; it deliberately
// does not purchase a one- or three-year Dedicated Host Reservation because
// that is a separate, irreversible billing commitment with payment-term input.
resource "aws_ec2_host" "collector" {
availability_zone = aws_subnet.collector.availability_zone
instance_family = local.instance_family

// Require every instance to name this host explicitly. This prevents an
// unrelated M9g launch in the account from silently consuming benchmark
// capacity through EC2 auto-placement.
auto_placement = "off"

// M9g supports Dedicated Host recovery, so EC2 can allocate replacement
// hardware after supported power or network failures. It does not cover
// every failure mode (notably scheduled host retirement), which still needs
// operator action.
host_recovery = "on"

tags = {
Name = "rustc-perf-graviton5"
Environment = "prod"
Service = "rustc-perf"
}

lifecycle {
precondition {
condition = local.availability_zone_id != null
error_message = "${local.instance_type} is not offered in this AWS region."
}

// A quota request can exist in state while AWS is still reviewing it.
// Gate host allocation on the applied quota because billing begins when
// the Dedicated Host is allocated, even if it contains no instances.
precondition {
condition = data.aws_servicequotas_service_quota.m9g_hosts.value >= var.required_dedicated_hosts
error_message = "The Running Dedicated m9g Hosts quota increase must be approved before allocating the host."
}
}
}
70 changes: 70 additions & 0 deletions terragrunt/modules/rustc-perf-collector/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// The instance role is intentionally limited to Systems Manager. Workload
// access to rustc-perf storage or secrets should be added separately and with
// narrower policies once the collector is enrolled.
resource "aws_iam_role" "collector" {
name = "rustc-perf-collector"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}

resource "aws_iam_role_policy_attachment" "ssm" {
// This managed policy lets the *agent* establish its outbound SSM control
// channel. Human permission to start a session comes from Identity Center.
role = aws_iam_role.collector.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

// EC2 receives IAM roles through an instance-profile wrapper rather than by
// attaching aws_iam_role directly to aws_instance.
resource "aws_iam_instance_profile" "collector" {
name = "rustc-perf-collector"
role = aws_iam_role.collector.name
}

// SSM documents are account-and-region-local configuration consumed by
// Systems Manager. This one defines the default interactive shell used by
// `aws ssm start-session`; it is not a startup script for the instance.
// Managing it here makes the no-SSH access path available without a manual
// console step, runs operators as Ubuntu's normal sudo-capable user, and puts
// finite idle and total limits on forgotten sessions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can try SSM as the access mechanism to start, but we should think about whether just exposing ssh on port 22 to the bastion is reasonable. We do that for other instances we manage and it feels fairly reasonable to me.

resource "aws_ssm_document" "session_preferences" {
name = "SSM-SessionManagerRunShell"
document_format = "JSON"
document_type = "Session"

content = jsonencode({
schemaVersion = "1.0"
description = "Regional Session Manager settings for rustc-perf-prod"
sessionType = "Standard_Stream"
inputs = {
// There is no account-local log destination yet, so session contents are
// not streamed. Session API activity is still captured by CloudTrail.
s3BucketName = ""
s3KeyPrefix = ""
s3EncryptionEnabled = true
cloudWatchLogGroupName = ""
cloudWatchEncryptionEnabled = true
cloudWatchStreamingEnabled = false
kmsKeyId = ""
runAsEnabled = true
runAsDefaultUser = "ubuntu"
idleSessionTimeout = "20"
maxSessionDuration = "240"
shellProfile = {
windows = ""
linux = "cd /home/ubuntu"
}
}
})
}
67 changes: 67 additions & 0 deletions terragrunt/modules/rustc-perf-collector/instance.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
resource "aws_instance" "collector" {
ami = data.aws_ami.ubuntu.id
instance_type = local.instance_type
// host tenancy plus the explicit host ID places this instance on the
// account's M9g physical server instead of the normal shared EC2 fleet.
host_id = aws_ec2_host.collector.id
tenancy = "host"
subnet_id = aws_subnet.collector.id
vpc_security_group_ids = [aws_security_group.collector.id]
// The public address is only for outbound package, toolchain, and benchmark
// downloads. network.tf defines no ingress rules; administration goes
// through SSM. This avoids a continuously billed NAT gateway for one host.
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.collector.name

// Accidental deletion would discard a calibrated benchmark environment. A
// guest shutdown stops the partition rather than terminating its EBS volume;
// it does not stop Dedicated Host billing while the physical host is
// allocated. Detailed EC2 monitoring is unrelated to PMU counters.
disable_api_termination = true
ebs_optimized = true
instance_initiated_shutdown_behavior = "stop"
monitoring = false

// EC2 gives this script to cloud-init on the first boot. It installs the
// native perf tooling, enables unprivileged PMU access, starts the SSM agent,
// and runs a counter smoke test. It is bootstrap configuration, not a script
// rerun by every Terraform apply.
user_data = file("${path.module}/user-data.sh")

// Require IMDSv2 and keep its packets local to the host, reducing the chance
// that a benchmark process can accidentally expose instance-role credentials.
metadata_options {
http_endpoint = "enabled"
http_protocol_ipv6 = "disabled"
http_put_response_hop_limit = 1
http_tokens = "required"
instance_metadata_tags = "disabled"
}

// M9g is EBS-only. Provisioned gp3 performance reduces storage variance
// during compilation, while 500 GiB leaves room for toolchains, sources, and
// build artifacts. Encryption protects the persistent volume when stopped.
root_block_device {
delete_on_termination = true
encrypted = true
iops = 12000
throughput = 500
volume_size = 500

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12000 IOPS seems very much excessive to me. I would keep this at defaults personally unless we encounter issues. Most of the perf workload should be able to fit in memory.

500 gb is probably fine (I doubt we need that much though...).

volume_type = "gp3"
}

tags = {
Name = "rustc-perf-graviton5"
Environment = "prod"
Service = "rustc-perf"
}

lifecycle {
# Do not replace the collector just because Canonical published a new AMI.
ignore_changes = [ami]
}

// Ensure the documented SSM access method is configured before EC2 can
// register as a managed node and an operator attempts the first session.
depends_on = [aws_ssm_document.session_preferences]
}
68 changes: 68 additions & 0 deletions terragrunt/modules/rustc-perf-collector/network.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This service needs one outbound-only host, so a small dedicated VPC is easier
// to reason about than adopting a default VPC or deploying the multi-AZ/NAT
// topology used by web services.
resource "aws_vpc" "collector" {
cidr_block = "10.0.0.0/24"
enable_dns_hostnames = true
enable_dns_support = true

tags = {
Name = "rustc-perf-prod"
}
}

resource "aws_internet_gateway" "collector" {
vpc_id = aws_vpc.collector.id

tags = {
Name = "rustc-perf-prod"
}
}

resource "aws_subnet" "collector" {
availability_zone_id = local.availability_zone_id
cidr_block = "10.0.0.0/26"
map_public_ip_on_launch = true
vpc_id = aws_vpc.collector.id

tags = {
Name = "rustc-perf-prod"
}
}

resource "aws_route_table" "collector" {
vpc_id = aws_vpc.collector.id

route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.collector.id
}

tags = {
Name = "rustc-perf-prod"
}
}

resource "aws_route_table_association" "collector" {
route_table_id = aws_route_table.collector.id
subnet_id = aws_subnet.collector.id
}

// Deliberately define no ingress rules. SSM establishes its management channel
// outbound, so neither SSH nor a bastion needs to be exposed to the internet.
resource "aws_security_group" "collector" {
name = "rustc-perf-collector"
description = "No-ingress security group for the rustc-perf collector"
vpc_id = aws_vpc.collector.id

tags = {
Name = "rustc-perf-collector"
}
}

resource "aws_vpc_security_group_egress_rule" "collector" {
security_group_id = aws_security_group.collector.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
description = "Allow the collector to fetch toolchains and benchmark sources"
}
24 changes: 24 additions & 0 deletions terragrunt/modules/rustc-perf-collector/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
output "instance_id" {
description = "EC2 instance ID of the Graviton 5 collector"
value = aws_instance.collector.id
}

output "dedicated_host_id" {
description = "EC2 Dedicated Host containing the collector"
value = aws_ec2_host.collector.id
}

output "instance_type" {
description = "EC2 instance type of the collector"
value = aws_instance.collector.instance_type
}

output "availability_zone" {
description = "Availability Zone selected for the collector"
value = aws_instance.collector.availability_zone
}

output "perf_check_marker" {
description = "File created by cloud-init after hardware counters pass their smoke test"
value = "/var/lib/rustc-perf/perf-counters-ready"
}
Loading