Use Cloud-Init to Install and Update Software on New Servers

Create a Linode account to try this guide with a $100 credit.
This credit will be applied to any valid services used during your first 60 days.

Cloud-init offers a cross-platform, cross-distribution approach to automating server initialization. With Akamai’s Metadata service, you can leverage cloud-init to deploy Compute Instances, employing custom user data scripts to define your desired setup.

In this guide, learn how to manage packages on new servers using cloud-init. Whether you want to upgrade system packages, install packages during initialization, or manage your repositories, this tutorial shows you how.

Before getting started, you should review our guide on how to Use Cloud-Init to Automatically Configure and Secure Your Servers. There, you can see how to create a cloud-config file, which you need to follow along with the present guide. When you are ready to deploy your cloud-config, the guide linked above shows you how.

Upgrade Packages

Cloud-init includes a module for managing package updates and upgrades during initialization. The package_update option, when set to true, applies updates to installed packages and updates the package repositories. This option is generally useful to ensure the server is working from the most up-to-date package references.

File: cloud-config.yaml
1
package_update: true

The package_upgrade option upgrades installed packages to their latest versions. Unless you need specific package versions, running this option during initialization keeps your system more stable and secure.

File: cloud-config.yaml
1
package_upgrade: true

Additionally, cloud-config has an option to ensure that the system reboots for any package upgrades or installations that require it. With this option, you ensure that package upgrades and installations are ready to use immediately after the cloud-init process has finished.

File: cloud-config.yaml
1
2
3
package_update: true
package_upgrade: true
package_reboot_if_required: true

Install Packages

To install packages with cloud-init, use the packages option in your cloud-config. Provide the option a list of package names, and cloud-init handles installation during the initialization.

Below are examples installing the main components of a LAMP stack, a popular web application setup. Cloud-config requires exact package names, which can vary between distributions, as can the overall prerequisites for a setup. To demonstrate, the examples below show how the setup would look between two different distributions.

Learn more about the LAMP stack, and its package prerequisites, in our guide on How to Install a LAMP Stack. Use the drop down at the top of that guide to see different distributions.

File: cloud-config.yaml
1
2
3
4
5
6
packages:
  - apache2
  - mysql-server
  - php
  - libapache2-mod-php
  - php-mysql
Note
The package_reboot_if_required option covered in the previous section also affects package installations. If set to true, it ensures that the system restarts if any newly installed packages require it.

Add Software Repositories

Among the more advanced package manager tools within cloud-init is the ability to add custom repositories during initialization. Cloud-init uses specific modules for managing different package managers, so the steps vary depending on your distribution. What follows covers two of the most popular: APT, most often found on Debian and Ubuntu systems, and Yum/DNF, mostly found on CentOS, Fedora, and other RHEL-based distributions.

Other than these, cloud-init also supports the Zypper package manager, used on openSUSE distributions. You can learn about adding repositories for Zypper in cloud-init’s Zypper Add Repo module reference documentation.

Within cloud-config, the apt option allows for fine-grained management of the APT package manager. You can learn more about the range of features through cloud-init’s APT Configure module reference.

To add third-party repositories to APT, use the sources option within an apt block. The sources option is a dictionary, with one or more repository entries. Each repository entry needs a source string, indicating the repository location, and a set of key-related options, providing the GPG key for the repository.

There are two means of adding the repository, based on how you want to supply the GPG key:

  • To use a GPG key server, you can supply the key ID and the server location, as in this example.

    File: cloud-config.yaml
    1
    2
    3
    4
    5
    6
    
    apt:
      sources:
        docker:
          source: deb [arch="amd64"] https://download.docker.com/linux/ubuntu $RELEASE stable
          keyid: 8D81803C0EBFCD88
          keyserver: 'https://download.docker.com/linux/ubuntu/gpg'

    In this case, the key ID was obtained from the GPG public key using this set of commands:

    wget https://download.docker.com/linux/ubuntu/gpg -O docker.gpg.pub.key
    gpg --list-packets docker.gpg.pub.key | awk '/keyid:/{ print $2 }'
  • The other option is to manually add the GPG key, using the key option, as shown in this example. Replace the example GPG string with a full GPG public key, like the one retrieved with the wget command above.

    File: cloud-config.yaml
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    apt:
      sources:
        docker:
          source: deb [arch="amd64"] https://download.docker.com/linux/ubuntu $RELEASE stable
          key: |
            -----BEGIN PGP PUBLIC KEY BLOCK-----
    
            mQINBFit2ioBEADhWpZ8/wvZ6hUTiXOwQHXMAlaFHcPH9hAtr4F1y2+OYdbtMuth
            lqqwp028AqyY+PRfVMtSYMbjuQuu5byyKR01BbqYhuS3jtqQmljZ/bJvXqnmiVXh
            38UuLa+z077PxyxQhu5BbqntTPQMfiyqEiU+BKbq2WmANUKQf+1AmZY/IruOXbnq
            ...
            jCxcpDzNmXpWQHEtHU7649OXHP7UeNST1mCUCH5qdank0V1iejF6/CfTFU4MfcrG
            YT90qFF93M3v01BbxP+EIY2/9tiIPbrd
            =0YYh
            -----END PGP PUBLIC KEY BLOCK-----        

With either method, you can verify the added repository on the new system with a command like this:

sudo apt-cache policy
Package files:
 100 /var/lib/dpkg/status
     release a=now
 500 https://download.docker.com/linux/ubuntu jammy/stable amd64 Packages
     release o=Docker,a=jammy,l=Docker CE,c=stable,b=amd64
     origin download.docker.com
...

Verify Update and Installation

Cloud-init stores a log at /var/log/cloud-init-output.log with all of the output from cloud-init’s initialization steps. For instance, the example output below shows the portion of the logs for APT installing the apache2 package.

sudo cat /var/log/cloud-init-output.log
...
The following additional packages will be installed:
  apache2-bin apache2-data apache2-utils libapache2-mod-php8.1 libapr1
  libaprutil1 libaprutil1-dbd-sqlite3 libaprutil1-ldap libcgi-fast-perl
  libcgi-pm-perl libclone-perl libencode-locale-perl libevent-pthreads-2.1-7
  libfcgi-bin libfcgi-perl libfcgi0ldbl libhtml-parser-perl
  libhtml-tagset-perl libhtml-template-perl libhttp-date-perl
  libhttp-message-perl libio-html-perl liblua5.3-0 liblwp-mediatypes-perl
  libmecab2 libprotobuf-lite23 libtimedate-perl liburi-perl mecab-ipadic
  mecab-ipadic-utf8 mecab-utils mysql-client-8.0 mysql-client-core-8.0
  mysql-common mysql-server-8.0 mysql-server-core-8.0 php-common php8.1
  php8.1-cli php8.1-common php8.1-mysql php8.1-opcache php8.1-readline
  ssl-cert
Suggested packages:
  apache2-doc apache2-suexec-pristine | apache2-suexec-custom www-browser
  php-pear libdata-dump-perl libipc-sharedcache-perl libbusiness-isbn-perl
  libwww-perl mailx tinyca
The following NEW packages will be installed:
  apache2 apache2-bin apache2-data apache2-utils libapache2-mod-php
  libapache2-mod-php8.1 libapr1 libaprutil1 libaprutil1-dbd-sqlite3
  libaprutil1-ldap libcgi-fast-perl libcgi-pm-perl libclone-perl
  libencode-locale-perl libevent-pthreads-2.1-7 libfcgi-bin libfcgi-perl
  libfcgi0ldbl libhtml-parser-perl libhtml-tagset-perl libhtml-template-perl
  libhttp-date-perl libhttp-message-perl libio-html-perl liblua5.3-0
  liblwp-mediatypes-perl libmecab2 libprotobuf-lite23 libtimedate-perl
  liburi-perl mecab-ipadic mecab-ipadic-utf8 mecab-utils mysql-client-8.0
  mysql-client-core-8.0 mysql-common mysql-server mysql-server-8.0
  mysql-server-core-8.0 php php-common php-mysql php8.1 php8.1-cli
  php8.1-common php8.1-mysql php8.1-opcache php8.1-readline ssl-cert
0 upgraded, 49 newly installed, 0 to remove and 11 not upgraded.
...

While the logs’ high level of detail is useful for debugging, it makes verifying upgraded and installed packages somewhat cumbersome. Instead, use commands specific to your system’s package manager to verify package upgrades and installations. Below you can find steps for doing that on Debian/Ubuntu systems (using APT) and RHEL-based systems like CentOS and Fedora (using DNF or Yum).

The APT package manager includes a list command that provides useful functions for reviewing packages. Using the --upgradable option with the command displays a list of packages that have available upgrades.

sudo apt list --upgradable

Ideally, the output would be empty, but your system usually has a few packages that do not get upgraded with apt upgrade. Typically, this is due to dependencies. If this is the case, the upgrade command, and the cloud-init logs, should indicate the un-upgraded packages.

File: /var/log/cloud-init-output.log
1
2
3
...
0 upgraded, 0 newly installed, 0 to remove and 11 not upgraded.
...

The list command’s --installed option provides a comprehensive list of packages installed with the APT package manager. However, this list can be hard to navigate for verification as it also includes all packages installed as dependencies. To only see the packages that have been installed explicitly, use the --manual-installed option instead.

sudo apt list --manual-installed

The package installation example earlier in this guide only had a few packages, so a short text filter could even further shorten the output. The command below does this by piping to grep. Each \| separates a search term, and each search term identifies one or more of the packages indicated in the cloud-config.

sudo apt list --manual-installed | grep 'apache2\|mysql-server\|php'
apache2/jammy-updates,now 2.4.52-1ubuntu4.6 amd64 [installed]
libapache2-mod-php/jammy,now 2:8.1+92ubuntu1 all [installed]
mysql-server/jammy-updates,jammy-security,now 8.0.34-0ubuntu0.22.04.1 all [installed]
php-mysql/jammy,now 2:8.1+92ubuntu1 all [installed]
php/jammy,now 2:8.1+92ubuntu1 all [installed]

More Information

You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

This page was originally published on


Your Feedback Is Important

Let us know if this guide was helpful to you.


Join the conversation.
Read other comments or post your own below. Comments must be respectful, constructive, and relevant to the topic of the guide. Do not post external links or advertisements. Before posting, consider if your comment would be better addressed by contacting our Support team or asking on our Community Site.
The Disqus commenting system for Linode Docs requires the acceptance of Functional Cookies, which allow us to analyze site usage so we can measure and improve performance. To view and create comments for this article, please update your Cookie Preferences on this website and refresh this web page. Please note: You must have JavaScript enabled in your browser.