Technology

Dynamic Management of OVH Instances in Laravel PHP

In our sale events platform, Auctibles, we use the OVH public cloud to run a dynamic set of compute instances running the LiveKit WebRTC video server. We vary the number of instances based on the number of video-streaming sale events.

As our platform is built using the Laravel PHP framework, we use the php-ovh SDK to manage instances.

We define an OVH service where all interaction with the OVH public cloud will occur.

#The Service

use \Ovh\Api;

class OVH
{

		private $ovh;
		private $flavor; // aka VM type based on size
    private $image_name;
    private $datacenter_region_name;
    
    public function __construct()
    {
        $this->flavor = config('ovh.flavor');
        $this->image_name = config('ovh.image_name');
        $this->datacenter_region_name = config('ovh.region');

        $this->ovh = new Api(
            config('ovh.OVH_APPLICATION_KEY'),
            config('ovh.OVH_APPLICATION_SECRET'),
            config('ovh.OVH_ENDPOINT'),
            config('ovh.OVH_CONSUMER_KEY')
        );            
       
    }

}

After creating an instance, we launch a job to monitor progress:

WaitInstance::dispatch($instance_id, 'started');

#Create Instance

A service is a project in the OVH public cloud, the network is the public Internet, and the instance name is a random string that our code generates.

public function create_instance($service_name, $network_id, $instance_name)
{
    try {

        $user_data_str = $this->fill_cloud_init(config('ovh.OVH_USER_DATA_FILE_NAME'));

					// get all images for our region for this particular flavor
        $result = $this->ovh->get("/cloud/project/$service_name/image", array(
            'flavorType' => $this->flavor, 
            'osType' => 'linux', 
            'region' => $this->datacenter_region_name, 
        ));

        $image_name = $this->image_name;
        $images = collect($result)->filter(function($image)  use($image_name) {
            return $image['name'] == $image_name; 
        });
		
					// Ubunto image to create
        $ubuntu_image = $images->first();

					
        $server_config = [
            'flavorId' => $this->flavor,
            'imageId' => $ubuntu_image['id'],
            'sshKeyId' => config('ovh.OVH_SSH_KEY_ID'),
            'name' => $instance_name,
            'region' => $this->datacenter_region_name,
            'networks' => [
                [
	                    // connect the server to our private network for the project
                    'networkId' => config('ovh.OVH_PRIVATE_NETWORK_ID'),
                ],
                [
                    'networkId' => $network_id,
                ]
            ],

            'userData' => $user_data_str,
        ];

        $result = $this->ovh->post("/cloud/project/$service_name/instance", 
            $server_config
        );
        return $result;

    }  catch (GuzzleHttp\Exception\ClientException $e) {
        $response = $e->getResponse();
        $responseBodyAsString = $response->getBody()->getContents();        
        throw new Exception("ClientExcepion cannot create instance $responseBodyAsString");

    } catch(GuzzleHttp\Exception\ServerException $e) {
        $response = $e->getResponse();
        $responseBodyAsString = $response->getBody()->getContents();
        throw new Exception("ServerExcepion cannot create instance $responseBodyAsString");   
   
    } catch(Exception $e) {
        $message = $e->getMessage();
        throw new Exception("Exception $message");

    }

}

#Destroy Instance

To destroy an instance, we first stop it.

public function destroy_instance($service_name, $instance)
{
    try {

        if (!$this->stop_livekit($service_name, $instance, config('ovh.OVH_LB_USER'))) {
            throw new Exception('cannot stop livekit');
        }

        $this->ovh->post("/cloud/project/$service_name/instance/$instance_id/stop");


        WaitInstance::dispatch($instance_id, 'stopped');

        return true;

    } catch (GuzzleHttp\Exception\ClientException $e) {
        $response = $e->getResponse();
        $responseBodyAsString = $response->getBody()->getContents();
        // notify on failure to destroy
        throw new Exception("ClientExcepion cannot do OVH action destroy_instance on instance $instance_id $responseBodyAsString");     
    } catch(GuzzleHttp\Exception\ServerException $e) {
        $response = $e->getResponse();
        $responseBodyAsString = $response->getBody()->getContents();
        // notify on failure to destroy
        throw new Exception("ServerExcepion cannot do OVH action destroy_instance on instance $instance_id $responseBodyAsString");          
    } catch(Exception $e) {
        // notify on failure to destroy
        throw new Exception("cannot do destroy instance $instance_id " . $e->getMessage());
    }


}

We launch a job to monitor the stopping of the instance:

WaitInstance::dispatch($instance_id, 'stopped');

#Monitoring Progress

class WaitInstance implements ShouldQueue, ShouldBeEncrypted
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $ovh;

    /**
     * Create a new job instance.
     */
    public function __construct(private string $instance_id, private string $goal, private int $times = 0)
    {
        $this->onQueue('ovh');        
    }
    
}

Where $goal is started or stopped.

 public function handle(): void
{
    $this->ovh = new OVH();
    $instance = $this->ovh->get_instance(config('ovh.OVH_SERVICE_NAME'), $this->instance_id);
  }

We get the instance and check its status relative to our goal:

if ($this->goal == 'started' && $instance['status'] == 'ACTIVE') {
	// instance running
} else if ($this->goal == 'stopped' && $instance['status'] == 'SHUTOFF') { 
	// instance stopped
	
	// delete instance
	$this->ovh->delete("/cloud/project/$service_name/instance/$instance_id");
}    

If none of these cases, we enter into a loop of waiting a maximum of $times, each time for 1 minute. So we dispatch again the WaitInstance job with a delay of 1 minute.

if ($this->times + 1 < config('ovh.number_of_waits')) { // still can loop
	// dispatch the wait job in a delay of 1 minute
	WaitInstance::dispatch($this->instance_id, $this->goal, $this->times + 1)->delay(now()->addMinutes(1));
}
Yoram Kornatzky

Yoram Kornatzky