This post is part of the iPXE + Pure Cloud Block Store series.
    A Guide to Using iPXE + Pure Cloud Block Store for Stateless EC2 Instances - Part 3

A Guide to Using iPXE + Pure Cloud Block Store for Stateless EC2 Instances - Part 3

Share on:

Welcome back to Part 3 of the Using iPXE + Pure Cloud Block Store (CBS) for Stateless EC2. In the first part of the series we covered getting all the details ready, Part 2 did a deeper into the AWS and Pure Storage ecosystem and now part 3 will include some advanced day 2 automation.

Previously in Part1 and Part2 we covered the configuration of the environment and deploying a single EC2. It included the following steps:

  1. Prerequisite Setup
  2. Building and Exporting the boot image
  3. Downloading boot image to our local machine
  1. Create and mount a CBS volume to jump host
  2. Write boot image to be used as a template
  3. Deploy Stateless EC2 with boot volumes on CBS.

Now in Part 3 we will use advanced automation to bootstrap our machine.

  1. Create a custom iPXE Image that allows nested user data
  2. Install Additional Packages and Configuration
  3. Deployment of additional iSCSI volumes to our guest

Currently, the iPXE image supports a single user data file. There is a pending update for the image to be updated to support additional bootstrapping. The following code can be used to clone the repository, make the local image and then upload it to AWS to use as a custom AMI.

1git clone https://github.com/amazon-contributing/upstream-to-ipxe.git
2cd upstream-to-ipxe/src
3make CONFIG=cloud EMBED=config/cloud/aws.ipxe bin-x86_64-pcbios/ipxe.usb
4../contrib/cloud/aws-import -r us-west-2 bin-x86_64-pcbios/ipxe.usb

When the import is complete, we will find the ID of our new AMI.

The next section we will proceed to create our EC2 using this new AMI.

In this section we will create an EC2 using our new AMI, it allows the ability to do both pre-boot user data s well as post-boot user data. We will use this configuration to install updates as well as NGINX. Once the deployment is complete we will see that the default nginx site is available. The below sample Terraform code has been updated with the new AMI and an updated user data script.

provider "aws" {
region = var.aws_region
}
variable "aws_region" {
type = string
default = "us-west-2"
}
variable "instance_type" {
type = string
default = "t3.small"
}
variable "key_name" {
type = string
default = "mykeypair"
}
variable "vpc_security_group_ids" {
type = list(string)
default = ["sg-abcd1234"]
}
variable "subnet_id" {
type = string
default = "subnet-abcd1234"
}
variable "ami_id" {
type = string
default = "ami-067a0fad89512f721"
}
variable "tags" {
type = map(string)
default = {
Name = "ubuntu1"
}
}
resource "aws_instance" "ubuntu1" {
ami = var.ami_id
instance_type = var.instance_type
vpc_security_group_ids = var.vpc_security_group_ids
subnet_id = var.subnet_id
key_name = var.key_name
user_data = file("${path.module}/userdata2.sh")
tags = var.tags
}
view raw ipxe2.tf hosted with ❤ by GitHub
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
--==BOUNDARY==
MIME-Version: 1.0
Content-Type: text/ipxe; charset="utf-8"
#!ipxe
#==============================================================================
#
# Pre-Boot iSCSI Script
#
# This script is intended to handle the iPXE Boot to Allow Boot From SAN
#
#==============================================================================
# --- iPXE Variables ---
set initiator-iqn iqn.2010-04.org.ipxe:ubuntu1
set target-ip1 172.18.10.68
set target-ip2 172.18.10.92
set array-iqn iqn.2010-06.com.purestorage:flasharray.22a52e281e6dbb52
# --- iPXE Boot Commands ---
dhcp
sanboot iscsi:${target-ip1}:::1:${array-iqn} iscsi:${target-ip2}:::1:${array-iqn}
--==BOUNDARY==
MIME-Version: 1.0
Content-Type: text/x-shellscript; charset="utf-8"
#!/bin/bash
#==============================================================================
#
# Post-Boot Setup Script
#
# This script is intended to run after an OS has been SAN-booted via iPXE.
# It configures the OS and Installs Packages
#
#==============================================================================
# --- Helper function for logging ---
log() {
echo "=> $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
# --- Initial System and User Setup ---
log "Updating system and installing necessary packages..."
apt update -y && apt upgrade -y
apt install -y nginx
log "Script finished"
--==BOUNDARY==--
view raw userdata2.sh hosted with ❤ by GitHub

Once you run terraform apply your machine will be deployed! Once the deployment is complete, navigate to your IP and see your website. The user data is not immediate so you can track the status on our linux machine at /var/log/cloud-init-output.log.

In the final section we will take what we have done and build our final automation package. We will deploy our EC2 machine, configure the guest for iSCSI and mount an additional volume from our Pure CBS array.

NOTE
This example will utilize additional open source providers that may not be fully supported so please be aware of this when reviewing the code and examples.

variable "aws_region" {
type = string
default = "us-west-2"
}
variable "instance_type" {
type = string
default = "t3.small"
}
variable "key_name" {
type = string
default = "mykey"
}
variable "vpc_security_group_ids" {
type = list(string)
default = ["sg-abcd1234"]
}
variable "subnet_id" {
type = string
default = "subnet-abcd1234"
}
variable "ami_id" {
type = string
default = "ami-abcd1234"
}
variable "tags" {
type = map(string)
default = {
Name = "ubuntu2"
}
}
variable "purestorage_target" {
type = string
default = "1.1.1." # IP of your CBS Array
}
variable "purestorage_apitoken" {
type = string
default = "1111-1111-111-111" # API Token for your CBS Array
}
variable "purestorage_host" {
type = string
default = "ubuntu2"
}
variable "purestorage_iqn" {
type = list(string)
default = ["iqn.2010-04.org.ipxe:ubuntu2"]
}
variable "purestorage_source_vol" {
type = string
default = "ubuntu24-goldtemplate"
}
terraform {
required_providers {
purestorage = {
source = "devans10/flash"
}
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
region = var.aws_region
}
provider "purestorage" {
target = var.purestorage_target
api_token = var.purestorage_apitoken
}
resource "purestorage_host" "ubuntu2" {
name = var.purestorage_host
iqn = var.purestorage_iqn
provider = purestorage
volume {
vol = purestorage_volume.boot.name
lun = 1
}
volume {
vol = purestorage_volume.data1.name
lun = 2
}
}
resource "purestorage_volume" "boot" {
name = "${var.purestorage_host}-boot"
source = var.purestorage_source_vol
provider = purestorage
}
resource "purestorage_volume" "data1" {
name = "${var.purestorage_host}-data1"
size = 10737418240
provider = purestorage
}
resource "aws_instance" "ubuntu2" {
ami = var.ami_id
instance_type = var.instance_type
vpc_security_group_ids = var.vpc_security_group_ids
subnet_id = var.subnet_id
key_name = var.key_name
user_data = file("${path.module}/userdata3.sh")
tags = var.tags
depends_on = [purestorage_host.ubuntu2]
}
view raw ipxe3.tf hosted with ❤ by GitHub
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
--==BOUNDARY==
MIME-Version: 1.0
Content-Type: text/ipxe; charset="utf-8"
#!ipxe
# --- iPXE Variables ---
set initiator-iqn iqn.2010-04.org.ipxe:ubuntu2
set target-ip1 172.18.10.68
set target-ip2 172.18.10.92
set array-iqn iqn.2010-06.com.purestorage:flasharray.22a52e281e6dbb52
# --- iPXE Boot Commands ---
dhcp
sanboot iscsi:${target-ip1}:::1:${array-iqn} iscsi:${target-ip2}:::1:${array-iqn}
--==BOUNDARY==
MIME-Version: 1.0
Content-Type: text/x-shellscript; charset="utf-8"
#!/bin/bash
#==============================================================================
#
# Post-Boot iSCSI LUN Setup Script for Non-MPIO Environments
#
# This script is intended to run after an OS has been SAN-booted via iPXE.
# It configures the OS to connect to a data LUN, format it, and mount it
# using a persistent device name provided by the system.
#
#==============================================================================
# --- Configuration ---
# These variables should align with the iPXE script and your environment.
ISCSI_TARGET_IP1="172.18.10.68"
ISCSI_TARGET_IP2="172.18.10.92"
INITIATOR_IQN="iqn.2010-04.org.ipxe:ubuntu2"
VOLUME_MNT="/data"
# --- End Configuration ---
# --- Helper function for logging ---
log() {
echo "=> $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
# --- Initial System and User Setup ---
log "Updating system and installing necessary packages..."
apt update -y && apt upgrade -y
apt install -y open-iscsi multipath-tools fio
# --- iSCSI Configuration and Login ---
log "Starting and enabling iSCSI services..."
systemctl enable --now iscsid.service
log "Configuring initiator IQN to ${INITIATOR_IQN}..."
echo "InitiatorName=${INITIATOR_IQN}" > /etc/iscsi/initiatorname.iscsi
systemctl restart iscsid.service
log "Discovering iSCSI targets..."
iscsiadm -m discovery -t st -p ${ISCSI_TARGET_IP1}
iscsiadm -m discovery -t st -p ${ISCSI_TARGET_IP2}
log "Logging into iSCSI targets..."
iscsiadm -m node -p ${ISCSI_TARGET_IP1} --login
iscsiadm -m node -p ${ISCSI_TARGET_IP2} --login
log "Setting iSCSI sessions to log in automatically on boot..."
iscsiadm -m node -L automatic
log "Increase iSCSI Sessions..."
#
log "Rescanning for new devices..."
rescan-scsi-bus.sh
log "Updating multipath configuration..."
systemctl enable multipathd
systemctl restart multipathd
log "Mounting multipath device..."
mkdir -p ${VOLUME_MNT}
# Find the multipath device name
disk=$(multipath -ll | head -n 1 | awk '{print $1}')
log "Found multipath device: /dev/mapper/${disk}"
# Check if a filesystem already exists on the device
log "Checking for existing filesystem on /dev/mapper/${disk}..."
if ! blkid /dev/mapper/${disk} > /dev/null 2>&1; then
# No filesystem was found, so it's safe to format
log "No filesystem detected. Formatting device with ext4..."
mkfs.ext4 /dev/mapper/${disk}
else
# A filesystem already exists, so skip formatting
log "Filesystem already exists on /dev/mapper/${disk}. Skipping format."
fi
# Check if an entry for the mount point already exists before adding it
if ! grep -q " ${VOLUME_MNT} " /etc/fstab; then
echo "Adding fstab entry for ${VOLUME_MNT}..."
echo "/dev/mapper/${disk} ${VOLUME_MNT} ext4 defaults,nofail 0 2"| tee -a /etc/fstab
systemctl daemon-reload
else
echo "fstab entry for ${VOLUME_MNT} already exists. Skipping."
fi
# Mount the device using the new fstab entry
log "Mounting device..."
mount -a
log "Script finished. Multipath device mounted at ${VOLUME_MNT} and configured for persistence."
--==BOUNDARY==--
view raw userdata3.sh hosted with ❤ by GitHub

When we run our terraform apply we will see that the host and volumes will be automatically created on thee array.

Once the deployment finishes we can see that our volume has been mounted to our /data directory. We can also again track the status of the automation on our linux machine at /var/log/cloud-init-output.log.

This automation creates a great repeatable process that can scale and provide ease of use to deploy multiple machines using a template boot volume.

That’s it! Hopefully this series gave you a few ideas on how powershell this can be, is there anything else you would like to see?

If you have questions or feedback, feel free to leave a comment below!

See Also