VmWare-LXD 1:1 #
In the Kata Containers technology it’s used an Hardware Virtualization to supply an additional isolation with a lightweight VM and individual kernels.
In a similar way the use case describe here try to use a single Linux VM over VmWare
where install a standalone LXD instance and then through lxd-compose
deploy one or
more services using a Physical vNic that is managed by LXD and added from the VM
to the Container deployed.
In this scenario to supply the classic HA services it’s needed follow exactly the steps that normally are follow on delivery HA service directly over VMs. This means deploy multiple VMs with the same services (for example two nodes for Nginx Server) and eventually using VIPs.
As visible in the image every VM is configured with two differents vNICs.
A management vNic/Iface
that is only available over the Host/VM
and it’s used to communicate with LXD HTTPS API (normally over the 8443 port)
and/or for SSH access (VM packages upgrades, maintenance, etc.).
It’s a good idea to reach this interface only over a private VPN.
A service vNic/iface
that is used over the container to supply services
configured in the container.
To ensure a more clean setup of the host a best practices is to rename the network’s
interface with a name more oriented with the target infrastructure. In the example,
it’s used the name srv0
defined in the LXD profile assigned to the container to
deploy. The same could be done for the management interface with a name like man0
.
To rename the network interfaces it’s needed create an udev rule based on the MAC
addresses assigned from VmWare to the VM. For example, editing the file
/etc/udev/rules.d/70-persistent-net.rules
:
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="XX:XX:XX:XX:XX:XX", ATTR{dev_id}=="0x0", ATTR{type}=="1", KERNEL=="eth*", NAME="man0"
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="XX:XX:XX:XX:XX:YY", ATTR{dev_id}=="0x0", ATTR{type}=="1", KERNEL=="eth*", NAME="srv0"
and then without the needed of reboot the VM just to execute:
$> # Ensure that the selected interface are down
$> ip link set eth0 down
$> ip link set eth1 down
$> udevadm control -R && udevadm trigger --action=add -v -s net
When the VMs are all configured with LXD reachable over HTTPS the lxd-compose
tool will reach every nodes through the different remotes. The file
lxd-conf/config.yml
will be configured in this way:
default-remote: local
remotes:
images:
addr: https://images.linuxcontainers.org
protocol: simplestreams
public: true
macaroni:
addr: https://macaronios.mirror.garr.it/images/lxd-images
protocol: simplestreams
public: true
myserver:
addr: https://mysimplestreams.server.local/lxd-images
protocol: simplestreams
public: true
local:
addr: unix://
public: false
local-snapd:
addr: unix:///var/snap/lxd/common/lxd/unix.socket
public: false
vm1:
addr: https://vm1.infra:8443
auth_type: tls
project: default
protocol: lxd
public: false
vm2:
addr: https://vm2.infra:8443
auth_type: tls
project: default
protocol: lxd
public: false
aliases: {}
The remote vm1
and vm2
are the remotes of the VMs where deploy the
target services.
As described in the schema over the LXD instances is configured a LXD profile
that map the VM vNIC srv0
of the VM to the container with the same name.
The nictype is physical
is the type implemented by LXD to support this feature.
Using native bridge or OpenVswitch bridge attached to a VmWare vNic is
something that requires a specific setup over VmWare because the vSwitch
uses MAC caching and MAC filter that often doesn’t permit to have something
that work correctly. For this reason and because not always the people that
implement and deploy the services are the same that control the VmWare the uses
of the physical
interface over the container is the best solution to follow
because it’s transparent to VmWare. The MAC address assigned to the container is
the same mapped over VmWare. An alternative is to use the port map between the LXD
containers and the VM vNIC but it’s another use case describe later.
In the example, it’s been used only one network interface to supply services but there aren’t limitations on this. Could be configured multiple vNics at VmWare level that are assigned to the containers, for example to supply an SSH service through a management interface directly in the container.
For this solution it’s a good idea to prepare a VmWare VM Template that is used to create new VMs to attach to the service when it’s needed to scale or just upgrade the OS and reduce the offline time with an upgrade over a VM not attached to the running services.
Dynamic nodes with the LXD Compose render engine #
Before describe how could be possible using the render engine to define the
lxd-compose
specifications to manage the service setup I want remember to the
reader that a specific connection with an LXD instance could be defined only
at group level. It’s possible that multiple groups could be connected to the
same LXD instance but it’s not true that the same group could be used to control
multiple LXD instance at the same time (excludes the use case with LXD Cluster not
covered by this scenario).
Based on this concept a defined group is mapped to a specific VM and on using this scenario
a solution that works pretty well it’s the use of the render engine to specify the
list of the remotes (or LXD instance) to reach for a specific service following the
pattern described hereinafter.
The render file will supply the list of the remotes assigned for a specific service,
for example the NGinx servers that exposes the HTTP resources. The render file is
defined directly over the .lxd-compose.yml
or --render-values|--render-default
options.
$ cat .lxd-compose.yml
general:
lxd_confdir: ./lxd-conf
render_default_file: ./render/default.yaml
render_values_file: ./render/prod.yaml
env_dirs:
- ./envs/
in this case are defined both default render and the values render.
The prod.yaml
file could be configured in this way:
release: "22.10"
nginx_nodes:
- connection: "vm1"
name: "nginx1"
- connection: "vm2"
name: "nginx2"
And these could be the options defined in the default.yaml
:
ephemeral: false
privileged_containers: false
The connections vm1
and vm2
are the remotes described before and defined in the config.yml
file.
This could be an example of the LXD Compose specs where is defined the setup of the Nginx server.
# Description: Setup the Nginx Production Service
version: "1"
include_storage_files:
- common/storages/default.yml
include_profiles_files:
- common/profiles/default.yml
- common/profiles/net-phy-srv.yml
- common/profiles/logs-disk.yml
- common/profiles/autostart.yml
projects:
- name: "nginx-services"
description: |
Setup Nginx services.
include_env_files:
- myenv/vars/common.yml
# Include the variable file that contains
# the network configurations options.
{{ range $k, $v := .Values.nginx_nodes }}
- myenv/vars/{{ $v.name }}-net.yml
{{ end }}
groups:
{{ $groups := .Values.nginx_nodes }}
{{ $ephemeral := .Values.ephemeral }}
{{ range $k, $v := .Values.nginx_nodes }}
- name: "{{ $v.name }}-group"
description: "Setup Nginx Frontend Node {{ $v.name }}"
connection: "{{ $v.connection }}"
common_profiles:
- default
- net-phy-srv
- logs-disk
{{- if $privileged_containers }}
- privileged
{{ end }}
- autostart
ephemeral: {{ $ephemeral }}
include_hooks_files:
- common/hooks/systemd-net-static.yml
- common/hooks/systemd-dns.yml
- common/hooks/hosts.yml
nodes:
- name: {{ $v.name }}
image_source: "nginx/{{ $.Values.release }}"
image_remote_server: "myserver"
hooks:
- event: post-node-sync
flags:
- config
commands:
- >-
systemctl daemon-reload &&
systemctl enable nginx &&
systemctl restart nginx
# Generate the nginx configuration file based on template
# and project variables.
config_templates:
- source: nginx/templates/nginx.tmpl
dst: /tmp/lxd-compose/myinfra/{{ $v.name }}/nginx.conf
sync_resources:
- source: /tmp/lxd-compose/myinfra/{{ $v.name }}/nginx.conf
dst: /etc/nginx/
{{ end }}
Some helpful commands to analyze the specs are these:
$> lxd-compose project list
| PROJECT NAME | DESCRIPTION | # GROUPS |
|-------------------|-------------------------------------------------------------------------|----------|
| nginx-services | Setup Nginx Services | 2 |
$> lxd-compose group list nginx-services
| GROUP NAME | DESCRIPTION | # NODES |
|----------------|--------------------------------------|---------|
| nginx1-group | Setup Nginx Frontend Node nginx1 | 1 |
| nginx2-group | Setup Nginx Frontend Node nginx2 | 1 |
Deploy the service #
The first time the all containers are down it’s possible just deploy all nodes with the apply
command:
$> lxd-compose apply nginx-services
When you want upgrade a production service it’s a good practice replace one node a time in this way:
$> # Destroy the node nginx1.
$> lxd-compose destroy nginx-services --enable-group nginx1-group
$> # Deploy the new container related to the new release 22.10.01.
$> # The variable release could be passed in input or updated directly on default.yaml
$> lxd-compose apply nginx-services --enable-group nginx1-group --render-env "release=22.10.01"
The same could be done in for the second node when the first is up and running.
If you have a slow bandwitdh between the LXD images server could be helpful download the images
over the VMs before execute the upgrade with the fetch
command:
$> lxd-compose fetch nginx-services
This command will download the LXD images over the configured LXD instances of the selected project without destroy and/or create containers.
In the example it’s used the image LXD with aliasnginx/<release>
. This image is created and exposed over HTTPS in a separated step. If this is not available it’s possible to use the images available over Macaroni Simplestreams Server or Canonical Server and just install packages in thepost-node-creation
phase to prepare the container with all needed software.
If it’s needed add a new node/VM you need just editing the file prod.yaml
and the config.yml
to add the new node.
$> cat lxd-conf/config.yml
...
vm3:
addr: https://vm3.infra:8443
auth_type: tls
project: default
protocol: lxd
public: false
$> cat render/prod.yml
release: "22.10"
nginx_nodes:
- connection: "vm1"
name: "nginx1"
- connection: "vm2"
name: "nginx2"
- connection: "vm3"
name: "nginx3"
Again, if it’s needed update the configuration files of all Nginx servers this could be
executed sequentially from lxd-compose
with the same apply
command.
In the reported example for every container is assigned a static IP address that is
defined in one vars file following this pattern that display the file nginx1-net.yml
:
envs:
nginx1_resolved:
- file: dns.conf
content: |
[Resolve]
DNS=1.1.1.1
FallbackDNS=8.8.8.8
Domains=
MulticastDNS=no
LLMNR=no
nginx1_hosts:
# - ip: <ip>
# domain: <domain1>,<domain2>
- ip: "10.10.10.1"
domain: nginx-backend01
- ip: "10.10.10.2"
domain: nginx-backend02
nginx1_net:
- file: 01-srv0.network
content: |
[Match]
Name=srv0
[Network]
Address=172.18.20.100/24
Gateway=172.18.20.1
LLMNR=no
# Disable binding of port ::58
IPv6AcceptRA=no
The hooks used for the configuration are systemd-net-static.yml
, systemd-dns.yml
and hosts.yml
.
Upgrade the OS of the VM #
Hereinafter a short description about what are the possibile steps to follow in Production to replace the VM and move an active container over a new VM where is been upgraded the OS.
Obviously, it’s possible upgrade the OS and the LXD instance without replacing the VM but we will try to share a way that could be used for rollback if something goes wrong.
So, the first step is to clone the existing VM or just create a new VM from a template. If the choice is cloning, just disable service network from VmWare before boostrap of the VM.
The new VM with a specific management interface could be upgraded meantime the service is up and running. Until the network interface srv0 is not assigned to the container is visible in the VM.
When the new VM is ready the steps to follow are describe in the image hereinafter:
In short, through the lxd-compose
tool it’s needed destroy the existing container.
Eventually, you can just create a backup of the container with the normal lxc
tool
or copy it before run the destroy command.
$> # Create the backup of the container for the rollback.
$> lxc copy vm1:nginx1 vm1:nginx-bkp1
$> # Or just stop the existing container.
$> lxc stop vm1:nginx1
If the container is stopped with lxc
command the destroy command is not needed.
When the container is stopped or destroyed, the service IP address 172.18.20.100
of the example will be reused in the deploy of the new container over the new VM.
To maintain the same name of the container it’s only needed modify the prod.yml
file to have vm1-new
(or any other name choiced) that is the name of the
remote of the new VM.
So, in the prod.yml
the content become:
release: "22.10"
nginx_nodes:
- connection: "vm1-new"
name: "nginx1"
- connection: "vm2"
name: "nginx2"
- connection: "vm3"
name: "nginx3"
After this change it’s only needed to execute the apply command that deploy the new container:
$> lxd-compose apply --enable-group nginx1-group
If something goes wrong and the user want rollback the previous container it’s needed:
shutdown the new VM; if the container is been stopped and not destroyed, just rerun
the apply command with the connection
reconfigured to vm1
.
If the container is been destroyed and it’s been used a production-ready LXD image
the user could just redeploy the new container with the previous release
.
Best Practices for Production environments #
As all know, today all Linux distribution are Rolling Release
for different
reasons: the world go ahead very fast, CVE and security issues that are identified
every day requires fast updates, etc. This means that what is deployed at time T0
at the 99% is not installable ad the same way at time T1. So, for production
services it’s better to prepare LXD images that will be used on delivery without
execute OS upgraded on container creation.
The upgrades of the container OS must be apply in a testing environment where it’s possible verify that there aren’t regression with a new LXD images that when the QA are passed could be used to upgrade production environment.
The Simplestreams Builder could be used to prepare an HTTPS endpoint where expose LXD images used in the installation over the Simplestreams Protocol. The alternative is to use directly another LXD Instance as LXD images supplier.
For my experience it works well to try using an environment
specification with a
single project to define a group of VMs that supply a specific service. This will
simplify the upgrade process with the use of one group
for every LXD instance
as described in the previous chapter. Users are free to find their correct way.
Having multiple projects defined in the same environment file it works too like
to have a static definition of the groups without using render engine.
Using the physical
nictype doesn’t permit to see the network interfaces from
the VM OS, so it’s important to supply on the created LXD images the right tools for
throubleshooting and analyses (tcpdump, ethtool, etc.).
Using a VmWare VM already supply a good isolation but to improve the security levels using the unprivileged containers is the best choice.
In a production environment often it’s important to maintain the logs files of exposed services for subsequent analyzes. This could be handled through the mount of the VM path inside the container that is persistent between the upgrade of one container with another but not if the VM is replaced. But there are different ways to resolve this efficiently for example through an Rsyslog remote server or over Vmware using a secondary disk that is detached and attached to the new VM after the upgrade. In the presented example the profile used for this mission is logs-disk.
The use of additional API to setup and create a VM through the VmWare API it’s out of scope of this guide. This doesn’t mean that is not possible.lxd-compose
permits to define hookspre-project
related to the node host that means the local shell where lxd-compose is executed. Inside that hook it’s possible execute a bash script or other that prepare the VmWare VM before execute the delivery of the container.