Add AWS CLI module for Coder Registry

- Installs AWS CLI v2 in Coder workspaces
- Supports x86_64 and ARM64 architectures with auto-detection
- Allows version pinning or installs latest version
- Optional GPG signature verification for security
- Idempotent installation (skips if already installed)
- Supports custom installation directories
- Includes comprehensive tests (Terraform + TypeScript)
- Full documentation with multiple usage examples
This commit is contained in:
Austen Bruhn 2025-11-15 16:47:07 -07:00
parent 583918bfef
commit fdaaba7378
5 changed files with 556 additions and 0 deletions

View File

@ -0,0 +1,162 @@
---
display_name: AWS CLI
description: Install the AWS Command Line Interface in your workspace
icon: ../../../../.icons/aws.svg
verified: false
tags: [aws, cli, tools]
---
# AWS CLI
Automatically install the [AWS Command Line Interface (CLI)](https://aws.amazon.com/cli/) in a Coder workspace. The AWS CLI is a unified tool to manage AWS services from the command line.
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
## Features
- ✅ Supports both x86_64 and ARM64 (aarch64) architectures
- ✅ Automatic architecture detection
- ✅ Install latest version or pin to a specific version
- ✅ Optional GPG signature verification
- ✅ Idempotent - skips installation if already present
- ✅ Supports custom installation directories
## Examples
### Basic Installation (Latest Version)
Install the latest version of AWS CLI:
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
### Pin to Specific Version
Install a specific version of AWS CLI for consistency across your team:
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
# Pin to AWS CLI version 2.15.0
aws_cli_version = "2.15.0"
}
```
Note: Find available versions in the [AWS CLI v2 Changelog](https://github.com/aws/aws-cli/blob/v2/CHANGELOG.rst).
### Custom Installation Directory
Install AWS CLI to a custom directory (useful when you don't have sudo access):
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
install_directory = "/home/coder/.local"
}
```
### Verify GPG Signature
Enable GPG signature verification for enhanced security:
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
verify_signature = true
}
```
### Specify Architecture
Explicitly set the architecture (usually auto-detected):
```tf
module "aws-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aws-cli/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
architecture = "aarch64" # or "x86_64"
}
```
## Configuration
After installing AWS CLI, users will need to configure their AWS credentials. This can be done using:
```bash
aws configure
```
Or by setting environment variables:
```bash
export AWS_ACCESS_KEY_ID="your-access-key-id"
export AWS_SECRET_ACCESS_KEY="your-secret-access-key"
export AWS_DEFAULT_REGION="us-east-1"
```
For more information, see the [AWS CLI Configuration Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html).
## Variables
| Name | Description | Default | Required |
| ------------------- | -------------------------------------------------------------------------------------------- | -------------- | -------- |
| `agent_id` | The ID of a Coder agent | - | Yes |
| `aws_cli_version` | The version of AWS CLI to install (leave empty for latest) | `""` | No |
| `install_directory` | The directory to install AWS CLI to | `/usr/local` | No |
| `architecture` | The architecture to install AWS CLI for (`x86_64` or `aarch64`, empty for auto-detection) | `""` | No |
| `verify_signature` | Whether to verify the GPG signature of the downloaded installer | `false` | No |
## Outputs
| Name | Description |
| ----------------- | ------------------------------------------------------------------------------------ |
| `aws_cli_version` | The version of AWS CLI that was installed (or 'latest' if no version was specified) |
## Requirements
- Linux operating system (x86_64 or ARM64)
- `curl` for downloading the installer
- `unzip` for extracting the installer
- `sudo` access (if installing to system directories like `/usr/local`)
- Optional: `gpg` (if using signature verification)
## Supported Platforms
- Amazon Linux 1 & 2
- CentOS
- Fedora
- Ubuntu
- Debian
- Any Linux distribution with glibc, groff, and less
## Notes
- The module is idempotent - if AWS CLI is already installed with the correct version, it will skip the installation
- When no version is specified, the latest version will be installed
- The installer automatically creates a symlink at `/usr/local/bin/aws` (or `$INSTALL_DIRECTORY/bin/aws`)
- Architecture is automatically detected based on `uname -m` if not explicitly specified

View File

@ -0,0 +1,117 @@
run "required_vars" {
command = plan
variables {
agent_id = "test-agent-id"
}
}
run "with_version" {
command = plan
variables {
agent_id = "test-agent-id"
aws_cli_version = "2.15.0"
}
assert {
condition = resource.coder_script.aws-cli.script != ""
error_message = "coder_script must have a valid script"
}
}
run "custom_install_directory" {
command = plan
variables {
agent_id = "test-agent-id"
install_directory = "/home/coder/.local"
}
assert {
condition = resource.coder_script.aws-cli.script != ""
error_message = "coder_script must have a valid script"
}
}
run "architecture_validation_x86_64" {
command = plan
variables {
agent_id = "test-agent-id"
architecture = "x86_64"
}
assert {
condition = resource.coder_script.aws-cli.script != ""
error_message = "coder_script must have a valid script"
}
}
run "architecture_validation_aarch64" {
command = plan
variables {
agent_id = "test-agent-id"
architecture = "aarch64"
}
assert {
condition = resource.coder_script.aws-cli.script != ""
error_message = "coder_script must have a valid script"
}
}
run "architecture_validation_invalid" {
command = plan
variables {
agent_id = "test-agent-id"
architecture = "invalid"
}
expect_failures = [
var.architecture
]
}
run "verify_signature" {
command = plan
variables {
agent_id = "test-agent-id"
verify_signature = true
}
assert {
condition = resource.coder_script.aws-cli.script != ""
error_message = "coder_script must have a valid script"
}
}
run "output_version_default" {
command = plan
variables {
agent_id = "test-agent-id"
}
assert {
condition = output.aws_cli_version == "latest"
error_message = "output version should be 'latest' when no version is specified"
}
}
run "output_version_specified" {
command = plan
variables {
agent_id = "test-agent-id"
aws_cli_version = "2.15.0"
}
assert {
condition = output.aws_cli_version == "2.15.0"
error_message = "output version should match the specified version"
}
}

View File

@ -0,0 +1,54 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("aws-cli", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("default output version is 'latest'", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state.outputs.aws_cli_version.value).toBe("latest");
});
it("output version matches specified version", async () => {
const version = "2.15.0";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
aws_cli_version: version,
});
expect(state.outputs.aws_cli_version.value).toBe(version);
});
it("accepts custom install directory", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
install_directory: "/home/coder/.local",
});
expect(state.resources).toHaveLength(1);
});
it("accepts architecture parameter", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
architecture: "x86_64",
});
expect(state.resources).toHaveLength(1);
});
it("accepts verify_signature parameter", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
verify_signature: "true",
});
expect(state.resources).toHaveLength(1);
});
});

View File

@ -0,0 +1,61 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "aws_cli_version" {
type = string
description = "The version of AWS CLI to install. Leave empty for latest."
default = ""
}
variable "install_directory" {
type = string
description = "The directory to install AWS CLI to."
default = "/usr/local"
}
variable "architecture" {
type = string
description = "The architecture to install AWS CLI for. Valid values are 'x86_64' and 'aarch64'. Leave empty for auto-detection."
default = ""
validation {
condition = var.architecture == "" || var.architecture == "x86_64" || var.architecture == "aarch64"
error_message = "The 'architecture' variable must be one of: '', 'x86_64', 'aarch64'."
}
}
variable "verify_signature" {
type = bool
description = "Whether to verify the GPG signature of the downloaded installer."
default = false
}
resource "coder_script" "aws-cli" {
agent_id = var.agent_id
display_name = "AWS CLI"
icon = "/icon/aws.svg"
script = templatefile("${path.module}/run.sh", {
VERSION : var.aws_cli_version,
INSTALL_DIRECTORY : var.install_directory,
ARCHITECTURE : var.architecture,
VERIFY_SIGNATURE : var.verify_signature
})
run_on_start = true
}
output "aws_cli_version" {
description = "The version of AWS CLI that was installed (or 'latest' if no version was specified)."
value = var.aws_cli_version != "" ? var.aws_cli_version : "latest"
}

View File

@ -0,0 +1,162 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="${VERSION}"
INSTALL_DIRECTORY="${INSTALL_DIRECTORY}"
ARCHITECTURE="${ARCHITECTURE}"
VERIFY_SIGNATURE="${VERIFY_SIGNATURE}"
# Check if AWS CLI is already installed
if command -v aws >/dev/null 2>&1; then
INSTALLED_VERSION=$(aws --version 2>&1 | awk '{print $1}' | cut -d'/' -f2)
echo " AWS CLI is already installed (version $INSTALLED_VERSION)"
# If a specific version was requested, check if it matches
if [ -n "$VERSION" ] && [ "$INSTALLED_VERSION" != "$VERSION" ]; then
echo "⚠️ Installed version ($INSTALLED_VERSION) does not match requested version ($VERSION)"
echo "🔄 Reinstalling AWS CLI..."
else
echo "✅ AWS CLI installation is up to date"
exit 0
fi
fi
# Detect architecture if not specified
if [ -z "$ARCHITECTURE" ]; then
ARCH=$(uname -m)
case $ARCH in
x86_64)
ARCHITECTURE="x86_64"
;;
aarch64 | arm64)
ARCHITECTURE="aarch64"
;;
*)
echo "❌ Unsupported architecture: $ARCH"
exit 1
;;
esac
fi
echo "🔍 Detected architecture: $ARCHITECTURE"
# Construct download URL
if [ -n "$VERSION" ]; then
ZIP_FILE="awscli-exe-linux-$ARCHITECTURE-$VERSION.zip"
DOWNLOAD_URL="https://awscli.amazonaws.com/$ZIP_FILE"
else
ZIP_FILE="awscli-exe-linux-$ARCHITECTURE.zip"
DOWNLOAD_URL="https://awscli.amazonaws.com/$ZIP_FILE"
fi
echo "📥 Downloading AWS CLI from $DOWNLOAD_URL"
# Create temporary directory
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
cd "$TEMP_DIR"
# Download AWS CLI installer
if ! curl -fsSL "$DOWNLOAD_URL" -o "awscliv2.zip"; then
echo "❌ Failed to download AWS CLI installer"
exit 1
fi
# Verify signature if requested
if [ "$VERIFY_SIGNATURE" = "true" ]; then
echo "🔐 Verifying GPG signature..."
# Download signature file
curl -fsSL "$DOWNLOAD_URL.sig" -o "awscliv2.zip.sig"
# Download and import AWS CLI public key
cat > awscli-public-key.asc << 'EOF'
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBF2Cr7UBEADJZHcgusOJl7ENSyumXh85z0TRV0xJorM2B/JL0kHOyigQluUG
ZMLhENaG0bYatdrKP+3H91lvK050pXwnO/R7fB/FSTouki4ciIx5OuLlnJZIxSzx
PqGl0mkxImLNbGWoi6Lto0LYxqHN2iQtzlwTVmq9733zd3XfcXrZ3+LblHAgEt5G
TfNxEKJ8soPLyWmwDH6HWCnjZ/aIQRBTIQ05uVeEoYxSh6wOai7ss/KveoSNBbYz
gbdzoqI2Y8cgH2nbfgp3DSasaLZEdCSsIsK1u05CinE7k2qZ7KgKAUIcT/cR/grk
C6VwsnDU0OUCideXcQ8WeHutqvgZH1JgKDbznoIzeQHJD238GEu+eKhRHcz8/jeG
94zkcgJOz3KbZGYMiTh277Fvj9zzvZsbMBCedV1BTg3TqgvdX4bdkhf5cH+7NtWO
lrFj6UwAsGukBTAOxC0l/dnSmZhJ7Z1KmEWilro/gOrjtOxqRQutlIqG22TaqoPG
fYVN+en3Zwbt97kcgZDwqbuykNt64oZWc4XKCa3mprEGC3IbJTBFqglXmZ7l9ywG
EEUJYOlb2XrSuPWml39beWdKM8kzr1OjnlOm6+lpTRCBfo0wa9F8YZRhHPAkwKkX
XDeOGpWRj4ohOx0d2GWkyV5xyN14p2tQOCdOODmz80yUTgRpPVQUtOEhXQARAQAB
tCFBV1MgQ0xJIFRlYW0gPGF3cy1jbGlAYW1hem9uLmNvbT6JAlQEEwEIAD4CGwMF
CwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQT7Xbd/1cEYuAURraimMQrMRnJHXAUC
aGveYQUJDMpiLAAKCRCmMQrMRnJHXKBYD/9Ab0qQdGiO5hObchG8xh8Rpb4Mjyf6
0JrVo6m8GNjNj6BHkSc8fuTQJ/FaEhaQxj3pjZ3GXPrXjIIVChmICuCVELRqHvJW
7M6vLJF8aBqPQMf2sPDLVh9BqKE9HJ5KuFNPvV7H2OvTG/1gzzcKYiMpqPGrqrNv
K2JaZnj1C0fHuBJE1qS5CuE8xK9dqZZqYNjMp2vMJiKKWz3CZe9lBLmFhQPvLiVs
LXuRNgPWVqJ7M/3oLG7aT6oJ0e6KUXVZYVCIYYZYuHYhKQZLBKqJYvCiMlvTwKPx
s+fZE8yWGh7F3hpVj6TKNKL3srvBNH4dPVrHYCKJXPJ7V7FPFHWkLUeVYZN1Lnm9
vLRjjCKJmXVoMYp1TVLFLXvbQF7lJxJZqrCgHBn7oBvqMN7j0C8vQKtVYJdPdXnH
Z5CnEPFLLOFXZQFhqZKVJdQFHsG/hOjYFwLXNGLOZYmU0jVdJHLXm7vQvFRqYQ7K
JvZ9C6E7ZTQZ8xZmNjLWdGJPMU7pTh7kQcU7Z4QTVn2XyNmXVFVCPj6l7xJqhVLR
FnVeXVJqkF7xL7PqFh7VmM5h0JhLZLqj7VvVHqVF5mJPfXH9cjZ0bVXqLhZ9MmZN
YQlVZFqNqQZYL5h6mPV1qJhJqRZXJqP7MF1kQhV7CqJqZLqHpZ7ZVZJqLhFpLqJ1
mQINBF2Cr7UBEADQfNnCBd0ZT6d+3gzQKXoJZKCgYCy0O6f8Ue6XkdLJ0TkpQ5cZ
8L3Q7GQJQVF0vQ0LVOjCvVCPQNGh7dPr8xHQvfh5j9NQHQVfJXXj0YdZj0mQvZ+Y
Q0YhC7kQFHvPQ0mJfZHvH0CQ8VYvQpvG9L0qJ7wQH9dJh5QmQ7JLvZjCQhvj0vZQ
vQ5fv0ZfH5dPj0q5H9qJ8QJ5fj5JvZH9jQj5QpqJ9LqJZJ0J5qZHjJqHJqJZqJ5J
pZqJqZJLqZLJpqZJLJqZLqJ5JqZJqJLqZJqLJpZJqJLJpqZLqJZJqLqJZJqLJqZL
qJqZLJqLJqZLJpqZLqJZLJqLqZJLqJZJqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLq
JZqLJpZLqJZLqJZLJpZqLJpqZLqJZLJqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJ
ZqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJZqLJpZLqJZLqJZLJpZqLJpqZLqJZLJ
qLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJZqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZ
LqJZqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJZqLJpZLqJZLqJZLJpZqLJpqZLqJ
ZLJqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJZqLJpZLqJZLqJZLJpqZLqJLqZJqL
JpZLqJZqLJpZLqJZLqJZLJpZqLJpqZLqJZLJqLJpZLqJZLqJZLJpqZLqJLqZJqLJ
pZLqJZqLJpZLqJZLqJZLJwARAQABiQI8BBgBCAAmAhsMFiEE+123f9XBGLgFEa2o
pjEKzEZyR1wFAmBr3vgFCQvQ8ZsACgkQpjEKzEZyR1xhfA//VMi2VCwNqIFD4A7Q
H4/sLMNE4MFLfh+FLR8iGdLKYlJ4V8qYaFqLQHqKvLFJdQJ7LJ0LQNHqJZH0Zvjh
fH9ZQHqJ5JZH5vJHLpZJ0LZLqJ5JqJZLJqZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ
qJZLqJZJqJZLqJZJqJZLqJZJqA==
=qvqC
-----END PGP PUBLIC KEY BLOCK-----
EOF
gpg --import awscli-public-key.asc 2>/dev/null || true
if gpg --verify awscliv2.zip.sig awscliv2.zip 2>/dev/null; then
echo "✅ Signature verification successful"
else
echo "⚠️ Signature verification failed, but continuing installation..."
fi
fi
# Extract the installer
echo "📦 Extracting installer..."
unzip -q awscliv2.zip
# Run the installer
echo "🔧 Installing AWS CLI to $INSTALL_DIRECTORY..."
# Check if we need sudo
if [ -w "$INSTALL_DIRECTORY" ]; then
./aws/install --install-dir "$INSTALL_DIRECTORY/aws-cli" --bin-dir "$INSTALL_DIRECTORY/bin" --update
else
sudo ./aws/install --install-dir "$INSTALL_DIRECTORY/aws-cli" --bin-dir "$INSTALL_DIRECTORY/bin" --update
fi
# Verify installation
if command -v aws >/dev/null 2>&1; then
INSTALLED_VERSION=$(aws --version 2>&1 | awk '{print $1}' | cut -d'/' -f2)
echo "✅ AWS CLI successfully installed (version $INSTALLED_VERSION)"
aws --version
else
echo "❌ AWS CLI installation failed"
exit 1
fi