brefphp / bref

Serverless PHP on AWS Lambda
https://bref.sh
MIT License
3.11k stars 367 forks source link

Run a console command as a cron #339

Closed steefaan closed 3 years ago

steefaan commented 5 years ago

First of all thanks for your great work on bref and thanks for sharing the code. Everything is working smoothly. I made it working to have a symfony console command deployed to AWS Lambda I can execute.

To execute my command I need to run vendor/bin/bref --region=eu-central-1 cli symfony-console -- app:update-inventory-quantity. This works great but I'm struggling a lot to automate this command to run it every 15 minutes. After some googling I figured out that CloudWatch provides what I'm searching for but I stuck on the configuration part. I'm even not sure if my question belongs to CloudWatch (so out of bref's scope) or bref itself. Due to my "specific" use case (lambda, php, bref, symfony) I couldn't find anything on Google what answers my question satisfying.

The simple question is how to pass parameters to CloudWatch? Although I'm able to configure CloudWatch to execute symfony-console itself I need a possibility to pass the command I want to execute (app:update-inventory-quantity).

That's my template.yaml:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
    Function:
        Environment:
            Variables:
                APP_ENV: prod

Resources:
    Console:
        Type: AWS::Serverless::Function
        Properties:
            FunctionName: 'symfony-console'
            CodeUri: .
            Handler: bin/console
            Timeout: 120 # in seconds
            Runtime: provided
            Layers:
                - 'arn:aws:lambda:eu-central-1:209497400698:layer:php-73:6' # PHP
                - 'arn:aws:lambda:eu-central-1:209497400698:layer:console:6' # The "console" layer
deleugpn commented 5 years ago

Here's what you can try, I'm not sure if it's going to work as I have no experience with Symfony and this solution is something you made me think right now.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
    Function:
        Environment:
            Variables:
                APP_ENV: prod

Resources:
    Console:
        Type: AWS::Serverless::Function
        Properties:
            FunctionName: 'symfony-console'
            CodeUri: .
            Handler: bin/console
            Timeout: 120 # in seconds
            Runtime: provided
            Layers:
                - 'arn:aws:lambda:eu-central-1:209497400698:layer:php-73:6' # PHP
                - 'arn:aws:lambda:eu-central-1:209497400698:layer:console:6' # The "console" layer
            Events:
                UpdateInventoryQuantity:
                    Type: Schedule
                    Properties:
                        Schedule: rate(15 minutes)
                        Input: '{"cli": "app:update-inventory-quantity"}'

If this works, please let me know as I might be able to take advantage of this on Laravel artisan as well.

steefaan commented 5 years ago

@deleugpn Awesome! It works as it should!

mnapoli commented 5 years ago

Let's keep this issue open, it seems like a common use case and I think it's worth considering if:

deleugpn commented 5 years ago

I think this is pretty good and extend to more than just cron. What this issue made me realize is that the bref cli (a tool which I don't use) makes usage of something within the layer that allows you to pass a parameter called cli to execute commands.

The way I 'hacked' this on Laravel was by introducing an environment variable and changing the artisan file to accommodate it.

$command = $_ENV['ARTISAN_COMMAND'] ?? null;

$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput($command ? ['lambda', $command] : null),
    new Symfony\Component\Console\Output\ConsoleOutput
);

Now that I know that the layer is capable of intercepting the argument cli, I can restore the artisanto it's original state and simply change the Lambda invocation to provide {"cli": "my:command"}

mnapoli commented 5 years ago

A simpler alternative could be to change Bref so that this is possible:

          Handler: 'bin/console app:update-inventory-quantity'

But again: is going through the CLI console application the best solution? Should we encourage instead to write functions (as in FaaS)? For example:

          Handler: cron/update-inventory-quantity.php

To me the answer is not obvious: I get that the console is practical and familiar, but it is an unnecessary overhead (not just in performance but in simplicity).

deleugpn commented 5 years ago

AWS CloudFormation does not allow tab, whitespace or breakline on the Handler attribute. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#cfn-lambda-function-handler

As for writing a dedicated file for each process goes against framework utilities. Bootstrapping Laravel through Artisan adds an overhead that I consciously don't mind since I get IoC Container, ORM, Cache, Validation, etc all autowired automatically. Furthermore, if you think about framework-provided features such as migrate (Doctrine or Laravel), what would be the point for me to write and maintain a php file with the sole purpose of making my Lambda function execute something that the framework already provides?

The simpler approach you described is already possible and can be encouraged if you want, but having the alternative of running Symfony CLI (or similars) through bref's magic attribute cli is incredibly powerful and useful.

mnapoli commented 5 years ago

AWS CloudFormation does not allow tab, whitespace or breakline on the Handler attribute.

Good point, that specific syntax isn't possible then.

As for writing a dedicated file for each process goes against framework utilities. Bootstrapping Laravel through Artisan adds an overhead that I consciously don't mind since I get IoC Container, ORM, Cache, Validation, etc

Right, but what I suggest doesn't mean you don't get that.

I.e. in our example the cron/update-inventory-quantity.php could be something like this (Symfony example):

<?php
require __DIR__.'/../vendor/autoload.php';

lambda(function ($event) {
    $kernel = new Kernel('prod', false);
    $inventory = $kernel->getContainer()->get(InventoryService::class);

    $inventory->updateQuantity();
});

And it wouldn't be crazy to imagine frameworks provide helpers to do something like this (e.g. with PHP-DI it's pretty easy to do):

<?php
require __DIR__.'/../vendor/autoload.php';

$kernel = new Kernel('prod', false);

lambda($kernel->autowire(function (InventoryService $inventory) {
    $inventory->updateQuantity();
}));

but this is another question so this isn't really the point here.

My point is: AWS Lambda is about a paradigm of simple functions. The CLI abstraction (with parameters and arguments) is helpful when those parameters are dynamic, here we are talking about hardcoding a specific command line. The function paradigm seem to make more sense. And the matter of bootstrapping frameworks is a good point, but at first look it seems easily doable.

patrickbussmann commented 4 years ago

Maybe another solution could be to make the input as plain command instead of a JSON string.

Like:

        events:
            - schedule:
                rate: rate(1 minute)
                input:
                    cli: doctrine:migrate --force

Instead of doing:

        events:
            - schedule:
                rate: rate(1 minute)
                input: '{"cli": "doctrine:migrate --force"}'

Or as simple as it could be:

        events:
            - schedule:
                rate: rate(1 minute)
                input: 'doctrine:migrate --force'

😉

mnapoli commented 4 years ago

@patrickbussmann you make a very good point 🤔

The '{"cli": "doctrine:migrate --force"}' JSON structure used to exist for Bref 0.2 that used to mix event types for the same handler. Now, we don't really need this anymore (AFAICT).

We could switch the console runtime to a simple string and be done with it. That would make cron much easier, like you suggest:

        events:
            - schedule:
                rate: rate(1 minute)
                input: 'doctrine:migrate --force'

Heck, that could even been invokable easily via serverless invoke:

serverless invoke -f console -d 'doctrine:migrate --force'

I love that!

mnapoli commented 4 years ago

I have opened #615 to implement that.

Maxwell2022 commented 3 years ago

should this issue be closed now?

mnapoli commented 3 years ago

@Maxwell2022 absolutely, thank you for the heads up!

mnapoli commented 3 years ago

For reference, here is the documentation: https://bref.sh/docs/cron.html