Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Run tests
Expand All @@ -54,7 +54,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Build release binary
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
## 2.1.2

### Added

* **macOS support** on EC2 Mac, macOS 15 and later (Apple silicon, aarch64). The agent ships as a `.pkg` and runs as a launchd LaunchDaemon. Deployments use AppSpec `os: linux`. AppSpec `permissions` entries that set `acls` or `context` are rejected before the install runs; `owner`, `group` and `mode` apply. Agent self-update is not available on macOS yet.
* **Optional parallel S3 bundle downloads.** Setting `s3_download_max_concurrency` above 1 (max 64) downloads a bundle in parallel HTTP range requests of `s3_download_chunk_size` bytes (default 8MiB, minimum 64KiB). Every range is pinned to the object's ETag, or to its version when one is set, so an object replaced mid-download fails the deployment. The default of 1 keeps the single-request download.

### Fixed

* **Deployment log format matches the 1.x agent ([#430](https://github.com/aws/aws-codedeploy-agent/issues/430)).** Each `scripts.log` line is now written to `codedeploy-agent-deployments.log` as it is produced, as `[<timestamp>] [<deployment_id>]<message>`. Previously the agent copied the last 2048 bytes of script output into the log after each lifecycle event, which dropped the deployment ID, doubled the timestamp, added a blank line after each record, and lost `LifecycleEvent` headers and any older output. The failure-summary line is removed, and blank script output lines are logged as a bare `[stdout]` record in both files.
* **Command endpoints in the European Sovereign Cloud and ISO partitions.** Hosts in `eusc-*`, `us-iso-*`, `us-isob-*`, `eu-isoe-*` and `us-isof-*` regions resolved an `amazonaws.com` endpoint that does not exist. Each partition now uses its own DNS suffix.

### Security

* **(opt-in)** On macOS, `strip_loader_env_in_hooks` strips the `DYLD_*` loader variables from hook environments.

## 2.1.0

### Added

* **`RESTART` deployment mode.** The agent installs from the copy of the revision from the deployment group's last successful deployment already on the instance. If that copy is missing or incomplete, the agent downloads the revision.

### Security

* A deployment whose AppSpec file path resolves outside the application revision directory is rejected. Nested paths such as `configs/appspec.yml` are still supported.

## 2.0.1

### Fixed

* **Windows AppSpec paths with leading separators.** A `files.source` or `hooks.location` beginning with `\` (including the bare `\` form) or `/` resolved against the drive root instead of the deployment archive, failing Install events with access-denied errors. Leading separators are now stripped.

## 2.0.0

Version 2.0.0 replaces the Ruby runtime with a single statically linked binary in Rust, completely removing the dependency on a Ruby runtime.
Expand Down
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ edition = "2024"
rust-version = "1.91"
homepage = "https://aws.amazon.com/codedeploy/"
repository = "https://github.com/aws/aws-codedeploy-agent"
version = "2.1.0"
version = "2.1.2"
description = "AWS CodeDeploy agent responsible for deploying software on an individual EC2/On-prem instance"
license = "Apache-2.0"
authors = ["Amazon Web Services"]
Expand All @@ -27,7 +27,8 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "regist
tracing-appender = "0.2"
time = { version = "0.3", features = ["macros", "formatting"] }
nu-path = "0.110"
tokio = { version = "1", features = ["process", "io-util", "time", "rt", "macros", "sync"] }
# `rt-multi-thread`: parallel S3 range downloads.
tokio = { version = "1", features = ["process", "io-util", "time", "rt", "rt-multi-thread", "macros", "sync"] }
chrono = "0.4"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
# `vendored`: ship OpenSSL inside the binary so one artifact runs across
Expand All @@ -53,7 +54,7 @@ subtle = "2"
strip-ansi-escapes = "0.2"

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29", features = ["user", "fs", "signal", "process", "resource"] }
nix = { version = "0.29", features = ["user", "fs", "signal", "process", "resource", "event"] }
signal-hook = "0.3"

[target.'cfg(windows)'.dependencies]
Expand Down
29 changes: 29 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,34 @@ rustup default stable-x86_64-pc-windows-gnu
cargo build --release
```

## Building on macOS

Target: `aarch64-apple-darwin` (Apple silicon), built with the Xcode
command-line tools. `MACOSX_DEPLOYMENT_TARGET=15.0` matches the oldest
supported macOS (15 Sequoia).

```bash
xcode-select --install
rustup target add aarch64-apple-darwin

export MACOSX_DEPLOYMENT_TARGET=15.0
cargo build --release --target aarch64-apple-darwin
```

Package the binary as a `.pkg` (payload: binary, `codedeploy-local` symlink,
config template, LaunchDaemon plist; see `macos/pkg/`):

```bash
macos/pkg/build-pkg.sh target/aarch64-apple-darwin/release/codedeploy-agent aarch64 2.1.1 2.1.1-1 out/
macos/pkg/build-pkg.sh target/x86_64-apple-darwin/release/codedeploy-agent x86_64 2.1.1 2.1.1-1 out/
sudo installer -pkg out/codedeploy-agent-2.1.1-1.aarch64.pkg -target /
```

The package installs `/Library/LaunchDaemons/com.amazon.aws.codedeploy-agent.plist`
(the launchd counterpart of `systemd/codedeploy-agent.service`) and starts the
agent. `sudo launchctl print system/com.amazon.aws.codedeploy-agent` shows its
state; `sudo /opt/codedeploy-agent/bin/uninstall.sh` removes it.

## Running the Agent Locally

Build and locate the binary:
Expand Down Expand Up @@ -471,6 +499,7 @@ so `teardown` works across terminal sessions.
| `e2e-ec2-iam-session.sh` | Fresh EC2 instance | On-prem registration, INI file — IMDS bypassed |
| `e2e-ec2-concurrent.sh` | Fresh EC2 instance | IMDS, runs N parallel deployments (default 3) against the same instance |
| `e2e-windows.sh` | Fresh Windows Server EC2 | IMDS |
| _(macOS)_ | No script: EC2 Mac requires a Dedicated Host with a 24 h minimum charge. Build the `.pkg` (see [Building on macOS](#building-on-macos)), install it on a Mac instance and run a deployment by hand. | IMDS |

### Shortest path to a green E2E run

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ AWS Lambda compute platform.

The agent is licensed under [Apache License 2.0](LICENSE).

## Supported platforms

The agent is built and tested on the following operating systems. Linux and
Windows ship as rpm/deb and msi; macOS ships as a `.pkg` (see
[Building on macOS](DEVELOPMENT.md#building-on-macos)).

| Platform | Versions | Architectures | Service manager |
|---|---|---|---|
| Amazon Linux | 2, 2023 | x86_64, aarch64 | systemd |
| RHEL, Oracle Linux | 8, 9, 10 | x86_64, aarch64 | systemd |
| Rocky Linux, CentOS Stream | 9, 10 | x86_64, aarch64 | systemd |
| SLES | 15 | x86_64, aarch64 | systemd |
| Debian | 11, 12, 13 | x86_64, aarch64 | systemd |
| Ubuntu Server | 16.04–26.04 LTS, 25.04 | x86_64, aarch64 | systemd |
| Windows Server | 2016, 2019, 2022, 2025 | x86_64 | Service Control Manager |
| macOS (EC2 Mac) | 15 Sequoia, 26 Tahoe, 27 | aarch64 | launchd |

## Getting Started

### Prerequisites
Expand Down
11 changes: 11 additions & 0 deletions conf/codedeployagent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# Logging
:program_name: codedeploy-agent
:log_dir: /var/log/aws/codedeploy-agent
# macOS: the LaunchDaemon plist also writes codedeploy-agent.launchd.log into
# the default directory above; changing :log_dir does not move that file.
:verbose: false

# Capture Amazon S3 HTTP wire logs to <program_name>.aws_wire.log in log_dir
Expand Down Expand Up @@ -123,6 +125,15 @@
# :archive_max_extraction_size: "4GiB" # binary (powers of 1024)
# :archive_max_extraction_size: "4GB" # SI (powers of 1000)

# Bundle download tuning (Amazon S3 revisions). By default the agent downloads
# a bundle with a single GetObject.
# s3_download_max_concurrency: parallel HTTP range requests in flight
# (default 1, max 64). Set above 1 to download in parallel ranges.
# s3_download_chunk_size: bytes per request (default 8MiB, minimum 64KiB).
# Accepts bytes or strings like "16MB".
# :s3_download_max_concurrency: 10
# :s3_download_chunk_size: "8MiB"

# Endpoints (leave empty for defaults)
# :deploy_control_endpoint:
# :s3_endpoint_override:
Expand Down
2 changes: 1 addition & 1 deletion crates/codedeploy-commands/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "codedeploy-commands"
version = "2.1.0"
version = "2.1.2"
edition = "2024"
rust-version = "1.91"
publish = false
Expand Down
84 changes: 80 additions & 4 deletions crates/codedeploy-commands/src/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,25 @@ pub fn resolve(
service.push_str("-fips");
}

let domain = if region.starts_with("cn-") {
format!("https://{service}.{region}.{}", partition_dns_suffix(region))
}

fn partition_dns_suffix(region: &str) -> &'static str {
if region.starts_with("cn-") {
"amazonaws.com.cn"
} else if region.starts_with("eusc-") {
"amazonaws.eu"
} else if region.starts_with("us-iso-") {
"c2s.ic.gov"
} else if region.starts_with("us-isob-") {
"sc2s.sgov.gov"
} else if region.starts_with("eu-isoe-") {
"cloud.adc-e.uk"
} else if region.starts_with("us-isof-") {
"csp.hci.ic.gov"
} else {
"amazonaws.com"
};

format!("https://{service}.{region}.{domain}")
}
}

#[cfg(test)]
Expand Down Expand Up @@ -77,6 +89,70 @@ mod tests {
);
}

#[test]
fn eusc_region_endpoint() {
assert_eq!(
resolve("eusc-de-east-1", None, false, false),
"https://codedeploy-commands.eusc-de-east-1.amazonaws.eu"
);
}

#[test]
fn eusc_region_secure_endpoint() {
assert_eq!(
resolve("eusc-de-east-1", None, false, true),
"https://codedeploy-commands-secure.eusc-de-east-1.amazonaws.eu"
);
}

#[test]
fn gov_cloud_stays_on_default_domain() {
assert_eq!(
resolve("us-gov-west-1", None, false, true),
"https://codedeploy-commands-secure.us-gov-west-1.amazonaws.com"
);
}

#[test]
fn iso_region_endpoint() {
assert_eq!(
resolve("us-iso-east-1", None, false, false),
"https://codedeploy-commands.us-iso-east-1.c2s.ic.gov"
);
}

#[test]
fn iso_b_region_endpoint() {
assert_eq!(
resolve("us-isob-east-1", None, false, false),
"https://codedeploy-commands.us-isob-east-1.sc2s.sgov.gov"
);
}

#[test]
fn iso_e_region_endpoint() {
assert_eq!(
resolve("eu-isoe-west-1", None, false, false),
"https://codedeploy-commands.eu-isoe-west-1.cloud.adc-e.uk"
);
}

#[test]
fn iso_f_region_endpoint() {
assert_eq!(
resolve("us-isof-south-1", None, false, false),
"https://codedeploy-commands.us-isof-south-1.csp.hci.ic.gov"
);
}

#[test]
fn iso_region_secure_endpoint() {
assert_eq!(
resolve("us-iso-east-1", None, false, true),
"https://codedeploy-commands-secure.us-iso-east-1.c2s.ic.gov"
);
}

#[test]
fn custom_override_bypasses_construction() {
assert_eq!(
Expand Down
102 changes: 102 additions & 0 deletions launchd/com.amazon.aws.codedeploy-agent.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
AWS CodeDeploy Agent LaunchDaemon. Installed by the .pkg (macos/pkg/) to
/Library/LaunchDaemons. Mirrors systemd/codedeploy-agent.service; keep them
in step.

ProgramArguments: the systemd ExecStart string, plus `exec` so the agent
(not bash) is the PID launchd signals on bootout; without it the agent is
SIGKILLed with the process group and cannot drain. `-a` exports
BASH_EXECUTION_STRING, which hooks may read and expect to equal the Linux
string exactly, so it is reassigned before `exec`.
Do not otherwise change the string. The master runs in the foreground
(Type=simple).

RunAtLoad start at boot
KeepAlive Crashed=true Restart=on-abnormal: restart after a signal death
only. Not SuccessfulExit=false, which would loop a
clean non-zero startup exit every ThrottleInterval.
ThrottleInterval 10 RestartSec=10
ExitTimeOut 7200 TimeoutStopSec=7200: SIGTERM, then SIGKILL after
this long, so a deployment in progress can finish
(kill_agent_max_wait_time_seconds). Default is 20 s.
NumberOfFiles 65536 LimitNOFILE
Core -1 LimitCORE=infinity; daemon::core_dumps decides.
Standard*Path codedeploy-agent.launchd.log: panics and stderr
before the logger is up. A separate file because
launchd holds its fd for the job's lifetime; it
would follow the agent log's rename-rotation into
the archive and keep writing to an unlinked inode
after pruning. The agent never writes tracing to
stdout, so this file stays small. The path is fixed
here; changing log_dir in codedeployagent.yml moves
the agent's logs but not this file.

Stop is `launchctl bootout` (SIGTERM to the agent; daemon/signal.rs). No
KillMode=control-group equivalent: launchd treats the job as stopped once
the master exits, so the worker watches the master's pid (kqueue NOTE_EXIT,
daemon/worker.rs) and exits with it. Hooks run in their own group and
are orphaned only if the master is SIGKILLed at ExitTimeOut mid-hook.
-->
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.amazon.aws.codedeploy-agent</string>

<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-a</string>
<string>-c</string>
<string>[ -f /etc/profile ] &amp;&amp; source /etc/profile; BASH_EXECUTION_STRING='[ -f /etc/profile ] &amp;&amp; source /etc/profile; /opt/codedeploy-agent/bin/codedeploy-agent start'; exec /opt/codedeploy-agent/bin/codedeploy-agent start</string>
</array>

<key>UserName</key>
<string>root</string>

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<dict>
<key>Crashed</key>
<true/>
</dict>

<key>ThrottleInterval</key>
<integer>10</integer>

<key>ExitTimeOut</key>
<integer>7200</integer>

<!-- LimitCORE=infinity: leave the hard limit unbounded so the in-process
policy (daemon::core_dumps, disable_core_dumps) is the only knob, as
on Linux. -1 is RLIM_INFINITY. -->
<key>SoftResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>65536</integer>
<key>Core</key>
<integer>-1</integer>
</dict>
<key>HardResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>65536</integer>
<key>Core</key>
<integer>-1</integer>
</dict>

<key>StandardOutPath</key>
<string>/var/log/aws/codedeploy-agent/codedeploy-agent.launchd.log</string>
<key>StandardErrorPath</key>
<string>/var/log/aws/codedeploy-agent/codedeploy-agent.launchd.log</string>

<!-- Adaptive, not Background: Background puts the agent and the hook
scripts it spawns in the throttled scheduling tier (lower CPU
priority, I/O throttling), slowing deployments. -->
<key>ProcessType</key>
<string>Adaptive</string>
</dict>
</plist>
Loading
Loading