How to run commands on Linux Container (LXD) instance at provision launch time

I would like to perform common automated configuration tasks and run commands/scripts after the LXD instance starts. How to use cloud-init to run commands on my Linux Container (LXD) instance at launch time?

LXD can use the cloud-init directive to run commands or scripts at the first boot cycle when you launch an instance using the lxc command.

What is a cloud-init?

cloud-init handles early initialization of a cloud instance including LXD and Linux containers. By default cloud-init installed in the Ubuntu/CentOS and all other major cloud images. With cloud-init you can configure:

Sample cloud-init file for lxc/lxd

  1. Hostname
  2. Update system
  3. Install additional packages
  4. Generate ssh private keys
  5. Install ssh keys to a users .ssh/authorized_keys so they can log in without a password
  6. Configure static IP or networking
  7. Include users/groups
  8. Creating files
  9. Install and run chef recipes
  10. Setup and run puppet
  11. Add apt or yum repositories
  12. Run commands on first boot
  13. Disk setup
  14. Configure RHN subscription and more.

Let us get started with an example.

Step 1: Create lxc container

Type the following command to create a Ubuntu LXC container called foo (but do not run the lxc container yet):
$ lxc init ubuntu: foo
One can create a CentOS 7 based Linux container too:
$ lxc init images:centos/7/amd64 bar
You can apply certain profile too:
$ lxc init images:ubuntu/xenial/amd64 C2 -p staticlanwan

Step 2: Create yml cloud-init config file

In this example, I’m going to setup my lxc hostname, update my system, and Install ssh keys to a users .ssh/authorized_keys so they can log in without a password:
$ vi config.xml
First line must be #cloud-config:
#cloud-config
Next, I want to run ‘apt-get upgrade’ on first boot to download and install all security updates for my Linux container, so append:
# Apply updates using apt
package_upgrade: true

Setup hostname and domain name and update /etc/hosts file:
# Set hostname
hostname: foo
fqdn: foo.nixcraft.com
manage_etc_hosts: true

Run the following commands on first boot. In this case, update sshd to listen only on private IP and reload sshd, append:

#Run command on first boot only
bootcmd:
 - [sh, -c, "echo 'ListenAddress 192.168.1.100' >> /etc/ssh/sshd_config"]
 - systemctl reload ssh

You can install php7 and nginx packages as follows, append:

# Install packages
packages:
 - nginx
 - php-common
 - php7.0
 - php7.0-cli
 - php7.0-common
 - php7.0-fpm
 - php7.0-gd
 - php7.0-mysql
 - php7.0-opcache
 - php-pear

Finally, install a ssh-key for vivek login and add vivek to sudo file too, append:

# User setup
users:
 - name: vivek
   ssh-authorized-keys:
     - ***insert-your-key-here****
   sudo: ['ALL=(ALL) NOPASSWD:ALL']
   groups: sudo
   shell: /bin/bash

Save and close the file.

Step 3: Pass cloud-init directives to an instance with user data

You need to set a user.user-data variable as follows for foo Linux container:
$ lxc config set foo user.user-data -
To view your lxc config for foo container, run:
$ lxc config show foo
Sample outputs:


name: foo
profiles:
- default
config:
  user.user-data: "#cloud-config\npackage_upgrade: true\n\n#Set hostname\nhostname:
    foo\nfqdn: foo.nixcraft.com\nmanage_etc_hosts: true\n\n#Run command on first boot
    only\nbootcmd:\n - [sh, -c, \"echo 'ListenAddress 192.168.1.100' >> /etc/ssh/sshd_config\"]\n
    - systemctl reload ssh\n \n# Install packages\npackages:\n - nginx\n - php-common\n
    - php7.0\n - php7.0-fpm\n - php7.0-gd\n - php7.0-mysql\n\n# User setup\nusers:\n
    - name: vivek\n   ssh-authorized-keys:\n     - ***insert-your-key-here****\n   sudo:
    ['ALL=(ALL) NOPASSWD:ALL']\n   groups: sudo\n   shell: /bin/bash\n\n"
  volatile.apply_template: create
  volatile.base_image: 315bedd32580c3fb79fd2003746245b9fe6a8863fc9dd990c3a2dc90f4930039
  volatile.eth0.hwaddr: 00:16:3e:3d:d9:47
  volatile.last_state.idmap: '[{"Isuid":true,"Isgid":false,"Hostid":100000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":100000,"Nsid":0,"Maprange":65536}]'
devices:
  root:
    path: /
    type: disk
ephemeral: false

Step 4: Start your container

Type the following command:
$ lxc start foo
Wait for 2-5 minutes. To run all above tasks.

Step 5: Verify it

To login to foo LXC, enter:
$ lxc exec foo bash
Verify that sshd bind to private IP:
$ netstat -tulpn
Verify that packages are installed and system updated:
$ sudo tail -f /var/log/apt/history.log

A note about LXD not working with cloud-init

Please note that cloud-init in LXD triggers after network is up. In other words if network defined as DHCP or static but failed to get an IP address may result into hang ups in cloud-init. It will fail without much warning. Set the following command prior to the first container startup as described in step #4:
$ lxc config set foo user.network_mode link-local
$ lxc start foo

Log files for LXD

If you are having problems with cloud-init or cloud-config, take look at the following log files:
$ lxc exec foo bash
You can see the actual process logs for cloud-init's processing of the configuration file here:
# tail -f /var/log/cloud-init.log
Output of your commands can be found here:
# tail -f /var/log/cloud-init-output.log

Do I need to install the cloud-init package on the host server?

No.

References:

🐧 If you liked this page, please support my work on Patreon or with a donation.
🐧 Get the latest tutorials on SysAdmin, Linux/Unix, Open Source/DevOps topics:
CategoryList of Unix and Linux commands
File Managementcat
FirewallAlpine Awall CentOS 8 OpenSUSE RHEL 8 Ubuntu 16.04 Ubuntu 18.04 Ubuntu 20.04
Network Utilitiesdig host ip nmap
OpenVPNCentOS 7 CentOS 8 Debian 10 Debian 8/9 Ubuntu 18.04 Ubuntu 20.04
Package Managerapk apt
Processes Managementbg chroot cron disown fg jobs killall kill pidof pstree pwdx time
Searchinggrep whereis which
User Informationgroups id lastcomm last lid/libuser-lid logname members users whoami who w
WireGuard VPNAlpine CentOS 8 Debian 10 Firewall Ubuntu 20.04
2 comments… add one
  • Ben Nov 1, 2016 @ 11:06

    Good guide, was a good read.

    You may have a typo when creating the config file:
    vi config.xml

  • Lawrence Cherone Mar 29, 2017 @ 3:48

    You would think your presumptions about any remote: having cloud-init, step 1 create image… cloud-init is not bundled with images on images: only ubuntu: and ubuntu-daily: remotes. So only the first, lxc init ubuntu: foo would work… See issue here https://github.com/lxc/lxd/issues/3126

Leave a Reply

Your email address will not be published. Required fields are marked *

Use HTML <pre>...</pre>, <code>...</code> and <kbd>...</kbd> for code samples.