diff --git a/.github/workflows/build-ami.yml b/.github/workflows/build-ami.yml new file mode 100644 index 0000000..285a47b --- /dev/null +++ b/.github/workflows/build-ami.yml @@ -0,0 +1,70 @@ +name: Build DAppNode AMI + +on: + workflow_run: + workflows: ["Release"] + types: [completed] + +permissions: + id-token: write + contents: read + +jobs: + build-ami: + name: Build DAppNode AMI + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Configure AWS credentials via OIDC + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.IMAGE_BUILDER_ROLE_ARN }} + aws-region: us-east-1 + + - name: Bump recipe and trigger AMI build + env: + PIPELINE_ARN: ${{ secrets.IMAGE_BUILDER_PIPELINE_ARN }} + INFRA_ARN: ${{ secrets.IMAGE_BUILDER_INFRA_ARN }} + DIST_ARN: ${{ secrets.IMAGE_BUILDER_DIST_ARN }} + run: | + # Get current recipe and extract component + version + CURRENT_RECIPE_ARN=$(aws imagebuilder get-image-pipeline \ + --image-pipeline-arn "$PIPELINE_ARN" \ + --query 'imagePipeline.imageRecipeArn' --output text) + + CURRENT_VERSION=$(echo "$CURRENT_RECIPE_ARN" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+$') + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" + echo "Recipe: $CURRENT_VERSION -> $NEW_VERSION" + + # Get component from current recipe + COMPONENT_ARN=$(aws imagebuilder get-image-recipe \ + --image-recipe-arn "$CURRENT_RECIPE_ARN" \ + --query 'imageRecipe.components[0].componentArn' --output text) + echo "Component: $COMPONENT_ARN" + + # Create new recipe version (same component, latest Ubuntu 24) + RECIPE_ARN=$(aws imagebuilder create-image-recipe \ + --name "dappnode-image" \ + --semantic-version "$NEW_VERSION" \ + --parent-image "arn:aws:imagebuilder:us-east-1:aws:image/ubuntu-server-24-lts-x86/x.x.x" \ + --components "[{\"componentArn\":\"$COMPONENT_ARN\"}]" \ + --block-device-mappings '[{"deviceName":"/dev/sda1","ebs":{"volumeSize":16,"volumeType":"gp3","deleteOnTermination":true}}]' \ + --working-directory "/tmp" \ + --query 'imageRecipeArn' --output text) + + # Update pipeline and trigger + aws imagebuilder update-image-pipeline \ + --image-pipeline-arn "$PIPELINE_ARN" \ + --image-recipe-arn "$RECIPE_ARN" \ + --infrastructure-configuration-arn "$INFRA_ARN" \ + --distribution-configuration-arn "$DIST_ARN" \ + --image-tests-configuration "imageTestsEnabled=false" + + EXECUTION=$(aws imagebuilder start-image-pipeline-execution \ + --image-pipeline-arn "$PIPELINE_ARN" \ + --query 'imageBuildVersionArn' --output text) + + echo "### AMI Build Triggered 🚀" >> "$GITHUB_STEP_SUMMARY" + echo "- **Recipe:** ${NEW_VERSION}" >> "$GITHUB_STEP_SUMMARY" + echo "- **Image ARN:** ${EXECUTION}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6cd00b..cf89b82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,5 @@ -name: Pre-release +name: Release + on: workflow_dispatch: inputs: @@ -247,11 +248,11 @@ jobs: run: | echo "Images directory content:" ls -lrt images/ - - name: Create pre-release + - name: Create release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.set-versions.outputs.core }} - prerelease: true + prerelease: false files: | ./images/Dappnode-*-debian-*-attended.iso ./images/Dappnode-*-debian-*-unattended.iso diff --git a/scripts/dappnode_ami_build.sh b/scripts/dappnode_ami_build.sh new file mode 100755 index 0000000..d4f9f87 --- /dev/null +++ b/scripts/dappnode_ami_build.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# DAppNode AMI Build Script +# Purpose: Install prerequisites, pre-download core Docker images, and set up +# first-boot installer for EC2 Image Builder. +# +# Env vars: +# PROFILE_URL — URL to dappnode_profile.sh (defaults to latest release) +# +# The installer still runs at first boot (via rc.local), but finds the heavy +# Docker images already cached in /usr/src/dappnode/DNCORE/, making boot fast. + +set -euo pipefail + +: "${PROFILE_URL:=https://github.com/dappnode/DAppNode/releases/latest/download/dappnode_profile.sh}" + +DAPPNODE_DIR="/usr/src/dappnode" +DNCORE_DIR="$DAPPNODE_DIR/DNCORE" +LOGS_DIR="$DAPPNODE_DIR/logs" +LOG_FILE="$LOGS_DIR/ami_build.log" + +export DEBIAN_FRONTEND=noninteractive + +mkdir -p "$DAPPNODE_DIR/scripts" "$DNCORE_DIR" "$LOGS_DIR" +touch "$LOG_FILE" +exec > >(tee -a "$LOG_FILE") 2>&1 + +log() { echo "[AMI-BUILD] $*"; } + +lsb_dist="$(. /etc/os-release && echo "$ID")" +log "OS: $lsb_dist | Profile: $PROFILE_URL" + +# ─── Phase 1: Prerequisites ────────────────────────────────────────────────── +log "=== Phase 1: Prerequisites ===" + +apt-get update -y + +if ! docker -v >/dev/null 2>&1; then + log "Installing Docker..." + apt-get remove -y docker docker-engine docker.io containerd runc || true + apt-get install -y ca-certificates curl lsb-release + install -m 0755 -d /etc/apt/keyrings + curl -fsSL "https://download.docker.com/linux/${lsb_dist}/gpg" -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$lsb_dist $(lsb_release -cs) stable" \ + | tee /etc/apt/sources.list.d/docker.list >/dev/null + apt-get update -y + apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +fi + +cat >/usr/local/bin/docker-compose <<'EOL' +#!/bin/bash +docker compose "$@" +EOL +chmod +x /usr/local/bin/docker-compose + +modprobe wireguard 2>/dev/null || apt-get install -y wireguard-dkms || apt-get install -y wireguard-tools || true +apt-get install -y lsof iptables xz-utils || true + +# ─── Phase 2: Pre-download core images ─────────────────────────────────────── +log "=== Phase 2: Pre-downloading core images ===" + +wget -O "$DNCORE_DIR/.dappnode_profile" "$PROFILE_URL" + +# Source only the version variables (up to ISOBUILD marker) +sed '/^\#\!ISOBUILD/q' "$DNCORE_DIR/.dappnode_profile" > /tmp/vars.sh +source /tmp/vars.sh + +COMPONENTS=(BIND IPFS WIREGUARD DAPPMANAGER WIFI HTTPS) + +for comp in "${COMPONENTS[@]}"; do + ver="${comp}_VERSION" + comp_lower="$(echo "$comp" | tr '[:upper:]' '[:lower:]')" + VERSION="${!ver}" + + if [[ "$VERSION" == /ipfs/* ]]; then + log "Skipping $comp (IPFS-based version)" + continue + fi + + BASE_URL="https://github.com/dappnode/DNP_${comp}/releases/download/v${VERSION}" + + log "Downloading $comp v${VERSION}..." + wget -q -O "$DNCORE_DIR/${comp_lower}.dnp.dappnode.eth_${VERSION}_linux-amd64.txz" \ + "${BASE_URL}/${comp_lower}.dnp.dappnode.eth_${VERSION}_linux-amd64.txz" || \ + log "WARNING: Failed to download $comp image" + + wget -q -O "$DNCORE_DIR/docker-compose-${comp_lower}.yml" \ + "${BASE_URL}/docker-compose.yml" || \ + log "WARNING: Failed to download $comp compose" + + wget -q -O "$DNCORE_DIR/dappnode_package-${comp_lower}.json" \ + "${BASE_URL}/dappnode_package.json" || \ + log "WARNING: Failed to download $comp manifest" +done + +# Content hashes for execution/consensus clients +CONTENT_HASH_PKGS=(besu geth nethermind erigon prysm teku lighthouse lodestar nimbus) +HASH_FILE="$DNCORE_DIR/packages-content-hash.csv" +rm -f "$HASH_FILE" +for pkg in "${CONTENT_HASH_PKGS[@]}"; do + HASH=$(wget -q -O- "https://github.com/dappnode/DAppNodePackage-${pkg}/releases/latest/download/content-hash" || true) + if [ -n "$HASH" ]; then + echo "${pkg}.dnp.dappnode.eth,${HASH}" >> "$HASH_FILE" + log "Got content hash: $pkg" + fi +done + +log "Pre-download complete:" +du -sh "$DNCORE_DIR/" + +# ─── Phase 3: First-boot installer ─────────────────────────────────────────── +log "=== Phase 3: First-boot setup ===" + +wget -O "$DAPPNODE_DIR/scripts/dappnode_install.sh" https://installer.dappnode.io +chmod +x "$DAPPNODE_DIR/scripts/dappnode_install.sh" + +cat > /etc/rc.local << 'RC' +#!/bin/sh -e +/usr/src/dappnode/scripts/dappnode_install.sh +exit 0 +RC +chmod +x /etc/rc.local +touch "$DAPPNODE_DIR/.firstboot" + +log "=== AMI build complete ==="