Cartoon space rocket flying in the stars

Migrating to Platform.sh

Thumbnail

Campbell Tilley

|

If you have an existing site, that you've been working on locally or were hosting elsewhere, this article will take you through the steps required to migrate your site to Platform.sh

Last week, in Getting started with Platform.sh, we looked at how to setup a fresh Drupal 8 install on Platform.sh. This week we are going to look at how we can migrate an existing site to Platform.sh.

Before starting the below, create a new project in Platform.sh with a blank template. Refer to my previous article if you are unsure how to do this.

File structure

Platform.sh strongly recommends installing the Platform.sh Config Reader library. Install this by running the following command while in the root of your project. 

composer require platformsh/config-reader

In order to push our code and host our site on Platform.sh, we need to add some additional folders and files to our project.

The below is taken from the Drupal 8 section of the Platform.sh docs.

composer.json
composer.lock
config/
  sync/
    <this is where exported configuration will go> 
drush/
.git/
.gitignore
.platform/
  routes.yaml
  services.yaml
.platform.app.yaml
scripts/
web
  index.php
  ... (other Drupal core files)
  modules/
    contrib/
      <empty until composer runs>
    custom/
      <your custom modules here>
  themes/
    contrib/
      <empty until composer runs>
    custom/
      <your custom themes here>
  sites/
    default/
      settings.php
      settings.platformsh.php

There are a few new files and folders above that are specific to Platform.sh, that you likely didn't have in your codebase already. Ensure you add these exactly as listed before moving to the next step.

File configuration

We have our folder structure, now we need to configure our new files.

Firstly, we need to setup our .platform.app.yaml. The below code snippet is an example provided by Platform.sh in their documentation.

# .platform.app.yaml

# The name of this application, which must be unique within a project.
name: 'app'

# The type key specifies the language and version for your application.
type: 'php:7.0'

# On PHP, there are multiple build flavors available. Pretty much everyone
# except Drupal 7 users will want the composer flavor.
build:
  flavor: composer

# The relationships of the application with services or other applications.
# The left-hand side is the name of the relationship as it will be exposed
# to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand
# side is in the form <service name>:<endpoint name>.
relationships:
    database: 'mysqldb:mysql'

# The hooks that will be triggered when the package is deployed.
hooks:
    # Build hooks can modify the application files on disk but not access any services like databases.
    build: |
      rm web/app_dev.php
    # Deploy hooks can access services but the file system is now read-only.
    deploy: |
      app/console --env=prod cache:clear


# The size of the persistent disk of the application (in MB).
disk: 2048

# The 'mounts' describe writable, persistent filesystem mounts in the application.
# The keys are directory paths relative to the application root. The values are a
# mount definition. In this case, web-files is just a unique name for the mount.
mounts:
    'web/files':
        source: local
        source_path: 'web-files'

# The configuration of the application when it is exposed to the web.
web:
    locations:
        '/':
            # The public directory of the application relative to its root.
            root: 'web'
            # The front-controller script which determines where to send
            # non-static requests.
            passthru: '/app.php'
        # Allow uploaded files to be served, but do not run scripts.
        # Missing files get mapped to the front controller above.
        '/files':
            root: 'web/files'
            scripts: false
            allow: true
            passthru: '/app.php'

Next we need to setup routes.yaml and services.yaml which live under the .platform directory.

For us, using the example routes.yaml from Platform.sh docs has been sufficient:

"https://{default}/":
  type: upstream
  upstream: "app:http"
"https://www.{default}/":
  type: redirect
  to: "https://{default}/"

However, we have modified our services.yaml. The example from Platform.sh is:

database1:
  type: mysql:10.1
  disk: 2048

database2:
  type: postgresql:9.6
  disk: 1024

Whereas ours looks like:

mysqldb:
  type: mysql:10.1
  disk: 2048

rediscache:
  type: "redis:3.0"

Lastly we need to configure settings.platformsh.php. Here is the default file from the Drupal 8 template:

<?php
/**
 * @file
 * Platform.sh settings.
 */
$platformsh = new \Platformsh\ConfigReader\Config();
if (!$platformsh->inRuntime()) {
  return;
}
// Configure the database.
$creds = $platformsh->credentials('database');
$databases['default']['default'] = [
  'driver' => $creds['scheme'],
  'database' => $creds['path'],
  'username' => $creds['username'],
  'password' => $creds['password'],
  'host' => $creds['host'],
  'port' => $creds['port'],
  'pdo' => [PDO::MYSQL_ATTR_COMPRESS => !empty($creds['query']['compression'])]
];
// Enable Redis caching.
if ($platformsh->hasRelationship('redis') && !drupal_installation_attempted() && extension_loaded('redis')) {
  $redis = $platformsh->credentials('redis');
  // Set Redis as the default backend for any cache bin not otherwise specified.
  $settings['cache']['default'] = 'cache.backend.redis';
  $settings['redis.connection']['host'] = $redis['host'];
  $settings['redis.connection']['port'] = $redis['port'];
  // Apply changes to the container configuration to better leverage Redis.
  // This includes using Redis for the lock and flood control systems, as well
  // as the cache tag checksum. Alternatively, copy the contents of that file
  // to your project-specific services.yml file, modify as appropriate, and
  // remove this line.
  $settings['container_yamls'][] = 'modules/contrib/redis/example.services.yml';
  // Allow the services to work before the Redis module itself is enabled.
  $settings['container_yamls'][] = 'modules/contrib/redis/redis.services.yml';
  // Manually add the classloader path, this is required for the container cache bin definition below
  // and allows to use it without the redis module being enabled.
  $class_loader->addPsr4('Drupal\\redis\\', 'modules/contrib/redis/src');
  // Use redis for container cache.
  // The container cache is used to load the container definition itself, and
  // thus any configuration stored in the container itself is not available
  // yet. These lines force the container cache to use Redis rather than the
  // default SQL cache.
  $settings['bootstrap_container_definition'] = [
    'parameters' => [],
    'services' => [
      'redis.factory' => [
        'class' => 'Drupal\redis\ClientFactory',
      ],
      'cache.backend.redis' => [
        'class' => 'Drupal\redis\Cache\CacheBackendFactory',
        'arguments' => ['@redis.factory', '@cache_tags_provider.container', '@serialization.phpserialize'],
      ],
      'cache.container' => [
        'class' => '\Drupal\redis\Cache\PhpRedis',
        'factory' => ['@cache.backend.redis', 'get'],
        'arguments' => ['container'],
      ],
      'cache_tags_provider.container' => [
        'class' => 'Drupal\redis\Cache\RedisCacheTagsChecksum',
        'arguments' => ['@redis.factory'],
      ],
      'serialization.phpserialize' => [
        'class' => 'Drupal\Component\Serialization\PhpSerialize',
      ],
    ],
  ];
}
// Configure private and temporary file paths.
if (!isset($settings['file_private_path'])) {
  $settings['file_private_path'] = $platformsh->appDir . '/private';
}
if (!isset($config['system.file']['path']['temporary'])) {
  $config['system.file']['path']['temporary'] = $platformsh->appDir . '/tmp';
}
// Configure the default PhpStorage and Twig template cache directories.
if (!isset($settings['php_storage']['default'])) {
  $settings['php_storage']['default']['directory'] = $settings['file_private_path'];
}
if (!isset($settings['php_storage']['twig'])) {
  $settings['php_storage']['twig']['directory'] = $settings['file_private_path'];
}
// Set trusted hosts based on Platform.sh routes.
if (!isset($settings['trusted_host_patterns'])) {
  $routes = $platformsh->routes();
  $patterns = [];
  foreach ($routes as $url => $route) {
    $host = parse_url($url, PHP_URL_HOST);
    if ($host !== FALSE && $route['type'] == 'upstream' && $route['upstream'] == $platformsh->applicationName) {
      // Replace asterisk wildcards with a regular expression.
      $host_pattern = str_replace('\*', '[^\.]+', preg_quote($host));
      $patterns[] = '^' . $host_pattern . '$';
    }
  }
  $settings['trusted_host_patterns'] = array_unique($patterns);
}
// Import variables prefixed with 'd8settings:' into $settings
// and 'd8config:' into $config.
foreach ($platformsh->variables() as $name => $value) {
  $parts = explode(':', $name);
  list($prefix, $key) = array_pad($parts, 3, null);
  switch ($prefix) {
    // Variables that begin with d8settings or drupal get mapped
    // to the $settings array verbatim, even if the value is an array.
    // For example, a variable named d8settings:example-setting' with
    // value 'foo' becomes $settings['example-setting'] = 'foo';
    case 'd8settings':
    case 'drupal':
      $settings[$key] = $value;
      break;
    // Variables that begin with d8config get mapped to the $config
    // array.  Deeply nested variable names, with colon delimiters,
    // get mapped to deeply nested array elements. Array values
    // get added to the end just like a scalar. Variables without
    // both a config object name and property are skipped.
    // Example: Variable d8config:conf_file:prop with value foo becomes
    // $config['conf_file']['prop'] = 'foo';
    // Example: Variable d8config:conf_file:prop:subprop with value foo becomes
    // $config['conf_file']['prop']['subprop'] = 'foo';
    // Example: Variable d8config:conf_file:prop:subprop with value ['foo' => 'bar'] becomes
    // $config['conf_file']['prop']['subprop']['foo'] = 'bar';
    // Example: Variable d8config:prop is ignored.
    case 'd8config':
      if (count($parts) > 2) {
        $temp = &$config[$key];
        foreach (array_slice($parts, 2) as $n) {
          $prev = &$temp;
          $temp = &$temp[$n];
        }
        $prev[$n] = $value;
      }
      break;
  }
}
// Set the project-specific entropy value, used for generating one-time
// keys and such.
$settings['hash_salt'] = $settings['hash_salt'] ?? $platformsh->projectEntropy;
// Set the deployment identifier, which is used by some Drupal cache systems.
$settings['deployment_identifier'] = $settings['deployment_identifier'] ?? $platformsh->treeId;

Copy the above and modify to your project needs.

Migrating to Platform.sh

Code

We have configured our codebase and are now ready to migrate to Platform.sh. The following two commands will push your code to your new Platform.sh project. Ensure you swap out the project ID for your own project ID.

git remote add platform [email protected]:nodzrdripcyh6.git
git push -u platform master

Database

You can import your database using the following command.

platform sql < my_database_snapshot.sql

You can also specify the target environment and database relationship.

platform sql --relationship database -e master < my_database_snapshot.sql

Files

The best way to load files to Platform.sh is by using rsync. Below is an example of using rsync to upload files from the private directory.

rsync -az ./private platform ssh --pipe:/app/private/

You can also specify an environment by adding -e after --pipe.

rsync -az ./private platform ssh --pipe -e master:/app/private/

 

We now have an existing site migrated to Platform.sh. To continue pushing updates to Platform.sh, use the command

git push platform master

Next week we will look at how to setup continuous integration with GitHub and Platform.sh.

Add new comment

The content of this field is kept private and will not be shown publicly.

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

Comments

  • Allowed HTML tags: <em> <strong> <cite> <blockquote cite> <ul type> <ol start type> <li> <dl> <dt> <dd> <p>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
  • Use [gist:#####] where ##### is your gist number to embed the gist
    You may also include a specific file within a multi-file gist with [gist:####:my_file].

Spread the word