- 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
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.
Prerequisites
- Python
- GitHub Reposistory for AWS Third Party Storage
- Deployed Cloud Block Store on AWS
- (Optional) Install Terraform
Recap
Previously in Part1 and Part2 we covered the configuration of the environment and deploying a single EC2. It included the following steps:
Part 1
- Prerequisite Setup
- Building and Exporting the boot image
- Downloading boot image to our local machine
Part 2
- Create and mount a CBS volume to jump host
- Write boot image to be used as a template
- Deploy Stateless EC2 with boot volumes on CBS.
Now in Part 3 we will use advanced automation to bootstrap our machine.
- Create a custom iPXE Image that allows nested user data
- Install Additional Packages and Configuration
- Deployment of additional iSCSI volumes to our guest
Create a Custom iPXE Image
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.
Create EC2 Instance and Install NGINX
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 | |
| } |
| 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==-- |
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.
Create EC2 Instance and Mount Additional iSCSI Volume
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] | |
| } |
| 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==-- |
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.
Conclusion
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
- A Guide to Using iPXE + Pure Cloud Block Store for Stateless EC2 Instances - Part 2
- A Guide to Using iPXE + Pure Cloud Block Store for Stateless EC2 Instances - Part 1
- Deploying a Linux EC2 Instance with Hashicorp Terraform and Vault to AWS and Connect to Pure Cloud Block Store
- Deploying a EC2 Instance with PowerShell to AWS
- Deploying a Windows EC2 Instance with Hashicorp Terraform and Vault to AWS



