Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,71 @@
> [!IMPORTANT]
> This package is in active development - contributions are welcome!

YOLO helps you deploy high-availability PHP applications on AWS.
YOLO helps you deploy high-availability PHP applications to AWS.

The CLI tool takes care of provisioning and configuring all required resources on AWS, coupled with build and deployment
The CLI tool lives inside your Laravel app in `vendor/bin/yolo`, and takes care of provisioning and configuring all
required resources on
AWS, coupled with build and deployment
commands to deploy applications to production from your local machine or CI pipeline.

YOLO has been battle-tested on apps that serve 2 million requests per day.

## Features

### Autoscaling Worker Groups

YOLO provisions an Application Load Balancer and autoscaling groups (web, queue, scheduler) for each environment.

Each group is self-healing should an instance become unresponsive, and the web group automatically scales up to handle
traffic bursts.

In addition, worker groups can be combined (coming soon) to a single EC2 instance to consolidate small workloads.

### Resource Sharing

YOLO shares various resources between applications to reduce costs.

### Zero-downtime Deployments

YOLO leverages AWS CodeDeploy to perform zero-downtime deployments, which can be triggered from the CLI or via a CI
pipeline.

### Multi-tenancy

Specify tenants in the manifest and YOLO will take care of provisioning resources for each tenant.

### S3

Leverage S3 for storing build artefacts and user data files.

### Octane (experimental)

YOLO supports Laravel Octane for turbocharged PHP applications.

### Video Transcoding

YOLO can provision resources on AWS to simplify video transcoding on AWS using AWS Elemental MediaConvert.

### And Much More...

- Least priviledge permissions with strong segregation across environments and apps
- Seperate commands that run on deployment across worker groups
- Scheduled MySQL backups using `mysqldump`
- Control of build and deploy commands
- Re-use existing VPCs, subnets, internet gateways and more

___

## Disclaimer

YOLO is designed for PHP developers who want to manage AWS using an infrastructure-as-code approach, using plain-old PHP
rather than CloudFormation / Terraform / K8s / Elastic Beanstalk / <some-other-fancy-alternative>.
YOLO is designed for PHP developers who are comfortable managing AWS using an infrastructure-as-code approach.

> [!IMPORTANT]
> While YOLO has been battle-tested on apps serving millions of requests per day, it is not supposed to be a
> set-and-forget solution for busy apps, but rather allows you to proactively manage, grow and adapt your infrastructure
> as requirements change over time.
It is, at it's core, a Symfony CLI app that leverages the AWS SDK, rather than CloudFormation / Terraform / K8s /
Elastic
Beanstalk / <some-other-fancy-alternative>.

While YOLO has underpinned very large, mission-critical production applications, it is not intended to be a set and
forget solution; rather it acts as a control plane that allows you to manage and expand your AWS footprint over time.

It goes without saying, but use YOLO at your own risk.

Expand Down Expand Up @@ -173,7 +222,7 @@ environments:
artefacts-bucket:
cloudfront:
alb:
transcoder: false
mediaconvert: false
autoscaling:
web:
queue:
Expand Down
6 changes: 0 additions & 6 deletions src/Aws.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Aws\CloudWatch\CloudWatchClient;
use Aws\CodeDeploy\CodeDeployClient;
use Aws\AutoScaling\AutoScalingClient;
use Aws\ElasticTranscoder\ElasticTranscoderClient;
use Aws\ElasticLoadBalancingV2\ElasticLoadBalancingV2Client;

class Aws
Expand Down Expand Up @@ -97,11 +96,6 @@ public static function elasticLoadBalancingV2(): ElasticLoadBalancingV2Client
return Helpers::app('elasticLoadBalancingV2');
}

public static function elasticTranscoder(): ElasticTranscoderClient
{
return Helpers::app('elasticTranscoder');
}

public static function iam(): IamClient
{
return Helpers::app('iam');
Expand Down
2 changes: 0 additions & 2 deletions src/AwsResources.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use Codinglabs\Yolo\Concerns\UsesCloudWatch;
use Codinglabs\Yolo\Concerns\UsesCodeDeploy;
use Codinglabs\Yolo\Concerns\UsesAutoscaling;
use Codinglabs\Yolo\Concerns\UsesElasticTranscoder;
use Codinglabs\Yolo\Concerns\UsesCertificateManager;
use Codinglabs\Yolo\Concerns\UsesElasticLoadBalancingV2;

Expand All @@ -25,7 +24,6 @@ class AwsResources
use UsesCodeDeploy;
use UsesEc2;
use UsesElasticLoadBalancingV2;
use UsesElasticTranscoder;
use UsesIam;
use UsesRds;
use UsesRoute53;
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/DeployCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class DeployCommand extends SteppedCommand
{
protected array $steps = [
Steps\Ensures\EnsureTranscoderExistsStep::class,
Steps\Ensures\EnsureIamRolesExistStep::class,
Steps\Ensures\EnsureHostedZonesExistStep::class,
Steps\Ensures\EnsureMultitenancyHostedZonesExistStep::class,
Steps\Ensures\EnsureEnvIsConfiguredCorrectlyStep::class,
Expand Down
4 changes: 0 additions & 4 deletions src/Commands/SyncComputeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ class SyncComputeCommand extends SteppedCommand
// // multitenancy
// Steps\Compute\SyncMultitenancyListenerOnPort443Step::class,
// Steps\Compute\AttachMultitenancySslCertificateToLoadBalancerListenerStep::class,

// transcoder
Steps\Compute\SyncElasticTranscoderPipelineStep::class,
Steps\Compute\SyncElasticTranscoderPresetStep::class,
];

protected function configure(): void
Expand Down
12 changes: 7 additions & 5 deletions src/Commands/SyncIamCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
class SyncIamCommand extends SteppedCommand
{
protected array $steps = [
Steps\Iam\SyncRoleStep::class,
Steps\Iam\SyncRolePolicyStep::class,
Steps\Iam\AttachRolePoliciesStep::class,
Steps\Iam\SyncInstanceProfileStep::class,
Steps\Iam\AttachRoleToInstanceProfileStep::class,
Steps\Iam\SyncEc2RoleStep::class,
Steps\Iam\SyncEc2RolePolicyStep::class,
Steps\Iam\AttachEc2RolePoliciesStep::class,
Steps\Iam\SyncEc2InstanceProfileStep::class,
Steps\Iam\AttachEc2RoleToInstanceProfileStep::class,
Steps\Iam\SyncMediaConvertRoleStep::class,
Steps\Iam\AttachMediaConvertRolePoliciesStep::class,
];

protected function configure(): void
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/SyncNetworkCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class SyncNetworkCommand extends SteppedCommand
Steps\Network\SyncRdsSecurityGroupStep::class,

// sns
Steps\Network\SyncSnsTopicStep::class,
Steps\Network\SyncSnsAlarmTopicStep::class,

// ssh
Steps\Network\SyncKeyPairStep::class,
Expand Down
21 changes: 13 additions & 8 deletions src/Concerns/RegistersAws.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
use Codinglabs\Yolo\Enums\ServerGroup;
use Aws\Credentials\CredentialProvider;
use GuzzleHttp\Exception\ConnectException;
use Aws\ElasticTranscoder\ElasticTranscoderClient;
use Codinglabs\Yolo\Exceptions\IntegrityCheckException;
use Aws\ElasticLoadBalancingV2\ElasticLoadBalancingV2Client;

trait RegistersAws
Expand All @@ -43,7 +43,6 @@ protected function registerAwsServices(): void
Helpers::app()->singleton('cloudWatch', fn () => new CloudWatchClient($arguments));
Helpers::app()->singleton('ec2', fn () => new Ec2Client($arguments));
Helpers::app()->singleton('elasticLoadBalancingV2', fn () => new ElasticLoadBalancingV2Client($arguments));
Helpers::app()->singleton('elasticTranscoder', fn () => new ElasticTranscoderClient($arguments));
Helpers::app()->singleton('iam', fn () => new IamClient($arguments));
Helpers::app()->singleton('rds', fn () => new RdsClient($arguments));
Helpers::app()->singleton('route53', fn () => new Route53Client($arguments));
Expand All @@ -66,14 +65,20 @@ protected static function awsCredentials(): callable|array|null
return null;
}

// in CI (GitHub Actions) we use environment variables, otherwise we
// are using a local env value to point to the correct AWS profile.
return static::detectCiEnvironment()
? [
// in CI (GitHub Actions) we use environment variables
if (static::detectCiEnvironment()) {
return [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
]
: CredentialProvider::ini(Helpers::keyedEnv('AWS_PROFILE'));
];
}

// otherwise we are using a local env value to point to the correct AWS profile.
if (in_array(Helpers::keyedEnv('AWS_PROFILE'), ['', null, 'default'])) {
throw new IntegrityCheckException(sprintf('Using the default AWS profile in your credentials file is risky. Name your profile to something specific and update %s in your .env file before proceeding.', Helpers::keyedEnvName('AWS_PROFILE')));
}

return CredentialProvider::ini(Helpers::keyedEnv('AWS_PROFILE'));
}

protected static function detectLocalEnvironment(): bool
Expand Down
5 changes: 2 additions & 3 deletions src/Concerns/RunsSteppedCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ protected function handleSteps(string $environment): int

// yellow
StepResult::SKIPPED => '<fg=yellow>SKIPPED</>',
StepResult::WOULD_SKIP => '<fg=yellow>WOULD SKIP</>',
StepResult::CUSTOM_MANAGED => '<fg=yellow>CUSTOM MANAGED</>',
StepResult::WOULD_CREATE => '<fg=yellow>WOULD CREATE</>',
StepResult::WOULD_SYNC => '<fg=yellow>WOULD SYNC</>',
Expand Down Expand Up @@ -152,7 +151,7 @@ protected static function normaliseStep(Step $step, $pad = false, $bold = false,
->when($bold && ! $step instanceof ExecutesTenantStep, fn (Stringable $string) => $string->wrap(before: '<options=bold>', after: '</>'))
};

return $name->limit(50)
->when($pad, fn (Stringable $string) => $string->padRight(50));
return $name->limit(70)
->when($pad, fn (Stringable $string) => $string->padRight(70));
}
}
40 changes: 0 additions & 40 deletions src/Concerns/UsesElasticTranscoder.php

This file was deleted.

46 changes: 33 additions & 13 deletions src/Concerns/UsesIam.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static function ec2Role(): array
throw new ResourceDoesNotExistException("Could not find IAM role with name $name");
}

public static function instanceProfile(): array
public static function ec2InstanceProfile(): array
{
$name = Helpers::keyedResourceName(Iam::INSTANCE_PROFILE, exclusive: false);
$instanceProfiles = Aws::iam()->listInstanceProfiles();
Expand All @@ -53,7 +53,7 @@ public static function instanceProfile(): array
throw new ResourceDoesNotExistException("Could not find IAM instance profile with name $name");
}

public static function policyDocument(): array
public static function ec2PolicyDocument(): array
{
return [
'Version' => '2012-10-17',
Expand All @@ -69,7 +69,6 @@ public static function policyDocument(): array
'elasticloadbalancing:DescribeTargetGroups',
'ec2:DescribeTags',
'elasticloadbalancing:DescribeLoadBalancers',
'elastictranscoder:ListPipelines',
'sqs:DeleteMessage',
'sqs:GetQueueUrl',
'sqs:ChangeMessageVisibility',
Expand All @@ -80,15 +79,6 @@ public static function policyDocument(): array
'sqs:ListQueues',
],
],
[
'Effect' => 'Allow',
'Resource' => [
'arn:aws:iam::*:role/Elastic_Transcoder_Default_Role',
],
'Action' => [
'iam:PassRole',
],
],
[
'Effect' => 'Allow',
'Resource' => '*',
Expand Down Expand Up @@ -137,7 +127,7 @@ public static function policyDocument(): array
];
}

public static function rolePolicyDocument(): array
public static function ec2RolePolicyDocument(): array
{
return [
'Version' => '2012-10-17',
Expand All @@ -152,4 +142,34 @@ public static function rolePolicyDocument(): array
],
];
}

public static function mediaConvertRole(): array
{
$name = Helpers::keyedResourceName(Iam::MEDIA_CONVERT_ROLE);
$roles = Aws::iam()->listRoles();

foreach ($roles['Roles'] as $role) {
if ($role['RoleName'] === $name) {
return $role;
}
}

throw new ResourceDoesNotExistException("Could not find IAM role with name $name");
}

public static function mediaConvertPolicyDocument(): array
{
return [
'Version' => '2012-10-17',
'Statement' => [
[
'Effect' => 'Allow',
'Principal' => [
'Service' => 'mediaconvert.amazonaws.com',
],
'Action' => 'sts:AssumeRole',
],
],
];
}
}
12 changes: 8 additions & 4 deletions src/Concerns/UsesSns.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@

trait UsesSns
{
public static function topic(): array
public static function alarmTopic(): array
{
return static::topicByName(Helpers::keyedResourceName(exclusive: false));
}

public static function topicByName(string $name): array
{
$topicName = Helpers::keyedResourceName(exclusive: false);
$topics = Aws::sns()->listTopics();

foreach ($topics['Topics'] as $topic) {
if (Str::afterLast($topic['TopicArn'], ':') === $topicName) {
if (Str::afterLast($topic['TopicArn'], ':') === $name) {
return $topic;
}
}

throw new ResourceDoesNotExistException("Could not find SNS topic with name $topicName");
throw new ResourceDoesNotExistException("Could not find SNS topic with name $name");
}
}
1 change: 1 addition & 0 deletions src/Enums/Iam.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
enum Iam: string
{
case INSTANCE_PROFILE = 'instance-profile';
case MEDIA_CONVERT_ROLE = 'mediaconvert-role';
}
Loading