Last active
February 7, 2026 21:40
-
-
Save nsticco/c417ce3c00a4d88ff2661e3cc60f18d4 to your computer and use it in GitHub Desktop.
Shell script to install DevOps tools for Ubuntu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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." |
Can this be edited for RedHat?
Author
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
Thanks for sharing!
Could you add K9s?