Skip to content

Instantly share code, notes, and snippets.

@diegofcornejo
Last active September 5, 2025 00:11
Show Gist options
  • Save diegofcornejo/68e9ffa20dbc353fb665f4e32f13fc5a to your computer and use it in GitHub Desktop.
Save diegofcornejo/68e9ffa20dbc353fb665f4e32f13fc5a to your computer and use it in GitHub Desktop.
Install Grafana Alloy and Node Exporter on EC2 Linux AMI 2023 and send metrics to Remote Mimir

Install Grafana Alloy and Node Exporter on EC2 Linux AMI 2023 and send metrics to Remote Mimir

Prerequisites

Create alloy user

sudo useradd -M -U alloy

Create a directory for Grafana Alloy

sudo mkdir -p /opt/alloy/{bin,config,data,logs}
sudo chown -R alloy:alloy /opt/alloy
sudo chmod -R 755 /opt/alloy

Install Node Exporter

Download Node Exporter

Check the architecture and download the appropriate version

uname -m
# This will output the architecture, for example: x86_64 or arm64

Download the appropriate version

Replace the architecture with the output from the previous command

wget https://github.com/prometheus/node_exporter/releases/download/v1.9.1/node_exporter-1.9.1.linux-amd64.tar.gz

Extract Node Exporter

tar -xvf node_exporter-1.9.1.linux-amd64.tar.gz

Move Node Exporter to /opt/node_exporter

sudo mv node_exporter-1.9.1.linux-amd64 /opt/node_exporter

Set ownership and permissions

sudo chown -R alloy:alloy /opt/node_exporter

Create systemd service

sudo nano /etc/systemd/system/node_exporter.service

Add the following content to the file

[Unit]
Description=Node Exporter
After=network-online.target

[Service]
User=alloy
Group=alloy
Restart=on-failure
ExecStart=/opt/node_exporter/node_exporter

[Install]
WantedBy=multi-user.target

Reload systemd

sudo systemctl daemon-reload

Enable Node Exporter to start on boot and start it

sudo systemctl enable node_exporter
sudo systemctl start node_exporter

Check Node Exporter status and ensure it is running

sudo systemctl status node_exporter
curl http://localhost:9100/metrics

Install Grafana Alloy

Download the appropriate version

wget https://github.com/grafana/alloy/releases/download/v1.10.2/alloy-linux-amd64.zip

Extract Grafana Alloy

unzip alloy-linux-amd64.zip

Move Grafana Alloy to /opt/alloy

sudo mv alloy-linux-amd64 /opt/alloy/bin/alloy

Set ownership and permissions

sudo chown -R alloy:alloy /opt/alloy
sudo chmod -R 755 /opt/alloy
sudo chmod 750 /opt/alloy/data

Configure Grafana Alloy

sudo nano /opt/alloy/config/config.river

Add the following content to the file

// =============================
// Grafana Alloy - config.river
// Local Node Exporter -> Remote Mimir (single-tenant, no auth)
// =============================

prometheus.scrape "node" {
  // List of targets (each target is a map with labels). __address__ is required.
  // You can set job and instance here.
  targets = [
    {
      "__address__" = "127.0.0.1:9100",
      "job"         = "node",
      "instance"    = "your-instance-name", // or sys.env("INSTANCE") if you want to use the instance name from the environment variable
    },
  ]

  forward_to	  = [prometheus.remote_write.to_mimir.receiver]
  scrape_interval = "15s"
  scrape_timeout  = "10s"
}

prometheus.remote_write "to_mimir" {
  endpoint {
    url = "http://your-mimir-host:9009/api/v1/push"

    // If you are using basic auth in the reverse proxy, enable this block
    // basic_auth {
    //   username = "prom_rw"
    //   password = env("MIMIR_RW_PASSWORD")
    // }

    // If you are using multi-tenant, enable this block
    // headers = {
    //   "X-Scope-OrgID" = "test"
    // }

    // Reasonable queue tuning
    queue_config {
      capacity              = 10000
      max_shards            = 4
      max_samples_per_send  = 5000
      batch_send_deadline   = "5s"
      min_backoff           = "100ms"
      max_backoff           = "5s"
      retry_on_http_429     = true
    }
  }
}

Create systemd service

sudo nano /etc/systemd/system/alloy.service

Add the following content to the file

[Unit]
Description=Grafana Alloy (metrics scraper & remote_write)
After=network-online.target
Wants=network-online.target

[Service]
User=alloy
Group=alloy
# Uncomment this if you want to use the instance name from the environment variable
# Environment="INSTANCE=your-instance-name" 
WorkingDirectory=/opt/alloy
ExecStart=/opt/alloy/bin/alloy run \
  --server.http.listen-addr=127.0.0.1:12345 \
  --storage.path=/opt/alloy/data \
  /opt/alloy/config/config.river
Restart=always
RestartSec=5
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Reload systemd

sudo systemctl daemon-reload

Enable Grafana Alloy to start on boot and start it

sudo systemctl enable --now alloy

Check Grafana Alloy status and ensure it is running

sudo systemctl status alloy

Check Grafana Alloy logs

sudo journalctl -u alloy -f

Check Grafana Alloy metrics

curl -s http://127.0.0.1:12345/metrics | egrep 'scrape|remote_write|alloy'
#!/usr/bin/env bash
set -euo pipefail
# ================================
# Config (adjust as needed)
# ================================
NODE_EXPORTER_VERSION="1.9.1" # e.g. 1.9.1
ALLOY_VERSION="1.10.2" # e.g. 1.10.2
# Local ports
NODE_EXPORTER_PORT="9100"
ALLOY_HTTP_PORT="12345"
# Instance label for metrics
INSTANCE_NAME="$(hostname -f || hostname)"
# Remote Mimir URL (adjust later in /etc/default/alloy if needed)
MIMIR_URL_DEFAULT="http://your-mimir-host:9009/api/v1/push"
# Paths
NODE_EXPORTER_DIR="/opt/node_exporter"
ALLOY_ROOT="/opt/alloy"
ALLOY_BIN="${ALLOY_ROOT}/bin/alloy"
ALLOY_CONF_DIR="${ALLOY_ROOT}/config"
ALLOY_DATA_DIR="${ALLOY_ROOT}/data"
ALLOY_LOG_DIR="${ALLOY_ROOT}/logs"
ALLOY_ENV_FILE="/etc/default/alloy"
# ================================
# Helpers
# ================================
log() { printf "\033[1;32m[INFO]\033[0m %s\n" "$*"; }
warn() { printf "\033[1;33m[WARN]\033[0m %s\n" "$*"; }
err() { printf "\033[1;31m[ERROR]\033[0m %s\n" "$*" >&2; }
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
err "This script must be run as root (use sudo)."
exit 1
fi
}
detect_arch() {
local m
m="$(uname -m)"
case "$m" in
x86_64) echo "amd64" ;;
aarch64) echo "arm64" ;;
arm64) echo "arm64" ;;
*)
err "Unsupported architecture: $m"
exit 1
;;
esac
}
install_deps() {
log "Installing dependencies (dnf unzip wget tar)..."
dnf -y install unzip wget tar >/dev/null
}
ensure_user() {
local user="alloy"
if id -u "$user" >/dev/null 2>&1; then
log "User '$user' already exists, skipping."
else
log "Creating user '$user'..."
useradd -r -s /usr/sbin/nologin -M -U "$user"
fi
}
prepare_dirs() {
log "Preparing Alloy directories in ${ALLOY_ROOT}..."
mkdir -p "${ALLOY_ROOT}/bin" "${ALLOY_CONF_DIR}" "${ALLOY_DATA_DIR}" "${ALLOY_LOG_DIR}"
chown -R alloy:alloy "${ALLOY_ROOT}"
chmod -R 755 "${ALLOY_ROOT}"
chmod 750 "${ALLOY_DATA_DIR}"
}
install_node_exporter() {
local arch="$1"
local ver="$NODE_EXPORTER_VERSION"
local pkg="node_exporter-${ver}.linux-${arch}.tar.gz"
local url="https://github.com/prometheus/node_exporter/releases/download/v${ver}/${pkg}"
if [[ -x "${NODE_EXPORTER_DIR}/node_exporter" ]]; then
log "Node Exporter already installed in ${NODE_EXPORTER_DIR}, skipping."
else
log "Downloading Node Exporter ${ver} (${arch})..."
cd /tmp
rm -f "${pkg}"
wget -q "${url}"
log "Extracting ${pkg}..."
tar -xzf "${pkg}"
local folder="node_exporter-${ver}.linux-${arch}"
if [[ ! -x "/tmp/${folder}/node_exporter" ]]; then
err "node_exporter binary not found after extraction."
exit 1
fi
log "Moving to ${NODE_EXPORTER_DIR}..."
mv "/tmp/${folder}" "${NODE_EXPORTER_DIR}"
chown -R alloy:alloy "${NODE_EXPORTER_DIR}"
fi
log "Creating systemd service for Node Exporter..."
cat >/etc/systemd/system/node_exporter.service <<EOF
[Unit]
Description=Node Exporter
After=network-online.target
Wants=network-online.target
[Service]
User=alloy
Group=alloy
Type=simple
Restart=on-failure
ExecStart=${NODE_EXPORTER_DIR}/node_exporter --web.listen-address=127.0.0.1:${NODE_EXPORTER_PORT}
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now node_exporter
systemctl --no-pager --full status node_exporter || true
}
install_alloy() {
local arch="$1"
local ver="$ALLOY_VERSION"
local zip="alloy-linux-${arch}.zip"
local url="https://github.com/grafana/alloy/releases/download/v${ver}/${zip}"
if [[ -x "${ALLOY_BIN}" ]]; then
log "Alloy already installed at ${ALLOY_BIN}, skipping."
else
log "Downloading Grafana Alloy ${ver} (${arch})..."
cd /tmp
rm -f "${zip}"
wget -q "${url}"
log "Extracting ${zip}..."
unzip -q "${zip}"
if [[ ! -x "/tmp/alloy-linux-${arch}" ]]; then
err "Alloy binary not found after extraction."
exit 1
fi
log "Moving binary to ${ALLOY_BIN}..."
mv "/tmp/alloy-linux-${arch}" "${ALLOY_BIN}"
chown alloy:alloy "${ALLOY_BIN}"
chmod 755 "${ALLOY_BIN}"
fi
}
write_alloy_envfile() {
log "Writing ${ALLOY_ENV_FILE}..."
cat > "${ALLOY_ENV_FILE}" <<EOF
# ================================
# /etc/default/alloy
# Edit and restart Alloy if needed
# ================================
INSTANCE="${INSTANCE_NAME}"
# Remote write
MIMIR_URL="${MIMIR_URL_DEFAULT}"
# (Optional) Basic auth
# MIMIR_RW_USERNAME="prom_rw"
# MIMIR_RW_PASSWORD="S3cr3t"
# (Optional) Multi-tenant
# MIMIR_TENANT_ID="test"
EOF
chmod 0644 "${ALLOY_ENV_FILE}"
}
write_alloy_config() {
log "Generating ${ALLOY_CONF_DIR}/config.river ..."
source "${ALLOY_ENV_FILE}" || true
local basic_auth_block=""
if [[ "${MIMIR_RW_USERNAME:-}" != "" && "${MIMIR_RW_PASSWORD:-}" != "" ]]; then
basic_auth_block=$(cat <<'E0'
basic_auth {
username = env("MIMIR_RW_USERNAME")
password = env("MIMIR_RW_PASSWORD")
}
E0
)
fi
local headers_block=""
if [[ "${MIMIR_TENANT_ID:-}" != "" ]]; then
headers_block=$(cat <<'E1'
headers = {
"X-Scope-OrgID" = env("MIMIR_TENANT_ID")
}
E1
)
fi
cat > "${ALLOY_CONF_DIR}/config.river" <<RIVER
prometheus.scrape "node" {
targets = [
{
"__address__" = "127.0.0.1:${NODE_EXPORTER_PORT}",
"job" = "node",
"instance" = env("INSTANCE"),
},
]
forward_to = [prometheus.remote_write.to_mimir.receiver]
scrape_interval = "15s"
scrape_timeout = "10s"
}
prometheus.remote_write "to_mimir" {
endpoint {
url = env("MIMIR_URL")
${basic_auth_block}
${headers_block}
queue_config {
capacity = 10000
max_shards = 4
max_samples_per_send = 5000
batch_send_deadline = "5s"
min_backoff = "100ms"
max_backoff = "5s"
retry_on_http_429 = true
}
}
}
RIVER
chown -R alloy:alloy "${ALLOY_CONF_DIR}"
chmod 0644 "${ALLOY_CONF_DIR}/config.river"
}
write_alloy_service() {
log "Creating systemd service for Alloy..."
cat >/etc/systemd/system/alloy.service <<EOF
[Unit]
Description=Grafana Alloy (metrics scraper & remote_write)
After=network-online.target
Wants=network-online.target
[Service]
User=alloy
Group=alloy
EnvironmentFile=-${ALLOY_ENV_FILE}
WorkingDirectory=${ALLOY_ROOT}
ExecStart=${ALLOY_BIN} run \
--server.http.listen-addr=127.0.0.1:${ALLOY_HTTP_PORT} \
--storage.path=${ALLOY_DATA_DIR} \
${ALLOY_CONF_DIR}/config.river
Restart=always
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now alloy
systemctl --no-pager --full status alloy || true
}
post_checks() {
log "Checking Node Exporter..."
curl -fsS "http://127.0.0.1:${NODE_EXPORTER_PORT}/metrics" >/dev/null && log "Node Exporter OK" || warn "Could not fetch /metrics."
log "Checking Alloy metrics..."
curl -fsS "http://127.0.0.1:${ALLOY_HTTP_PORT}/metrics" | egrep -E 'scrape|remote_write|alloy' || warn "No Alloy metrics found yet."
}
main() {
require_root
local arch
arch="$(detect_arch)"
log "Detected architecture: ${arch}"
install_deps
ensure_user
prepare_dirs
install_node_exporter "${arch}"
install_alloy "${arch}"
write_alloy_envfile
write_alloy_config
write_alloy_service
post_checks
cat <<NOTE
========================================
INSTALLATION COMPLETE
- Node Exporter: listening on 127.0.0.1:${NODE_EXPORTER_PORT}
- Alloy HTTP/Metrics: 127.0.0.1:${ALLOY_HTTP_PORT}
- Alloy config: ${ALLOY_CONF_DIR}/config.river
- Alloy environment vars: ${ALLOY_ENV_FILE}
To enable basic auth or multi-tenant:
sudo nano ${ALLOY_ENV_FILE}
# Uncomment/edit MIMIR_RW_USERNAME, MIMIR_RW_PASSWORD, MIMIR_TENANT_ID, MIMIR_URL
sudo systemctl daemon-reload
sudo systemctl restart alloy
Logs:
journalctl -u node_exporter -f
journalctl -u alloy -f
========================================
NOTE
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment