How to Ensure Service Starts After AWS User-Data Script Execution in Ubuntu 16.04 AMI


2 views

When working with AWS EC2 instances and custom AMIs, one common pain point is service startup sequencing relative to user-data execution. The default Ubuntu 16.04 cloud-init behavior creates a tricky dependency situation where:

cloud-init (cloud-final.service) → User-data scripts → Your custom service

The fundamental issue stems from cloud-final.service using RemainAfterExit=yes, which means it technically never "completes" from systemd's perspective, making traditional After= dependencies ineffective.

Here are three battle-tested approaches to solve this sequencing problem:

1. Using systemd Path Units

Create a path unit that watches for a user-data completion marker:

[Unit]
Description=Watch for user-data completion
Before=your-service.service

[Path]
PathExists=/var/lib/cloud/instance/boot-finished
Unit=your-service.service

[Install]
WantedBy=multi-user.target

2. Custom Startup Script with cloud-init Helper

Leverage cloud-init's native runcmd to trigger your service:

#cloud-config
runcmd:
  - systemctl start your-service

3. Modified Service Unit with ExecStartPre

Add explicit checks in your service definition:

[Unit]
Description=Your Custom Service
After=network.target cloud-init.target

[Service]
Type=simple
ExecStartPre=/bin/sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 2; done'
ExecStart=/usr/bin/your-service
Restart=on-failure

[Install]
WantedBy=multi-user.target

When choosing between these approaches, consider:

  • Timeout Handling: The Path Unit method may need additional timeout logic
  • Dependencies: Method #3 makes dependencies explicit in the unit file
  • Cloud-Init Version: Check /var/lib/cloud/instance/boot-finished exists in your AMI

After implementation, verify sequencing with:

journalctl -u cloud-final -u your-service --no-pager -o cat

For debugging startup timing issues:

systemd-analyze critical-chain your-service.service

When working with AWS EC2 instances, many developers encounter a common scenario: they need their custom systemd service to start after the user-data script completes execution. The standard approach of using After=cloud-final.service fails because:

# Check cloud-final.service properties
$ systemctl show cloud-final.service | grep RemainAfterExit
RemainAfterExit=yes

This RemainAfterExit=yes setting means the unit never technically "finishes," preventing dependent services from starting.

Method 1: Using cloud-init modules

The most robust solution is to leverage cloud-init's module system:

# Example /etc/cloud/cloud.cfg.d/99-my-service.cfg
cloud_final_modules:
 - [scripts-user, always]
 - [my-service-startup, once]

Then create a custom cloud-init module:

# /etc/cloud/cloud.cfg.d/my-service-startup.cfg
#cloud-config
runcmd:
 - systemctl start my-custom-service.service

Method 2: Create a custom target

For more control, create a dedicated target:

# /etc/systemd/system/user-data-complete.target
[Unit]
Description=User Data Processing Complete
After=cloud-final.service

Then modify your service:

[Unit]
Description=My Custom Service
After=user-data-complete.target
Requires=user-data-complete.target

[Service]
ExecStart=/usr/bin/my-service
Restart=on-failure

[Install]
WantedBy=multi-user.target

Method 3: Directly trigger from user-data

If you control the user-data script, simply add the service start command at the end:

#!/bin/bash
# Your user-data script content
apt-get update
apt-get install -y some-package
# ...
systemctl enable --now my-custom-service.service

To confirm your service starts after user-data completion:

# Check boot logs
$ journalctl -b | grep -E 'cloud-init|my-custom-service'

# Alternative using systemd-analyze
$ systemd-analyze critical-chain my-custom-service.service
  • Check cloud-init logs: /var/log/cloud-init.log
  • Verify execution order: systemd-analyze plot > boot-analysis.svg
  • Add explicit waits in user-data if needed: cloud-init status --wait