User data is up to 16 KB of text passed at launch. On first boot cloud-init runs it as root, either as a #! script or, when it starts with #cloud-config, as declarative YAML:
#cloud-config
package_update: true
packages:
- apache2
- libapache2-mod-php
- php-mysql
- php-mbstring
- php-xml
- mysql-server
swap:
filename: /swapfile
size: 1G
write_files:
- path: /var/www/html/index.php
defer: true
owner: www-data:www-data
permissions: '0644'
content: |
<?php
printf("Hello from %s, PHP %s, %s\n",
gethostname(), PHP_VERSION, php_sapi_name());
runcmd:
- rm -f /var/www/html/index.html
- systemctl enable --now apache2 mysqlThe swap file matters on 1 GiB with MySQL 524 . defer: true waits until the packages exist, so the www-data owner is valid, and runcmd runs last. SSH comes up before setup ends, so wait for it:
cloud-init status --wait; cloud-init status --longOutput
...status: done status: done extended_status: done ... errors: []
The package step took 56.3 seconds (cloud-init analyze show), and the whole setup finished 73.88 seconds after boot. On failure, read /var/log/cloud-init-output.log. User data runs once per instance, and any process on the instance can read it from the metadata service, so never put secrets in it. Add package_upgrade: true for production: 17 updates were already waiting.