Skip to content

Instantly share code, notes, and snippets.

@nsticco
Last active February 7, 2026 21:40
Show Gist options
  • Select an option

  • Save nsticco/c417ce3c00a4d88ff2661e3cc60f18d4 to your computer and use it in GitHub Desktop.

Select an option

Save nsticco/c417ce3c00a4d88ff2661e3cc60f18d4 to your computer and use it in GitHub Desktop.
Shell script to install DevOps tools for Ubuntu
#!/usr/bin/env bash
# bash <(curl -fsSL https://gist.githubusercontent.com/nsticco/c417ce3c00a4d88ff2661e3cc60f18d4/raw/bootstrap-ubuntu-devops.sh)
# Bootstrap Ubuntu 24.04+ with DevOps tools + popular AI coding agent CLIs.
# 2026-safe practices: no apt-key, uses /etc/apt/keyrings, non-interactive gpg, pkgs.k8s.io for kubectl,
# pipx for Ansible, dedicated keyrings, and idempotent-ish installs.
set -euo pipefail
# -----------------------------------------------------------------------------
# Defaults (override via env or --param value)
# -----------------------------------------------------------------------------
# Core DevOps
nodejs=${nodejs:-24} # Node LTS major (e.g., 24)
kubectl=${kubectl:-1.35.0} # latest stable (client) as of Feb 2026; or "latest"
terragrunt=${terragrunt:-0.99.1} # latest stable as of Feb 2026; or "latest"
packer=${packer:-1.15.0} # latest stable as of Feb 2026
vault=${vault:-1.21.3} # latest stable as of Feb 2026
java=${java:-25} # latest Java LTS
keygen=${keygen:-true} # generate SSH key (ed25519)
# AI coding agent CLIs (default: latest)
codex_cli=${codex_cli:-latest} # npm tag/version for @openai/codex
gemini_cli=${gemini_cli:-latest} # npm tag/version for @google/gemini-cli
claude_code=${claude_code:-latest} # latest|stable|<version> (Claude install script supports version args)
cursor_cli=${cursor_cli:-latest} # (latest only; cursor install script)
github_spec_kit=${github_spec_kit:-latest} # latest or git ref/tag/sha for spec-kit (Specify CLI)
github_cli=${github_cli:-true} # GitHub CLI (gh) via apt
github_copilot_cli=${github_copilot_cli:-latest} # latest|prerelease|vX.Y.Z|X.Y.Z
# -----------------------------------------------------------------------------
# Arg parsing: --param value
# -----------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--*)
param="${1#--}"
if [[ $# -lt 2 || "${2:-}" == --* ]]; then
echo "ERROR: Missing value for $1" >&2
exit 1
fi
declare "${param}"="$2"
shift 2
;;
*)
shift
;;
esac
done
log() { echo -e "\n==> $*\n"; }
lower() { echo "${1,,}"; }
is_disabled() {
case "$(lower "${1:-}")" in
false|0|no|off|skip|none) return 0 ;;
*) return 1 ;;
esac
}
ensure_path_line() {
local line="$1"
local file="$2"
touch "$file"
grep -qxF "$line" "$file" || echo "$line" >> "$file"
}
detect_arch() {
local uarch
uarch="$(uname -m)"
case "$uarch" in
x86_64|amd64)
ARCH="amd64"
AWS_ARCH="x86_64"
;;
aarch64|arm64)
ARCH="arm64"
AWS_ARCH="aarch64"
;;
*)
echo "ERROR: Unsupported architecture: $uarch" >&2
exit 1
;;
esac
}
detect_arch
CODENAME="$(. /etc/os-release && echo "${VERSION_CODENAME}")"
OS_PRETTY="$(. /etc/os-release && echo "${PRETTY_NAME}")"
# Ensure user-local bins are on PATH for this run and persist for future logins
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
ensure_path_line 'export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"' "$HOME/.profile"
# Non-interactive apt (still asks for some snaps sometimes, but apt installs will not prompt)
export DEBIAN_FRONTEND=noninteractive
# Helper: non-interactive, atomic keyring write (avoids "Overwrite? (y/N)")
dearmor_to_keyring() {
# Usage: dearmor_to_keyring <url> <dest_path>
local url="$1"
local dest="$2"
local tmp
tmp="$(mktemp)"
curl -fsSL "$url" | gpg --dearmor --yes --batch --output "$tmp"
sudo install -o root -g root -m 0644 "$tmp" "$dest"
rm -f "$tmp"
}
# -----------------------------------------------------------------------------
# Base packages
# -----------------------------------------------------------------------------
log "Installing base packages..."
sudo apt-get update -y
sudo apt-get install -y \
ca-certificates curl wget gnupg gpg lsb-release \
apt-transport-https \
unzip tar jq \
build-essential \
git vim nano \
python3 python3-pip python3-venv python3-full \
pipx \
ripgrep
# -----------------------------------------------------------------------------
# Node.js (NodeSource repo, keyring)
# -----------------------------------------------------------------------------
log "Installing Node.js ${nodejs}.x (NodeSource)..."
sudo mkdir -p /etc/apt/keyrings
dearmor_to_keyring \
"https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \
"/etc/apt/keyrings/nodesource.gpg"
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${nodejs}.x nodistro main" \
| sudo tee /etc/apt/sources.list.d/nodesource.list >/dev/null
sudo apt-get update -y
sudo apt-get install -y nodejs
# Configure npm global installs to avoid sudo npm installs
log "Configuring npm global install directory..."
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global" >/dev/null
export PATH="$HOME/.npm-global/bin:$PATH"
# -----------------------------------------------------------------------------
# Java (Eclipse Temurin via Adoptium apt repo)
# -----------------------------------------------------------------------------
log "Installing Temurin JDK ${java}..."
java="${java#v}"
sudo mkdir -p /etc/apt/keyrings
dearmor_to_keyring \
"https://packages.adoptium.net/artifactory/api/gpg/key/public" \
"/etc/apt/keyrings/adoptium.gpg"
echo "deb [signed-by=/etc/apt/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb ${CODENAME} main" \
| sudo tee /etc/apt/sources.list.d/adoptium.list >/dev/null
sudo apt-get update -y
sudo apt-get install -y "temurin-${java}-jdk"
# -----------------------------------------------------------------------------
# Ansible (pipx)
# -----------------------------------------------------------------------------
log "Installing/Upgrading Ansible (pipx)..."
pipx ensurepath >/dev/null 2>&1 || true
export PATH="$HOME/.local/bin:$PATH"
if command -v ansible >/dev/null 2>&1; then
pipx upgrade --include-injected ansible || true
else
pipx install --include-deps ansible
fi
# -----------------------------------------------------------------------------
# AWS CLI v2 (official installer)
# -----------------------------------------------------------------------------
log "Installing AWS CLI v2..."
tmpdir="$(mktemp -d)"
pushd "$tmpdir" >/dev/null
curl -fsS "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_ARCH}.zip" -o "awscliv2.zip"
unzip -q awscliv2.zip
sudo ./aws/install --update
popd >/dev/null
rm -rf "$tmpdir"
# -----------------------------------------------------------------------------
# kubectl (pkgs.k8s.io community-owned repos)
# -----------------------------------------------------------------------------
log "Installing kubectl (${kubectl}) via pkgs.k8s.io..."
KUBECTL_VERSION="${kubectl#v}"
if [[ "$(lower "$KUBECTL_VERSION")" == "latest" ]]; then
KUBECTL_VERSION="$(curl -fsSL https://dl.k8s.io/release/stable.txt | sed 's/^v//')"
fi
KUBECTL_MINOR="$(echo "$KUBECTL_VERSION" | awk -F. '{print $1"."$2}')"
sudo mkdir -p -m 755 /etc/apt/keyrings
dearmor_to_keyring \
"https://pkgs.k8s.io/core:/stable:/v${KUBECTL_MINOR}/deb/Release.key" \
"/etc/apt/keyrings/kubernetes-apt-keyring.gpg"
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${KUBECTL_MINOR}/deb/ /" \
| sudo tee /etc/apt/sources.list.d/kubernetes.list >/dev/null
sudo chmod 0644 /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update -y
sudo apt-get install -y kubectl
# -----------------------------------------------------------------------------
# Helm (official script)
# -----------------------------------------------------------------------------
log "Installing Helm..."
curl -fsSL -o /tmp/get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 /tmp/get_helm.sh
/tmp/get_helm.sh
rm -f /tmp/get_helm.sh
# -----------------------------------------------------------------------------
# k9s (latest GitHub release tarball)
# -----------------------------------------------------------------------------
log "Installing k9s..."
K9S_VERSION="$(curl -fsSL https://api.github.com/repos/derailed/k9s/releases/latest | jq -r '.tag_name' | sed 's/^v//')"
tmpdir="$(mktemp -d)"
pushd "$tmpdir" >/dev/null
curl -fsSLO "https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Linux_${ARCH}.tar.gz"
tar -xzf "k9s_Linux_${ARCH}.tar.gz" k9s
sudo install -o root -g root -m 0755 k9s /usr/local/bin/k9s
popd >/dev/null
rm -rf "$tmpdir"
# -----------------------------------------------------------------------------
# HashiCorp APT repo (Terraform via apt)
# -----------------------------------------------------------------------------
log "Adding HashiCorp APT repo (for Terraform)..."
sudo mkdir -p /usr/share/keyrings
# Non-interactive overwrite-safe:
dearmor_to_keyring \
"https://apt.releases.hashicorp.com/gpg" \
"/usr/share/keyrings/hashicorp-archive-keyring.gpg"
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com ${CODENAME} main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list >/dev/null
sudo apt-get update -y
log "Installing Terraform (apt)..."
sudo apt-get install -y terraform
# -----------------------------------------------------------------------------
# Terragrunt (GitHub releases)
# -----------------------------------------------------------------------------
log "Installing Terragrunt (${terragrunt})..."
TG_VERSION="${terragrunt#v}"
if [[ "$(lower "$TG_VERSION")" == "latest" ]]; then
TG_VERSION="$(curl -fsSL https://api.github.com/repos/gruntwork-io/terragrunt/releases/latest | jq -r '.tag_name' | sed 's/^v//')"
fi
curl -fsSLo /tmp/terragrunt "https://github.com/gruntwork-io/terragrunt/releases/download/v${TG_VERSION}/terragrunt_linux_${ARCH}"
sudo install -o root -g root -m 0755 /tmp/terragrunt /usr/local/bin/terragrunt
rm -f /tmp/terragrunt
# -----------------------------------------------------------------------------
# Packer (binary zip)
# -----------------------------------------------------------------------------
log "Installing Packer (${packer})..."
PACKER_VERSION="${packer#v}"
tmpdir="$(mktemp -d)"
pushd "$tmpdir" >/dev/null
curl -fsSLO "https://releases.hashicorp.com/packer/${PACKER_VERSION}/packer_${PACKER_VERSION}_linux_${ARCH}.zip"
unzip -q "packer_${PACKER_VERSION}_linux_${ARCH}.zip"
sudo install -o root -g root -m 0755 packer /usr/local/bin/packer
popd >/dev/null
rm -rf "$tmpdir"
# -----------------------------------------------------------------------------
# Vault (binary zip)
# -----------------------------------------------------------------------------
log "Installing Vault (${vault})..."
VAULT_VERSION="${vault#v}"
tmpdir="$(mktemp -d)"
pushd "$tmpdir" >/dev/null
curl -fsSLO "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_${ARCH}.zip"
unzip -q "vault_${VAULT_VERSION}_linux_${ARCH}.zip"
sudo install -o root -g root -m 0755 vault /usr/local/bin/vault
popd >/dev/null
rm -rf "$tmpdir"
# -----------------------------------------------------------------------------
# Postman (snap)
# -----------------------------------------------------------------------------
log "Installing Postman (snap)..."
if ! command -v snap >/dev/null 2>&1; then
sudo apt-get install -y snapd
fi
sudo snap install postman || true
# =============================================================================
# GitHub CLI (gh)
# =============================================================================
if ! is_disabled "$github_cli"; then
log "Installing GitHub CLI (gh)..."
if ! command -v gh >/dev/null 2>&1; then
sudo mkdir -p -m 755 /etc/apt/keyrings
# GitHub provides this key as a binary gpg keyring already; safe to store directly.
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null
sudo chmod 0644 /etc/apt/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null
sudo apt-get update -y
sudo apt-get install -y gh
else
echo "gh already installed; skipping."
fi
fi
# =============================================================================
# AI Coding Agent CLIs
# =============================================================================
# Codex CLI (npm)
if ! is_disabled "$codex_cli"; then
CODEX_TAG="$(lower "$codex_cli")"
log "Installing Codex CLI (@openai/codex@${CODEX_TAG})..."
npm i -g "@openai/codex@${CODEX_TAG}"
fi
# Gemini CLI (npm)
if ! is_disabled "$gemini_cli"; then
GEMINI_TAG="$(lower "$gemini_cli")"
log "Installing Gemini CLI (@google/gemini-cli@${GEMINI_TAG})..."
npm i -g "@google/gemini-cli@${GEMINI_TAG}"
fi
# Claude Code (native installer)
if ! is_disabled "$claude_code"; then
log "Installing Claude Code (${claude_code})..."
if [[ "$(lower "$claude_code")" == "latest" || "$(lower "$claude_code")" == "stable" ]]; then
curl -fsSL https://claude.ai/install.sh | bash
else
curl -fsSL https://claude.ai/install.sh | bash -s "$claude_code"
fi
fi
# Cursor CLI (native installer)
if ! is_disabled "$cursor_cli"; then
log "Installing Cursor CLI..."
curl -fsSL https://cursor.com/install | bash
fi
# GitHub Spec Kit (Specify CLI) via uv
if ! is_disabled "$github_spec_kit"; then
log "Installing uv (Astral) for Spec Kit..."
if ! command -v uv >/dev/null 2>&1; then
curl -LsSf https://astral.sh/uv/install.sh | sh
fi
export PATH="$HOME/.local/bin:$PATH"
log "Installing GitHub Spec Kit (Specify CLI)..."
SPEC_FROM="git+https://github.com/github/spec-kit.git"
if [[ "$(lower "$github_spec_kit")" != "latest" ]]; then
SPEC_FROM="git+https://github.com/github/spec-kit.git@${github_spec_kit}"
fi
uv tool install specify-cli --from "${SPEC_FROM}" --force
fi
# GitHub Copilot CLI
if ! is_disabled "$github_copilot_cli"; then
GH_COPILOT_VER="$(lower "$github_copilot_cli")"
log "Installing GitHub Copilot CLI (${GH_COPILOT_VER})..."
if [[ "$GH_COPILOT_VER" == "prerelease" ]]; then
npm install -g @github/copilot@prerelease
else
if [[ "$GH_COPILOT_VER" == "latest" ]]; then
curl -fsSL https://gh.io/copilot-install | PREFIX="$HOME/.local" bash
else
if [[ "$GH_COPILOT_VER" != v* ]]; then
GH_COPILOT_VER="v${GH_COPILOT_VER}"
fi
curl -fsSL https://gh.io/copilot-install | VERSION="$GH_COPILOT_VER" PREFIX="$HOME/.local" bash
fi
fi
fi
# -----------------------------------------------------------------------------
# Cleanup
# -----------------------------------------------------------------------------
log "Cleaning up..."
sudo apt-get autoremove -y
sudo apt-get clean -y
# -----------------------------------------------------------------------------
# SSH key generation (ed25519)
# -----------------------------------------------------------------------------
if ! is_disabled "$keygen"; then
log "SSH key setup (ed25519)..."
mkdir -p ~/.ssh
chmod 700 ~/.ssh
if [[ ! -f ~/.ssh/id_ed25519 ]]; then
ssh-keygen -q -t ed25519 -a 64 -N "" \
-C "$(whoami)@$(hostname) on ${OS_PRETTY}" \
-f ~/.ssh/id_ed25519
echo "Generated ~/.ssh/id_ed25519"
else
echo "~/.ssh/id_ed25519 already exists; skipping."
fi
fi
log "Done."
@fran7802
Copy link

Thanks for sharing!
Could you add K9s?

@fran7802
Copy link

Can this be edited for RedHat?

@nsticco
Copy link
Author

nsticco commented Mar 12, 2022

Can this be edited for RedHat?

I created a similar CentOS script which shouldn't be too hard to get to work with RedHat since they're part of the same family: https://gist.github.com/nsticco/9244cb8a68913ed61c6905881e354f05

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment