Skip to main content

commonware_deployer/aws/
mod.rs

1//! AWS EC2 deployer
2//!
3//! Deploy a custom binary (and configuration) to any number of EC2 instances across multiple regions. View metrics and logs
4//! from all instances with Grafana.
5//!
6//! # Features
7//!
8//! * Automated creation, update, and destruction of EC2 instances across multiple regions
9//! * Provide a unique name, instance type, region, binary, and configuration for each deployed instance
10//! * Collect metrics, profiles (when enabled), and logs from all deployed instances on a long-lived monitoring instance
11//!   (accessible only to the deployer's IP)
12//!
13//! # Architecture
14//!
15//! ```txt
16//!                    Deployer's Machine (Public IP)
17//!                                  |
18//!                                  |
19//!                                  v
20//!               +-----------------------------------+
21//!               | Monitoring VPC (us-east-1)        |
22//!               |  - Monitoring Instance            |
23//!               |    - Prometheus                   |
24//!               |    - Loki                         |
25//!               |    - Pyroscope                    |
26//!               |    - Tempo                        |
27//!               |    - Grafana                      |
28//!               |    - Tracer                       |
29//!               |  - Security Group                 |
30//!               |    - All: Deployer IP             |
31//!               |    - 3100: Binary VPCs            |
32//!               |    - 4040: Binary VPCs            |
33//!               |    - 4318: Binary VPCs            |
34//!               +-----------------------------------+
35//!                     ^                       ^
36//!                (Telemetry)             (Telemetry)
37//!                     |                       |
38//!                     |                       |
39//! +------------------------------+  +------------------------------+
40//! | Binary VPC 1                 |  | Binary VPC 2                 |
41//! |  - Binary Instance           |  |  - Binary Instance           |
42//! |    - Binary A                |  |    - Binary B                |
43//! |    - Promtail                |  |    - Promtail                |
44//! |    - Node Exporter           |  |    - Node Exporter           |
45//! |    - Pyroscope Agent         |  |    - Pyroscope Agent         |
46//! |  - Security Group            |  |  - Security Group            |
47//! |    - All: Deployer IP        |  |    - All: Deployer IP        |
48//! |    - 9090: Monitoring IP     |  |    - 9090: Monitoring IP     |
49//! |    - 9100: Monitoring IP     |  |    - 9100: Monitoring IP     |
50//! |    - 8012: 0.0.0.0/0         |  |    - 8765: 12.3.7.9/32       |
51//! +------------------------------+  +------------------------------+
52//! ```
53//!
54//! ## Instances
55//!
56//! ### Monitoring
57//!
58//! * Deployed in `us-east-1` with a configurable instance type (e.g., `t4g.small` for ARM64, `t3.small` for x86_64) and storage (e.g., 10GB gp2). Architecture is auto-detected from the instance type.
59//! * Runs:
60//!     * **Prometheus**: Scrapes binary metrics at `:9090` and system metrics from all instances at `:9100`.
61//!     * **Loki**: Listens at `:3100`, storing logs in `/loki/chunks` with a TSDB index at `/loki/index`.
62//!     * **Pyroscope**: Listens at `:4040`, storing profiles in `/var/lib/pyroscope`.
63//!     * **Tempo**: Listens at `:4318`, storing traces in `/tempo`.
64//!     * **Grafana**: Hosted at `:3000`, provisioned with Prometheus, Loki, and Tempo datasources and a custom dashboard.
65//!     * **Tracer**: Hosted at `:8080`, rendering multi-instance trace flamegraphs from the local Tempo.
66//! * Ingress:
67//!     * Allows deployer IP access (TCP 0-65535).
68//!     * Binary instance traffic to Loki (TCP 3100) and Tempo (TCP 4318).
69//!
70//! ### Binary
71//!
72//! * Deployed in user-specified regions with configurable ARM64 or AMD64 instance types and storage.
73//!   Instances whose type includes EC2 NVMe instance store automatically mount it at `/home/ubuntu`.
74//! * Run:
75//!     * **Custom Binary**: Executes with `--hosts=/home/ubuntu/hosts.yaml --config=/home/ubuntu/config.conf`, exposing metrics at `:9090`.
76//!       The binary uses the default allocator on the target Linux platform.
77//!     * **Promtail**: Forwards `/var/log/binary.log` to Loki on the monitoring instance.
78//!     * **Node Exporter**: Exposes system metrics at `:9100`.
79//!     * **Pyroscope Agent**: Forwards `perf` profiles to Pyroscope on the monitoring instance.
80//! * Ingress:
81//!     * Deployer IP access (TCP 0-65535).
82//!     * Monitoring IP access to `:9090` and `:9100` for Prometheus.
83//!     * User-defined ports from the configuration.
84//!
85//! _For allocator-sensitive workloads, consider compiling the binary with `jemalloc` or
86//! `mimalloc`._
87//!
88//! ## Networking
89//!
90//! ### VPCs
91//!
92//! One per region with CIDR `10.<region-index>.0.0/16` (e.g., `10.0.0.0/16` for `us-east-1`).
93//!
94//! ### Subnets
95//!
96//! One subnet per availability zone that supports any required instance type in the region
97//! (e.g., `10.<region-index>.<az-index>.0/24`), linked to a shared route table with an internet gateway.
98//! Each instance is placed in an AZ that supports its instance type, distributed round-robin across
99//! eligible AZs, with automatic fallback to other AZs on capacity errors.
100//! Instances that set the same `availability_zone_group` and region are launched in one
101//! mutually-supported AZ.
102//!
103//! ### VPC Peering
104//!
105//! Connects the monitoring VPC to each binary VPC, with routes added to route tables for private communication.
106//!
107//! ### Security Groups
108//!
109//! Separate for monitoring (tag) and binary instances (`{tag}-binary`), dynamically configured for deployer and inter-instance traffic.
110//!
111//! # Workflow
112//!
113//! ## Lifecycle
114//!
115//! Deployments are managed through `aws create`, `aws update`, and `aws destroy`. Stopping,
116//! starting, or rebooting EC2 instances outside this lifecycle is not supported. Deployment
117//! metadata, generated host files, security group rules, monitoring scrape targets, and service
118//! configuration are derived from the instance addresses observed during `aws create`.
119//! Additionally, instance types with EC2 NVMe instance store use ephemeral storage mounted at
120//! `/home/ubuntu`.
121//!
122//! ## `aws create`
123//!
124//! 1. Validates configuration and generates an SSH key pair, stored in `$HOME/.commonware_deployer/{tag}/id_rsa_{tag}`.
125//! 2. Persists deployment metadata (tag, regions, instance names) to `$HOME/.commonware_deployer/{tag}/metadata.yaml`.
126//!    This enables `destroy --tag` cleanup if creation fails.
127//! 3. Ensures the shared S3 bucket exists and caches tools if not already present.
128//! 4. Caches required container images as `docker save` tarballs in S3 (one per architecture) if not already present.
129//! 5. Uploads deployment-specific files (binaries, configs) to S3.
130//! 6. Creates VPCs, subnets, internet gateways, route tables, and security groups per region (concurrently).
131//! 7. Establishes VPC peering between the monitoring region and binary regions.
132//! 8. Launches the monitoring instance.
133//! 9. Launches binary instances.
134//! 10. Caches all static config files and uploads per-instance configs (hosts.yaml, Promtail, Pyroscope) to S3.
135//! 11. Configures monitoring and binary instances in parallel via SSH (BBR, service installation, service startup).
136//! 12. Updates the monitoring security group to allow telemetry traffic from binary instances.
137//! 13. Marks completion with `$HOME/.commonware_deployer/{tag}/created`.
138//!
139//! ## `aws update`
140//!
141//! Performs rolling updates across all binary instances:
142//!
143//! 1. Uploads the latest binary and configuration to S3.
144//! 2. For each instance (up to `--concurrency` at a time, default 128):
145//!    a. Stops the `binary` service.
146//!    b. Downloads the updated files from S3 via pre-signed URLs.
147//!    c. Restarts the `binary` service.
148//!    d. Waits for the service to become active before proceeding.
149//!
150//! _Use `--concurrency 1` for fully sequential updates that wait for each instance to be healthy
151//! before updating the next._
152//!
153//! ## `aws authorize`
154//!
155//! 1. Obtains the deployer's current public IP address (or parses the one provided).
156//! 2. For each security group in the deployment, adds an ingress rule for the IP (if it doesn't already exist).
157//!
158//! ## `aws destroy`
159//!
160//! Can be invoked with either `--config <path>` or `--tag <tag>`. When using `--tag`, the command
161//! reads regions from the persisted `metadata.yaml` file, allowing destruction without the original
162//! config file.
163//!
164//! 1. Terminates all instances across regions.
165//! 2. Deletes security groups, subnets, route tables, VPC peering connections, internet gateways, key pairs, and VPCs in dependency order.
166//! 3. Deletes deployment-specific data from S3 (cached tools and images remain for future deployments).
167//! 4. Marks destruction with `$HOME/.commonware_deployer/{tag}/destroyed`, retaining the directory to prevent tag reuse.
168//!
169//! ## `aws clean`
170//!
171//! 1. Deletes the shared S3 bucket and all its contents (cached tools, image tarballs, and any remaining deployment data).
172//! 2. Use this to fully clean up when you no longer need the deployer cache.
173//!
174//! ## `aws list`
175//!
176//! Lists all active deployments (created but not destroyed). For each deployment, displays the tag,
177//! creation timestamp, regions, and number of instances.
178//!
179//! ## `aws profile`
180//!
181//! 1. Loads the deployment configuration and locates the specified instance.
182//! 2. Caches the samply binary in S3 if not already present.
183//! 3. SSHes to the instance, downloads samply, and records a CPU profile of the running binary for the specified duration.
184//! 4. Downloads the profile locally via SCP.
185//! 5. Opens Firefox Profiler with symbols resolved from your local debug binary.
186//!
187//! # Profiling
188//!
189//! The deployer supports two profiling modes:
190//!
191//! ## Continuous Profiling (Pyroscope)
192//!
193//! Enable continuous CPU profiling by setting `profiling: true` in your instance config. This runs
194//! Pyroscope in the background, continuously collecting profiles that are viewable in the Grafana
195//! dashboard on the monitoring instance.
196//!
197//! For best results, build and deploy your binary with debug symbols and frame pointers:
198//!
199//! ```bash
200//! CARGO_PROFILE_RELEASE_DEBUG=true RUSTFLAGS="-C force-frame-pointers=yes" cargo build --release
201//! ```
202//!
203//! ## On-Demand Profiling (samply)
204//!
205//! To generate an on-demand CPU profile (viewable in the Firefox Profiler UI), run the
206//! following:
207//!
208//! ```bash
209//! deployer aws profile --config config.yaml --instance <name> --binary <path-to-binary-with-debug>
210//! ```
211//!
212//! This captures a 30-second profile (configurable with `--duration`) using samply on the remote
213//! instance, downloads it, and opens it in Firefox Profiler. Unlike Continuous Profiling, this mode
214//! does not require deploying a binary with debug symbols (reducing deployment time).
215//!
216//! Like above, build your binary with debug symbols (but not frame pointers):
217//!
218//! ```bash
219//! CARGO_PROFILE_RELEASE_DEBUG=true cargo build --release
220//! ```
221//!
222//! Now, strip symbols and deploy via `aws create` (preserve the original binary for profile symbolication
223//! when you run the `aws profile` command shown above):
224//!
225//! ```bash
226//! cp target/release/my-binary target/release/my-binary-debug
227//! strip target/release/my-binary
228//! ```
229//!
230//! # Persistence
231//!
232//! * A directory `$HOME/.commonware_deployer/{tag}` stores:
233//!   * SSH private key (`id_rsa_{tag}`)
234//!   * Deployment metadata (`metadata.yaml`) containing tag, creation timestamp, regions, and instance names
235//!   * Status files (`created`, `destroyed`)
236//! * The deployment state is tracked via these files, ensuring operations respect prior create/destroy actions.
237//! * The `metadata.yaml` file enables `aws destroy --tag` and `aws list` to work without the original config file.
238//!
239//! ## S3 Caching
240//!
241//! A shared S3 cache avoids repeated downloads from upstream sources. The cache name is stored in
242//! `$HOME/.commonware_deployer/bucket` and reused by later deployments.
243//!
244//! 1. **Faster deployments**: Tools are downloaded from upstream sources once and cached in S3.
245//!    Required container images are saved (via `docker save`) as per-architecture tarballs once and
246//!    `docker load`ed during instance setup, so instances never authenticate against a registry.
247//!
248//! 2. **Reduced bandwidth**: Instead of requiring the deployer to push binaries to each instance,
249//!    unique binaries are uploaded once to S3 and then pulled from there.
250//!
251//! Per-deployment data (binaries, configs, hosts files) is isolated under `deployments/{tag}/` to prevent
252//! conflicts between concurrent deployments.
253//!
254//! The bucket stores:
255//!   * `tools/binaries/{tool}/{version}/{platform}/{filename}` - Tool binaries and packages
256//!   * `tools/binaries/images/{image}/{platform}/image.tar.gz` - Cached container image tarballs
257//!   * `tools/configs/{deployer-version}/{component}/{file}` - Static configs and service files
258//!   * `deployments/{tag}/{kind}/{digest}` - Deployment-specific files, deduplicated by content
259//!     digest (kinds: `binaries`, `configs`, `hosts`, `promtail`, `pyroscope`, `monitoring`)
260//!
261//! Cached packages and images are namespaced by version and platform. Static configs are namespaced
262//! by deployer version to ensure cache invalidation when the deployer is updated.
263//!
264//! # Example Configuration
265//!
266//! ```yaml
267//! tag: ffa638a0-991c-442c-8ec4-aa4e418213a5
268//! monitoring:
269//!   instance_type: t4g.small  # ARM64 (Graviton)
270//!   storage_size: 10
271//!   storage_class: gp2
272//!   # storage_iops: 3000  # Required for io1/io2, optional for gp3.
273//!   # storage_throughput: 125  # Optional for gp3.
274//!   dashboard: /path/to/dashboard.json
275//! instances:
276//!   - name: node1
277//!     region: us-east-1
278//!     instance_type: t4g.small  # ARM64 (Graviton)
279//!     storage_size: 10
280//!     storage_class: gp2
281//!     # storage_iops: 3000
282//!     # storage_throughput: 125
283//!     binary: /path/to/binary-arm64
284//!     config: /path/to/config.conf
285//!     profiling: true
286//!   - name: node2
287//!     region: us-west-2
288//!     instance_type: t3.small  # x86_64 (Intel/AMD)
289//!     storage_size: 10
290//!     storage_class: gp2
291//!     # storage_iops: 3000
292//!     # storage_throughput: 125
293//!     binary: /path/to/binary-x86
294//!     config: /path/to/config2.conf
295//!     profiling: false
296//! ports:
297//!   - protocol: tcp
298//!     port: 4545
299//!     cidr: 0.0.0.0/0
300//! ```
301
302use serde::{Deserialize, Serialize};
303use std::net::IpAddr;
304
305cfg_if::cfg_if! {
306    if #[cfg(feature = "aws")] {
307        use std::path::PathBuf;
308        use thiserror::Error;
309
310        /// CPU architecture for EC2 instances
311        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
312        pub enum Architecture {
313            Arm64,
314            X86_64,
315        }
316
317        impl Architecture {
318            /// Returns the architecture string used in AMI names, download URLs, and labels
319            pub const fn as_str(&self) -> &'static str {
320                match self {
321                    Self::Arm64 => "arm64",
322                    Self::X86_64 => "amd64",
323                }
324            }
325        }
326
327        impl std::fmt::Display for Architecture {
328            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329                f.write_str(self.as_str())
330            }
331        }
332
333        /// Metadata persisted during deployment creation
334        #[derive(Serialize, Deserialize)]
335        pub struct Metadata {
336            pub tag: String,
337            pub created_at: u64,
338            pub regions: Vec<String>,
339            pub instance_count: usize,
340        }
341
342        mod create;
343        pub mod ec2;
344        mod images;
345        pub mod services;
346        pub use create::create;
347        mod update;
348        pub use update::update;
349        mod authorize;
350        pub use authorize::authorize;
351        mod destroy;
352        pub use destroy::destroy;
353        mod clean;
354        pub use clean::clean;
355        mod profile;
356        pub use profile::profile;
357        mod attach;
358        pub use attach::attach;
359        mod list;
360        pub use list::list;
361        pub mod s3;
362        pub mod utils;
363
364        /// Name of the monitoring instance
365        const MONITORING_NAME: &str = "monitoring";
366
367        /// AWS region where monitoring instances are deployed
368        const MONITORING_REGION: &str = "us-east-1";
369
370        /// File name that indicates the deployment completed
371        const CREATED_FILE_NAME: &str = "created";
372
373        /// File name that indicates the deployment was destroyed
374        const DESTROYED_FILE_NAME: &str = "destroyed";
375
376        /// File name for deployment metadata
377        const METADATA_FILE_NAME: &str = "metadata.yaml";
378
379        /// Port on instance where system metrics are exposed
380        const SYSTEM_PORT: u16 = 9100;
381
382        /// Port on monitoring where logs are pushed
383        const LOGS_PORT: u16 = 3100;
384
385        /// Port on monitoring where profiles are pushed
386        const PROFILES_PORT: u16 = 4040;
387
388        /// Port on monitoring where traces are pushed
389        const TRACES_PORT: u16 = 4318;
390
391        /// Maximum instances to manipulate at one time
392        pub const DEFAULT_CONCURRENCY: &str = "128";
393
394        /// Subcommand name
395        pub const CMD: &str = "aws";
396
397        /// Create subcommand name
398        pub const CREATE_CMD: &str = "create";
399
400        /// Update subcommand name
401        pub const UPDATE_CMD: &str = "update";
402
403        /// Authorize subcommand name
404        pub const AUTHORIZE_CMD: &str = "authorize";
405
406        /// Destroy subcommand name
407        pub const DESTROY_CMD: &str = "destroy";
408
409        /// Clean subcommand name
410        pub const CLEAN_CMD: &str = "clean";
411
412        /// Profile subcommand name
413        pub const PROFILE_CMD: &str = "profile";
414
415        /// Attach subcommand name
416        pub const ATTACH_CMD: &str = "attach";
417
418        /// List subcommand name
419        pub const LIST_CMD: &str = "list";
420
421        /// Directory where deployer files are stored
422        fn deployer_directory(tag: Option<&str>) -> PathBuf {
423            let base_dir = std::env::var("HOME").expect("$HOME is not configured");
424            let path = PathBuf::from(base_dir).join(".commonware_deployer");
425            match tag {
426                Some(tag) => path.join(tag),
427                None => path,
428            }
429        }
430
431        /// S3 operations that can fail
432        #[derive(Debug, Clone, Copy)]
433        pub enum S3Operation {
434            CreateBucket,
435            DeleteBucket,
436            HeadObject,
437            ListObjects,
438            DeleteObjects,
439        }
440
441        /// Reasons why accessing a bucket may be forbidden
442        #[derive(Debug, Clone, Copy)]
443        pub enum BucketForbiddenReason {
444            /// Access denied (missing s3:ListBucket permission or bucket owned by another account)
445            AccessDenied,
446        }
447
448        impl std::fmt::Display for BucketForbiddenReason {
449            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450                match self {
451                    Self::AccessDenied => write!(
452                        f,
453                        "access denied (check IAM permissions or bucket ownership)"
454                    ),
455                }
456            }
457        }
458
459        impl std::fmt::Display for S3Operation {
460            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461                match self {
462                    Self::CreateBucket => write!(f, "CreateBucket"),
463                    Self::DeleteBucket => write!(f, "DeleteBucket"),
464                    Self::HeadObject => write!(f, "HeadObject"),
465                    Self::ListObjects => write!(f, "ListObjects"),
466                    Self::DeleteObjects => write!(f, "DeleteObjects"),
467                }
468            }
469        }
470
471        /// Errors that can occur when deploying infrastructure on AWS
472        #[derive(Error, Debug)]
473        pub enum Error {
474            #[error("AWS EC2 error: {0}")]
475            AwsEc2(#[from] aws_sdk_ec2::Error),
476            #[error("AWS security group ingress error: {0}")]
477            AwsSecurityGroupIngress(#[from] aws_sdk_ec2::operation::authorize_security_group_ingress::AuthorizeSecurityGroupIngressError),
478            #[error("AWS describe instances error: {0}")]
479            AwsDescribeInstances(
480                #[from] aws_sdk_ec2::operation::describe_instances::DescribeInstancesError,
481            ),
482            #[error("S3 operation failed: {operation} on bucket '{bucket}'")]
483            AwsS3 {
484                bucket: String,
485                operation: S3Operation,
486                #[source]
487                source: Box<aws_sdk_s3::Error>,
488            },
489            #[error("S3 bucket '{bucket}' forbidden: {reason}")]
490            S3BucketForbidden {
491                bucket: String,
492                reason: BucketForbiddenReason,
493            },
494            #[error("IO error: {0}")]
495            Io(#[from] std::io::Error),
496            #[error("YAML error: {0}")]
497            Yaml(#[from] serde_yaml::Error),
498            #[error("creation already attempted")]
499            CreationAttempted,
500            #[error("invalid tag (must match [A-Za-z0-9_-]+): {0}")]
501            InvalidTag(String),
502            #[error("invalid instance name (must match [A-Za-z0-9_-]+ and not be `monitoring`): {0}")]
503            InvalidInstanceName(String),
504            #[error("invalid storage class for {target}: {storage_class}")]
505            InvalidStorageClass {
506                target: String,
507                storage_class: String,
508            },
509            #[error("storage_iops is required for {target} when storage_class is {storage_class}")]
510            MissingStorageIops {
511                target: String,
512                storage_class: String,
513            },
514            #[error(
515                "storage_iops for {target} is invalid for storage_class {storage_class}: {storage_iops}"
516            )]
517            InvalidStorageIops {
518                target: String,
519                storage_class: String,
520                storage_iops: i32,
521            },
522            #[error(
523                "storage_throughput is only supported for {target} when storage_class is gp3: {storage_class}"
524            )]
525            UnsupportedStorageThroughput {
526                target: String,
527                storage_class: String,
528            },
529            #[error(
530                "storage_throughput for {target} must be between 125 and 2000 MiB/s: {storage_throughput}"
531            )]
532            InvalidStorageThroughput {
533                target: String,
534                storage_throughput: i32,
535            },
536            #[error("reqwest error: {0}")]
537            Reqwest(#[from] reqwest::Error),
538            #[error("SSH failed")]
539            SshFailed,
540            #[error("keygen failed")]
541            KeygenFailed,
542            #[error("service timeout({0}): {1}")]
543            ServiceTimeout(String, String),
544            #[error("deployment does not exist: {0}")]
545            DeploymentDoesNotExist(String),
546            #[error("deployment is not complete: {0}")]
547            DeploymentNotComplete(String),
548            #[error("deployment already destroyed: {0}")]
549            DeploymentAlreadyDestroyed(String),
550            #[error("private key not found")]
551            PrivateKeyNotFound,
552            #[error("invalid IP address: {0}")]
553            IpAddrParse(#[from] std::net::AddrParseError),
554            #[error("IP address is not IPv4: {0}")]
555            IpAddrNotV4(std::net::IpAddr),
556            #[error("download failed: {0}")]
557            DownloadFailed(String),
558            #[error("command failed: {command}: {stderr}")]
559            CommandFailed { command: String, stderr: String },
560            #[error("S3 presigning config error: {0}")]
561            S3PresigningConfig(#[from] aws_sdk_s3::presigning::PresigningConfigError),
562            #[error("S3 presigning failed: {0}")]
563            S3PresigningFailed(
564                Box<aws_sdk_s3::error::SdkError<aws_sdk_s3::operation::get_object::GetObjectError>>,
565            ),
566            #[error("S3 builder error: {0}")]
567            S3Builder(#[from] aws_sdk_s3::error::BuildError),
568            #[error("duplicate instance name: {0}")]
569            DuplicateInstanceName(String),
570            #[error("instance not found: {0}")]
571            InstanceNotFound(String),
572            #[error("symbolication failed: {0}")]
573            Symbolication(String),
574            #[error("no subnet supports instance type: {0}")]
575            UnsupportedInstanceType(String),
576            #[error("no subnets available")]
577            NoSubnetsAvailable,
578            #[error(
579                "availability zone group '{group}' in region '{region}' has no mutually-supported AZ for instance types {instance_types:?}"
580            )]
581            AvailabilityZoneGroupUnsupported {
582                region: String,
583                group: String,
584                instance_types: Vec<String>,
585            },
586            #[error("metadata not found for deployment: {0}")]
587            MetadataNotFound(String),
588            #[error("must specify either --config or --tag")]
589            MissingTagOrConfig,
590            #[error("regions not enabled: {0:?}")]
591            RegionsNotEnabled(Vec<String>),
592        }
593
594        impl From<aws_sdk_s3::error::SdkError<aws_sdk_s3::operation::get_object::GetObjectError>>
595            for Error
596        {
597            fn from(
598                err: aws_sdk_s3::error::SdkError<aws_sdk_s3::operation::get_object::GetObjectError>,
599            ) -> Self {
600                Self::S3PresigningFailed(Box::new(err))
601            }
602        }
603    }
604}
605
606/// Port on binary where metrics are exposed
607pub const METRICS_PORT: u16 = 9090;
608
609/// Host deployment information
610#[derive(Serialize, Deserialize, Clone)]
611pub struct Host {
612    /// Name of the host
613    pub name: String,
614
615    /// Region where the host is deployed
616    pub region: String,
617
618    /// Public IP address of the host
619    pub ip: IpAddr,
620}
621
622/// Instance IP addresses.
623#[derive(Serialize, Deserialize, Clone)]
624pub struct Ips {
625    /// Public IP address of the instance.
626    pub public: IpAddr,
627
628    /// Private IP address of the instance.
629    pub private: IpAddr,
630}
631
632/// List of hosts
633#[derive(Serialize, Deserialize, Clone)]
634pub struct Hosts {
635    /// Public and private IP addresses of the monitoring instance.
636    pub monitoring: Ips,
637
638    /// Hosts deployed across all regions
639    pub hosts: Vec<Host>,
640}
641
642/// Port configuration
643#[derive(Serialize, Deserialize, Clone)]
644pub struct PortConfig {
645    /// Protocol (e.g., "tcp")
646    pub protocol: String,
647
648    /// Port number
649    pub port: u16,
650
651    /// CIDR block
652    pub cidr: String,
653}
654
655/// Instance configuration
656#[derive(Serialize, Deserialize, Clone)]
657pub struct InstanceConfig {
658    /// Name of the instance.
659    ///
660    /// Must be unique ignoring case, must not be `monitoring`, and must match `[A-Za-z0-9_-]+`.
661    pub name: String,
662
663    /// AWS region where the instance is deployed
664    pub region: String,
665
666    /// Optional group whose instances in the same region must launch in the same availability zone.
667    ///
668    /// The same group name may be reused in different regions, but each region is resolved
669    /// independently and the group will not span regions.
670    #[serde(default, skip_serializing_if = "Option::is_none")]
671    pub availability_zone_group: Option<String>,
672
673    /// Instance type (e.g., `t4g.small` for ARM64, `t3.small` for x86_64)
674    pub instance_type: String,
675
676    /// Storage size in GB
677    pub storage_size: i32,
678
679    /// Storage class (e.g., "gp2")
680    pub storage_class: String,
681
682    /// Provisioned IOPS for volumes that support it
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub storage_iops: Option<i32>,
685
686    /// Provisioned throughput in MiB/s for volumes that support it
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub storage_throughput: Option<i32>,
689
690    /// Path to the binary to deploy
691    pub binary: String,
692
693    /// Path to the binary configuration file
694    pub config: String,
695
696    /// Whether to enable profiling
697    pub profiling: bool,
698}
699
700/// Monitoring configuration
701#[derive(Serialize, Deserialize, Clone)]
702pub struct MonitoringConfig {
703    /// Instance type (e.g., `t4g.small` for ARM64, `t3.small` for x86_64)
704    pub instance_type: String,
705
706    /// Storage size in GB
707    pub storage_size: i32,
708
709    /// Storage class (e.g., "gp2")
710    pub storage_class: String,
711
712    /// Provisioned IOPS for volumes that support it
713    #[serde(default, skip_serializing_if = "Option::is_none")]
714    pub storage_iops: Option<i32>,
715
716    /// Provisioned throughput in MiB/s for volumes that support it
717    #[serde(default, skip_serializing_if = "Option::is_none")]
718    pub storage_throughput: Option<i32>,
719
720    /// Path to a custom dashboard file that is automatically
721    /// uploaded to grafana
722    pub dashboard: String,
723}
724
725/// Deployer configuration
726#[derive(Serialize, Deserialize, Clone)]
727pub struct Config {
728    /// Unique tag for the deployment.
729    ///
730    /// Must match `[A-Za-z0-9_-]+`.
731    pub tag: String,
732
733    /// Monitoring instance configuration
734    pub monitoring: MonitoringConfig,
735
736    /// Instance configurations
737    pub instances: Vec<InstanceConfig>,
738
739    /// Ports open on all instances
740    pub ports: Vec<PortConfig>,
741}