samdark / yii2-cookbook

Yii 2.0 Community Cookbook
1.45k stars 296 forks source link

Provide a recipe for Google App Engine #177

Open gusev-genotek opened 6 years ago

gusev-genotek commented 6 years ago

I have deployed yii2 based app to GAE. GAE can autoscale the instances horizontally. This presents some challenges:

  1. Using a centralized file storage for all instances. I am using gcsfuse to mount a single google cloud storage bucket for all user generated content etc, however I am yet to find a way to automatically mount a google bucket. As GAE can start and stop the instances (even in manual scaling mode) at will, the mount of the google storage bucket is not guaranteed.
  2. Caching and Session handling cannot be done in files, Redis and MemCache are combersome with GAE. Pretty often I get "Exception (Integrity constraint violation) 'yii\db\IntegrityException' with message 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1503914243' for key 'cache_expire_unique'" exception while using DBCache.
  3. GAE flexible environment routes requests from a single front end service to the instances via a ngnix server to port 8080. The worker instances with yii2 application are configured with apache2 servers serving 8080. How can this configuration be tuned?

It would be great to have comments on the above and best practice recommendations.

samdark commented 6 years ago

Indeed, it would be great to get info about it. I haven't deployed Yii 2 to GAE so if you did, please share your experience.

gusev-genotek commented 6 years ago

@samdark The above is basically my experience. To that I could add that the deployment is done via a Dockerfile and app.yaml files. What other details shall I provide?

samdark commented 6 years ago

Well, examples of such files would be cool.

gusev-genotek commented 6 years ago

I am attaching 2 Dockerfiles:

  1. Base image with our dependencies which are not changing often. Important for this discussion is the gcsfuse part. Currently this image can be found in docker hub as vladgen/yii2-apache-php7-bcm-gd-r-gcs Dockerfile-yii2-gae-base.txt

  2. Actual application image using 1. with the application php code that changes often. Dockerfile-yii2-gae-app.txt

  3. app.yaml file app.yaml.txt

to deploy this to GAE, place second Dockerfile and app.yaml into your yii2 project, change the settings to suit your app and run, for example

gcloud app deploy app.yaml --quiet --verbosity debug

Once it's deployed, ssh to your GAE instance (via a google web console), run

container_exec gaeapp /bin/bash

to get into your worker instance and then execute one of the gcsfuse mount commands. Since I am using fstab with gcsfuse I do

mount -a

to mount all gs buckets as folders, as described in the /etc/fstab.

The latter is done, by, for example


MY_BUCKET_NAME MY_LOCAL_PATH gcsfuse rw,noauto,allow_other,implicit_dirs,dir_mode=777,file_mode=777
tunecino commented 6 years ago

I don't know much about either GAE or dockers but I've seen a talk about a custom docker images they made for running PHP on their App Engine Flexible Runtime: https://github.com/GoogleCloudPlatform/php-docker

They where also talking about PHP security patches they added to their images. see it here: https://youtu.be/9PedC_6ZC3Q?t=7m19s

tunecino commented 6 years ago

I just deployed a similar template to this to a flex engine which is same as the advanced one except that it has api and auth folders as restful entries so I use no assets or session. But I use Redis within RedisLab (free up to 30mb and they are in the same datacenter) to store my short-living access tokens + I need it for later use with yii2-queue extension.

My steps where:

  1. GAE account + create new project + enable billing + download their SDK (better described here: https://cloud.google.com/php/getting-started/hello-world) NOTE: I use their Flexible environment instead of the Standard one as the latter uses php 5.5. read more about differences and tradeoffs of each here: https://cloud.google.com/php/quickstarts

  2. I used their SQL Cloud for DB which auto scales. You have to follow any of those steps and note your connectionName somewhere:

  3. For my 2 folders api and auth I created 2 files. Respectively api.yaml and auth.yaml as I wanted to deploy them as 2 separate services. They look pretty much the same. This is one of them:

runtime: php
env: flex
api_version: 1
service: auth

runtime_config:
  document_root: auth/web

beta_settings:
  cloud_sql_instances: "[connectionName]"

env_variables:
  # framework
  YII_DEBUG: false
  YII_ENV: prod
  #db
  POSTGRES_DSN: pgsql:dbname=alpha;host=/cloudsql/[connectionName]
  POSTGRES_USER: [user]
  POSTGRES_PASSWORD: [your-pass]
  POSTGRES_PORT: 5432

# This sample incurs costs to run on the App Engine flexible environment.
# The settings below are to reduce costs during testing and are not appropriate
# for production use. For more information, see:
# https://cloud.google.com/appengine/docs/flexible/python/configuring-your-app-with-app-yaml
manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

handlers:
- url: /.*
  script: index.php

NOTE: I don't think that is the proper way to create microservices. You may better have an app.yaml file with common scaling parameters and a dispatch.yaml file linking the other services. Read more about it here:

  1. Use their variables where needed inside your code. In the advanced template it would be inside environments/prod/common/main-local.php to be used after executing the init bat:
'db' => [
    'class' => 'yii\db\Connection',
    'dsn' => getenv('POSTGRES_DSN'),
    'username' => getenv('POSTGRES_USER'),
    'password' => getenv('POSTGRES_PASSWORD'),
    'charset' => 'utf8',
    'enableSchemaCache' => true,
    'schemaCacheDuration' => 3600,
    'schemaCache' => 'cache',
],

You can also change the entry script to this in case you think you may need to enable debug mode in their server:

defined('YII_DEBUG') or define('YII_DEBUG', getenv('YII_DEBUG') === 'true');
defined('YII_ENV') or define('YII_ENV', getenv('YII_ENV') ?: 'prod');
  1. I created an nginx-app.conf file (same level with yaml files: app root) with the following content:

    location / {
    try_files $uri $uri/ /index.php$is_args$args;
    }

    Which I guess should be auto merged with their nginx configs that you can see here: https://github.com/GoogleCloudPlatform/php-docker/blob/4454562b9be8134d630feded98a5bdb206e686ab/php-base/nginx.conf#L106

  2. See a list of enabled/disabled php extensions here: https://cloud.google.com/appengine/docs/flexible/php/runtime. The ones you need to activate add them to the require section of your package.json file:

    "require": {
    "php": "7.2.*",
    "ext-gd": "*",
    "ext-redis": "*",
    "ext-intl": "*",
    ...
    },

    NOTE: That documentation says you can enable them by adding a php.inifile. Don't do that. It doesn't work with Flex environment. (see https://github.com/GoogleCloudPlatform/php-docs-samples/issues/446) use the composer file instead.

  3. Use scripts in package.json similar to how it was done in Laravel and Symfony tutorials here and here to add Yii related scripts under any event you need like flashing db or like in my first deploy which was:

    "post-install-cmd": [
    "php init --env=Production --overwrite=All",
    "php yii migrate/up --interactive=0"
    ],
  4. Respectively (in my case):

    gcloud app deploy api.yaml
    gcloud app deploy auth.yaml

    There is many options you can also add like manually set version --version=alpha0or --verbosity=info... It will take long the first time and the script will give you the final url. You can also SSH to your instance within GAE interface and do stuff like docker exec -it gaeapp /bin/bash to see your code. I had to go there the first time to manually run the init script as I didn't know about composer post-install-cmd.

tunecino commented 6 years ago

There is also a list of disabled functions here:

exec
passthru
proc_open
proc_close
shell_exec
show_source
symlink
system

These needed could be whitelisted in the Yaml file within a comma separated string. I had to add this to make the yii2-queue extension work by runnig ./yii queue/listen:

runtime_config:
  document_root: api/web
  whitelist_functions: proc_open <--
michaelhunziker commented 1 month ago

@tunecino I just found your posts here about your experiences with yii2 and Google Cloud Platform... It is really helpful!!

Actually we are running Craft CMS. But since it is based on yii2 I thought you might be able to help.

Are you still running yii2 on GCP Appengine Flex? Do you have any reccomendations about the needed resources (manual_scaling, automatic_scaling, cpu, memory)?

We are currently experiencing issues with the image transform queue. After a deployment we do a php craft clear-caches/all. After that the queue has to process thousands of jobs. During this time our site is almost unresponsive.

Right now I'm trying https://plugins.craftcms.com/async-queue as a solution.

Did you have similar issues?

Our app.yaml:

runtime: php
env: flex

manual_scaling:
  instances: 1

resources:
  cpu: 2
  memory_gb: 4
  disk_size_gb: 10

runtime_config:
  operating_system: "ubuntu22"
  runtime_version: "8.3"
  document_root: web
  nginx_conf_include: "nginx-app-PROD.conf"
  nginx_conf_http_include: "nginx-http-PROD.conf"
  whitelist_functions: proc_open

beta_settings:
  cloud_sql_instances: deft-reflection-319018:europe-west3:craft-db-instance

build_env_variables:
  NGINX_SERVES_STATIC_FILES: true

We are running Craft 4.

tunecino commented 1 month ago

@michaelhunziker I remember that I did opt out from Flex and switched to GCP AppEngine Standard instead because with Flex I was paying for a 24h running servers while the Standard one scales down to 0 instances so it costed nothing when there was no traffic.

For Standard I remember starting with this template:

https://github.com/prawee/yii2-gae-api

Which worked for what I needed (RESTful APIs, 3 of them, each as an independent service/app: api, auth & shell for running migrations and console scripts). 2 things I remember I had to change with that template were:

The main idea is to keep everything standalone ready to scale either up or down. So avoid using any hard disk, use bucket for uploaded stuff & services (either from Google like logs or external when needed).

Don't know much about Craft CMS but it is built on top of Yii, and what is good with Yii is that pretty much every class can be extended to make it do stuff in a different manner.

For queued tasks, I simply used their built-in TaskQueue service, it looked something like this in my API code:

use google\appengine\api\taskqueue\PushTask;

$task = new PushTask("/firebase-sync/$id");
$task_name = $task->add('queue-firebase-sync');

The same project, had a queue.yaml file with following content:

# Set the total storage limit for all queues to 120MB
total_storage_limit: 120M

queue:
- name: queue-firebase-sync
  target: 00.shell
  rate: 20/s
  bucket_size: 40
  max_concurrent_requests: 10
  retry_parameters:
    task_retry_limit: 5
    min_backoff_seconds: 10

And in SHELL, which was a different AppEngine service, I had this controller code to execute coming tasks:

public function actionFirebaseSync($id)
 {
        $migration = new \app\commands\FirebaseSyncController('firebase-sync', Yii::$app);
        $output = $migration->runAction('index', [$id, 'interactive' => false]);
        return $this->buffer($output);
 }

 protected function buffer($output)
    {
        fclose(\STDOUT);
        $buffer = ob_get_clean();
        if ($output) {
            if (YII_ENV === 'prod') {
                $fp = fopen("gs://stdout0/stderr-".uniqid(), 'w');
                fwrite($fp, $buffer);
                fclose($fp);
            }
            throw new ServerErrorHttpException($buffer); 
        }
        return $output;
    }

Which pretty much, runs Yii console/commands scripts & logs the output.

I am roughly copying random code from an older version of an app I've worked on a long time ago, so please take it with a grant of salt, thinks may have changed overtime. I hope it helps.