Quick and Dirty Block Storage for OpenShift
This post is now also on the Official Red Hat OpenShift blog!
2020 Update: This post was written for OpenShift 3.x. With the new 4.x version, integration will need to be different. I’ll write a updated post when time allows.
Do you have an OpenShift installation, maybe a test cluster, but no fancy storage solution to provide your Persistent Volumes? Most people would turn to NFS for this, but did you know that it’s almost as easy to set up a simple iSCSI server? This blog post will walk through a simple example.
Why use block storage instead of NFS? Well, first off, it’s not always preferable. NFS (or something like GlusterFS) is required when multiple pods must share the same Persistent Volume. However, for pods that require a high degree of data integrity, like databases, block storage is preferred. NFS has several issues that can compromise data integrity for these kinds of pods. For more information, see Olaf Kirch’s Why NFS Sucks paper.
This setup is not recommended for anything but test/lab environments. It has no security, no high-availability, and no fancy management tool. Use this information at your own risk.
Terminology
iSCSI has its own vocabulary that takes some getting used to:
- Target – an iSCSI export. This is what the iSCSI server provides.
- Initiator – The client.
- IQN – iSCSI Qualified Name. A unique name that identifies iSCSI targets and initiators.
- LUN – Logical Unit Number. This represents the actual disk. There can be multiple LUNs exported by a Target.
- ACL – Access Control List. Usually a list of allowed initiator IQNs
- Portal – a IP and port combination that the target is accessible from.
- TPG – Target Portal Group. A grouping of LUNs, ACLs, and Portals that defines access permissions for the Target.
Prerequisites: Deciding on a backstore
In Linux, there are several options for what storage to actually use. In this blog we’ll talk about two kinds: file and block devices. I highly recommend using LVM to manage block devices, but you could also use disk partitions or files.
Files are generally slower, but may be more convenient if you can’t modify the partition table on your iSCSI server, but have free space available.
Nested How-To: Create a large, empty file
To create an arbitrarily-sized empty file for use as an iSCSI back store, use the dd
command.
Example of creating a 1024 MB file:
[root@iscsi-server ~]# dd if=/dev/zero of=/path/to/file bs=1M count=1024
dd
is a weird command, let’s break down the options:
if
stands for “input file”. In this case we’re pulling data from/dev/zero
, which is a special device that outputs an endless stream of 0x00 bytes.of
stands for “output file”. This should be the file name you want to create.bs
stands for “block size”. This tells dd how much data to read and write per iteration.count
This tells dd how many times to read/write.
So, bs
*count
=file size. Therefore, this creates a file of 1024 MB or 1
GB. The block size may influence how much time it takes to create a
file. 1 MB is not the optimal write size for most disks. It’s possible
to optimize the operation by tweaking the block size, but that’s way
out of scope for this post.
Nested How-To: LVM
LVM isn’t strictly required for iSCSI, but it makes the management of block devices much easier. LVM allows block devices to be created on the fly from a pool of available storage. The block devices are called logical volumes, and can be created, deleted and resized on the fly. Logical volumes are contained within volume groups. Volume groups are collections of physical volumes which are presented as a unified pool of storage.
The LVM tools are contained in the lvm2
package, and can be
installed via yum install -y lvm2
. In the default install of RHEL
and CentOS, these tools should be installed.
In this example, we have 100 GB of storaged attached to my my iSCSI
server as /dev/vdb
.
[root@iscsi-server ~]# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
vda 253:0 0 50G 0 disk
└─vda1 253:1 0 50G 0 part /
vdb 253:16 0 100G 0 disk
First, we have to create a Physical Volume using the pvcreate
command. This example uses the whole disk, but if using a partition,
be sure that the partition type is 0x8e
(Linux LVM).
[root@iscsi-server ~]# pvcreate /dev/vdb
Physical volume "/dev/vdb" successfully created.
[root@iscsi-server ~]# pvs
PV VG Fmt Attr PSize PFree
/dev/vdb lvm2 --- 100.00g 100.00g
Next, a volume group, my_vg
, needs to be created using the relevant
Physical Volumes. Here we only have one physical volume, but a Volume
Group can contain many physical volumes and will present all physical
volumes associated to the volume group as a single storage pool:
[root@iscsi-server ~]# vgcreate my_vg /dev/vdb
Volume group "my_vg" successfully created
[root@iscsi-server ~]# vgs
VG #PV #LV #SN Attr VSize VFree
my_vg 1 0 0 wz--n- 100.00g 100.00g
Finally, we can now create storage. The lvcreate
command will create a
logical volume. There are a whole suite of lv* commands, including
lvextend
, and lvremove
.
[root@iscsi-server ~]# lvcreate --name my_lv -L 1G my_vg
Logical volume "my_lv" created.
[root@iscsi-server ~]# lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
my_lv my_vg -wi-a----- 1.00g
[root@iscsi-server ~]# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
vda 253:0 0 50G 0 disk
└─vda1 253:1 0 50G 0 part /
vdb 253:16 0 100G 0 disk
└─my_vg-my_lv 252:0 0 1G 0 lvm
This logical volume is now available for use. The device mapper should
present it at /dev/mapper/my_vg-my_lv
. If we ever wanted to create
more logical volumes, we’d just need to run another lvcreate
command.
Now that we have our logical volume, there are setup steps on both the iSCSI server as well as any initiators (clients).
Installing the iSCSI server
On the iSCSI server, installing the targetcli
package will install all
the required dependencies for the iSCSI server. The target.service
unit is the actual service. Port 3260 UDP and TCP must be open.
yum install -y targetcli
firewall-cmd --add-service iscsi-target --permanent && firewall-cmd --reload
Installing the iSCSI client
The iSCSI client needs the iscsi-initiator-utils
package. This should
already be installed on OpenShift nodes.
Each iSCSI initiator (i.e. the client) is required to have a unique IQN (iSCSI Qualified Name). In RHEL and CentOS, a random one is generated at install time, but you may wish to set more meaningful names. This IQN is used when allowing the client to access iSCSI targets.
Example of a randomly generated IQN:
[root@iscsi-client ~]# cat /etc/iscsi/initiatorname.iscsi
InitiatorName=iqn.1994-05.com.redhat:d1fa8aab755
There is nothing wrong with keeping the default random name. However,
it can be helpful to assign meaningful names to each client. After
editing the /etc/iscsi/initiatorname.iscsi
file, restart
iscsid.service
for the new InitiatorName to take affect.
If setting a IQN, it is EXTERMELY IMPORTANT that every initiator has a UNIQUE IQN!
Exporting a Target
Now that we have our setup out of the way, it’s time to actually
export a target. In RHEL7/CentOS7, this is accomplished with the
targetcli
command.
targetcli
provides an interactive command prompt and presents the
iSCSI configuration as a directory tree. Configuring a new target
requires a few steps:
- Create a backstore object. This will represents the actual block device
- Create a target, which will automatically create a target portal group
- Assign the backstore to the target portal group
- Assign the initiator IQNs to the target portal group’s access control list
Let’s walk through a simple targetcli session:
[root@iscsi-server ~]# targetcli
targetcli shell version 2.1.fb41
Copyright 2011-2013 by Datera, Inc and others.
For help on commands, type 'help'.
/> ls
o- / ......................................................................................................................... [...]
o- backstores .............................................................................................................. [...]
| o- block .................................................................................................. [Storage Objects: 0]
| o- fileio ................................................................................................. [Storage Objects: 0]
| o- pscsi .................................................................................................. [Storage Objects: 0]
| o- ramdisk ................................................................................................ [Storage Objects: 0]
o- iscsi ............................................................................................................ [Targets: 0]
o- loopback ......................................................................................................... [Targets: 0]
Above, we’ve started targetcli and run ls. At the moment, there is nothing configured.
/> cd backstores/block
/backstores/block> create dev=/dev/mapper/my_vg-my_lv name=my_iscsi_pv
Created block storage object my_iscsi_pv using /dev/mapper/my_vg-my_lv.
/backstores/block> ls
o- block .................................................. [Storage Objects: 1]
o- my_iscsi_pv ....... [/dev/mapper/my_vg-my_lv (1.0GiB) write-thru activated]
We’ve now created a backstore object by changing my directory to
/backstores/block
and issuing the create
command. The dev
parameter is the device name of the logical volume created previously,
it could be any unused device (like a hypothetical /dev/sda4
), and
the name is an arbitrary name of my choosing that we will use in later
commands.
For a file instead of a device, we’ll do things slightly differently:
/> cd backstores/fileio
/backstores/fileio> create file_or_dev=/myfile name=myfile
Created fileio myfile with size 1073741824
/backstores/fileio> ls
o- fileio ................................................. [Storage Objects: 1]
o- myfile .......................... [/myfile (1.0GiB) write-back deactivated]
/backstores/fileio>
Notice we changed to a different directory: /backstores/fileio
instead of /backstores/block
. The options passed to create are also
slightly different.
/backstores/block> cd /iscsi
/iscsi> create iqn.2017-04.com.example.mydomain:myiscsipv
Created target iqn.2017-04.com.example.mydomain:myiscsipv.
Created TPG 1.
Global pref auto_add_default_portal=true
Created default portal listening on all IPs (0.0.0.0), port 3260.
/iscsi> ls
o- iscsi .............................................................................................................. [Targets: 1]
o- iqn.2017-04.com.example.mydomain:myiscsipv .......................................................................... [TPGs: 1]
o- tpg1 ................................................................................................. [no-gen-acls, no-auth]
o- acls ............................................................................................................ [ACLs: 0]
o- luns ............................................................................................................ [LUNs: 0]
o- portals ...................................................................................................... [Portals: 1]
o- 0.0.0.0:3260 ....................................................................................................... [OK]
We’ve now created a target by changing our directory to /iscsi
and
running the create
command. In this example, we specified the IQN of
the target, but if an IQN is ommited, the create
command will
generate a random IQN.
Notice that by creating the target object, a target portal group,
tpg1
was also created. A target can have more than one TPG assigned
to it, with different LUNs, ACLs, and Portals. In our
“quick-and-dirty” setup, we’ll just stick to a single TPG.
/iscsi> cd iqn.2017-04.com.example.mydomain:myiscsipv/tpg1/luns
/iscsi/iqn.20...ipv/tpg1/luns> create storage_object=/backstores/block/my_iscsi_pv
Created LUN 0.
/iscsi/iqn.20...ipv/tpg1/luns> ls
o- luns .............................................................. [LUNs: 1]
o- lun0 ........................ [block/my_iscsi_pv (/dev/mapper/my_vg-my_lv)]
We’ve now assigned a LUN to the target. Again, we changed our
directory to the luns directory and used the create
command.
A TPG can have multiple LUNs associated with it. To associate another LUN, run the create command again with a different backstore object. In OpenShift, there is an option for LUN number in the iSCSI PersistentVolume options. It’s a matter of taste as to whether to create one target for OpenShift that exports many LUNs, or to have multiple targets with fewer LUNs.
My personal preference is to have different targets for different uses. So, one target with multiple LUNs for general purpose PVs, and a different target for infra-related PVs (e.g. for the PVs related to Aggregate Logging).
/iscsi/iqn.20...ipv/tpg1/luns> cd /iscsi/iqn.2017-04.com.example.mydomain:myiscsipv/tpg1/acls
/iscsi/iqn.20...ipv/tpg1/acls> create wwn=iqn.1994-05.com.redhat:d1fa8aab755
Created Node ACL for iqn.1994-05.com.redhat:d1fa8aab755
Created mapped LUN 0.
/iscsi/iqn.20...ipv/tpg1/acls> ls
o- acls .............................................................. [ACLs: 1]
o- iqn.1994-05.com.redhat:d1fa8aab755 ....................... [Mapped LUNs: 1]
o- mapped_lun0 ............................... [lun0 block/my_iscsi_pv (rw)]
Next, we create the ACLs for the target. Notice, I used the ACL of the iSCSI client we set up earlier. This needs to be repeated for all clients that need access to the target. In OpenShift, this means ALL nodes that could have a PersistentVolumeClaim against the PV. So, if you have 12 nodes, then there needs to be 12 client IQNs added to the ACL.
/iscsi/iqn.20...ipv/tpg1/acls> cd /
/> ls
o- / ......................................................................................................................... [...]
o- backstores .............................................................................................................. [...]
| o- block .................................................................................................. [Storage Objects: 1]
| | o- my_iscsi_pv ....................................................... [/dev/mapper/my_vg-my_lv (1.0GiB) write-thru activated]
| o- fileio ................................................................................................. [Storage Objects: 0]
| o- pscsi .................................................................................................. [Storage Objects: 0]
| o- ramdisk ................................................................................................ [Storage Objects: 0]
o- iscsi ............................................................................................................ [Targets: 1]
| o- iqn.2017-04.com.example.mydomain:myiscsipv ........................................................................ [TPGs: 1]
| o- tpg1 ............................................................................................... [no-gen-acls, no-auth]
| o- acls .......................................................................................................... [ACLs: 1]
| | o- iqn.1994-05.com.redhat:d1fa8aab755 ................................................................... [Mapped LUNs: 1]
| | o- mapped_lun0 ........................................................................... [lun0 block/my_iscsi_pv (rw)]
| o- luns .......................................................................................................... [LUNs: 1]
| | o- lun0 .................................................................... [block/my_iscsi_pv (/dev/mapper/my_vg-my_lv)]
| o- portals .................................................................................................... [Portals: 1]
| o- 0.0.0.0:3260 ..................................................................................................... [OK]
o- loopback ......................................................................................................... [Targets: 0]
/> exit
Global pref auto_save_on_exit=true
Last 10 configs saved in /etc/target/backup.
Configuration saved to /etc/target/saveconfig.json
Now we’ve completed the basic setup, and the above shows our completed tree. This configuration is now active, and we can move on to testing the target.
Before moving on, you’ll notice we did not modify the portal part of
the TPG. The default 0.0.0.0:3260
means that this target is
accessible from any IP on the iSCSI server via port 3260. If desired,
we could change the port or restrict the listening IP addresses, but
for a “quick-and-dirty” setup, this is unnecessary.
Testing a LUN
To test the target, let’s log into our iSCSI client, and see if we can
see see the volume. To do this, we’ll use the iscsiadm
command.
First, check the initiator name to make sure it matches what you have in the ACL for the TPG. If it does not match, then this test will not work.
[root@iscsi-client ~]# cat /etc/iscsi/initiatorname.iscsi
InitiatorName=iqn.1994-05.com.redhat:d1fa8aab755
Next, use the iscsiadm
command to discover all targets on the iSCSI
server.
[root@iscsi-client ~]# iscsiadm --mode discoverydb --type sendtargets --portal iscsi-server.example.com --discover
172.31.0.127:3260,1 iqn.2017-04.com.example.mydomain:myiscsipv
Next, we login to the target.
[root@iscsi-client ~]# iscsiadm --mode node --targetname iqn.2017-04.com.example.mydomain:myiscsipv --portal iscsi-server.example.com:3260 --login
Logging in to [iface: default, target: iqn.2017-04.com.example.mydomain:myiscsipv, portal: 172.31.0.127,3260] (multiple)
Login to [iface: default, target: iqn.2017-04.com.example.mydomain:myiscsipv, portal: 172.31.0.127,3260] successful.
If the login command is successful, we should see a new block device
attached to the host. In /var/log/messages
, there should be messages
similar to the ones below:
Apr 18 23:53:31 iscsi-client kernel: scsi host2: iSCSI Initiator over TCP/IP
Apr 18 23:53:31 iscsi-client kernel: scsi 2:0:0:0: Direct-Access LIO-ORG my_iscsi_pv 4.0 PQ: 0 ANSI: 5
Apr 18 23:53:31 iscsi-client kernel: scsi 2:0:0:0: alua: supports implicit and explicit TPGS
Apr 18 23:53:31 iscsi-client kernel: scsi 2:0:0:0: alua: port group 00 rel port 01
Apr 18 23:53:31 iscsi-client kernel: scsi 2:0:0:0: alua: port group 00 state A non-preferred supports TOlUSNA
Apr 18 23:53:31 iscsi-client kernel: scsi 2:0:0:0: alua: Attached
Apr 18 23:53:31 iscsi-client kernel: scsi 2:0:0:0: Attached scsi generic sg0 type 0
Apr 18 23:53:31 iscsi-client kernel: sd 2:0:0:0: [sda] 2097152 512-byte logical blocks: (1.07 GB/1.00 GiB)
Apr 18 23:53:31 iscsi-client kernel: sd 2:0:0:0: [sda] Write Protect is off
Apr 18 23:53:31 iscsi-client kernel: sd 2:0:0:0: [sda] Write cache: enabled, read cache: enabled, supports DPO and FUA
Apr 18 23:53:31 iscsi-client kernel: sd 2:0:0:0: [sda] Attached SCSI disk
Apr 18 23:53:31 iscsi-client iscsid: Could not set session1 priority. READ/WRITE throughout and latency could be affected.
Apr 18 23:53:31 iscsi-client iscsid: Connection1:0 to [target: iqn.2017-04.com.example.mydomain:myiscsipv, portal: 172.31.0.127,3260] through [iface: default] is operational now
In the logs, we see this device is assigned to sda
, so we should see
sda
in the output of lsblk
.
[root@iscsi-client ~]# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 1G 0 disk
vda 253:0 0 50G 0 disk
└─vda1 253:1 0 50G 0 part
vdb 253:16 0 50G 0 disk
└─vdb1 253:17 0 50G 0 part /
This connection will persist through reboot. Since this is just a
test, we don’t want that. To break the connection, we’ll logout of the
target using iscsiadm
.
[root@iscsi-client ~]# iscsiadm --mode node --targetname iqn.2017-04.com.example.mydomain:myiscsipv --portal iscsi-server.example.com:3260 --logout
Logging out of session [sid: 1, target: iqn.2017-04.com.example.mydomain:myiscsipv, portal: 172.31.0.127,3260]
Logout of [sid: 1, target: iqn.2017-04.com.example.mydomain:myiscsipv, portal: 172.31.0.127,3260] successful.
After logging out of the target, we should no longer see sda attached to our system.
[root@iscsi-client ~]# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
vda 253:0 0 50G 0 disk
└─vda1 253:1 0 50G 0 part /
vdb 253:16 0 50G 0 disk
└─vdb1 253:17 0 50G 0 part
OpenShift PersistentVolume Configuration
Now that we’ve tested our iSCSI target, we can create an OpenShift PersistentVolume. Compared to the previous steps, this is a piece of cake! Below is example YAML, based upon our previous work:
apiVersion: v1
kind: PersistentVolume
metadata:
name: iscsi-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
iscsi:
targetPortal: iscsi-server.example.com
iqn: iqn.2017-04.com.example.mydomain:myiscsipv
lun: 0
fsType: 'ext4'
readOnly: false