diff --git a/README.md b/README.md index 17ec43d..75c33a7 100644 --- a/README.md +++ b/README.md @@ -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 / . +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 / . + +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. @@ -173,7 +222,7 @@ environments: artefacts-bucket: cloudfront: alb: - transcoder: false + mediaconvert: false autoscaling: web: queue: diff --git a/src/Aws.php b/src/Aws.php index 80c42e0..ee87dcd 100644 --- a/src/Aws.php +++ b/src/Aws.php @@ -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 @@ -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'); diff --git a/src/AwsResources.php b/src/AwsResources.php index d7a7c71..025b0c0 100644 --- a/src/AwsResources.php +++ b/src/AwsResources.php @@ -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; @@ -25,7 +24,6 @@ class AwsResources use UsesCodeDeploy; use UsesEc2; use UsesElasticLoadBalancingV2; - use UsesElasticTranscoder; use UsesIam; use UsesRds; use UsesRoute53; diff --git a/src/Commands/DeployCommand.php b/src/Commands/DeployCommand.php index cbbc1fe..a3f4b26 100644 --- a/src/Commands/DeployCommand.php +++ b/src/Commands/DeployCommand.php @@ -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, diff --git a/src/Commands/SyncComputeCommand.php b/src/Commands/SyncComputeCommand.php index 90fcac8..a5f5705 100644 --- a/src/Commands/SyncComputeCommand.php +++ b/src/Commands/SyncComputeCommand.php @@ -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 diff --git a/src/Commands/SyncIamCommand.php b/src/Commands/SyncIamCommand.php index d266bc8..fed2674 100644 --- a/src/Commands/SyncIamCommand.php +++ b/src/Commands/SyncIamCommand.php @@ -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 diff --git a/src/Commands/SyncNetworkCommand.php b/src/Commands/SyncNetworkCommand.php index 6760d5a..c87edc5 100644 --- a/src/Commands/SyncNetworkCommand.php +++ b/src/Commands/SyncNetworkCommand.php @@ -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, diff --git a/src/Concerns/RegistersAws.php b/src/Concerns/RegistersAws.php index d9182d2..0056c9e 100644 --- a/src/Concerns/RegistersAws.php +++ b/src/Concerns/RegistersAws.php @@ -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 @@ -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)); @@ -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 diff --git a/src/Concerns/RunsSteppedCommands.php b/src/Concerns/RunsSteppedCommands.php index 49248b2..c387ced 100644 --- a/src/Concerns/RunsSteppedCommands.php +++ b/src/Concerns/RunsSteppedCommands.php @@ -71,7 +71,6 @@ protected function handleSteps(string $environment): int // yellow StepResult::SKIPPED => 'SKIPPED', - StepResult::WOULD_SKIP => 'WOULD SKIP', StepResult::CUSTOM_MANAGED => 'CUSTOM MANAGED', StepResult::WOULD_CREATE => 'WOULD CREATE', StepResult::WOULD_SYNC => 'WOULD SYNC', @@ -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: '', 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)); } } diff --git a/src/Concerns/UsesElasticTranscoder.php b/src/Concerns/UsesElasticTranscoder.php deleted file mode 100644 index c34b88d..0000000 --- a/src/Concerns/UsesElasticTranscoder.php +++ /dev/null @@ -1,40 +0,0 @@ -listPipelines(); - - foreach ($pipelines['Pipelines'] as $pipeline) { - if ($pipeline['Name'] === $name) { - return $pipeline; - } - } - - throw ResourceDoesNotExistException::make("Could not find Elastic Transcoder pipeline with name $name") - ->suggest('sync:compute'); - } - - public static function elasticTranscoderPreset(): array - { - $name = Helpers::keyedResourceName(); - $presets = Aws::elasticTranscoder()->listPresets(); - - foreach ($presets['Presets'] as $preset) { - if ($preset['Name'] === $name) { - return $preset; - } - } - - throw ResourceDoesNotExistException::make("Could not find Elastic Transcoder preset with name $name") - ->suggest('sync:compute'); - } -} diff --git a/src/Concerns/UsesIam.php b/src/Concerns/UsesIam.php index 6d08dee..d6723e6 100644 --- a/src/Concerns/UsesIam.php +++ b/src/Concerns/UsesIam.php @@ -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(); @@ -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', @@ -69,7 +69,6 @@ public static function policyDocument(): array 'elasticloadbalancing:DescribeTargetGroups', 'ec2:DescribeTags', 'elasticloadbalancing:DescribeLoadBalancers', - 'elastictranscoder:ListPipelines', 'sqs:DeleteMessage', 'sqs:GetQueueUrl', 'sqs:ChangeMessageVisibility', @@ -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' => '*', @@ -137,7 +127,7 @@ public static function policyDocument(): array ]; } - public static function rolePolicyDocument(): array + public static function ec2RolePolicyDocument(): array { return [ 'Version' => '2012-10-17', @@ -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', + ], + ], + ]; + } } diff --git a/src/Concerns/UsesSns.php b/src/Concerns/UsesSns.php index 623fb58..3119d7d 100644 --- a/src/Concerns/UsesSns.php +++ b/src/Concerns/UsesSns.php @@ -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"); } } diff --git a/src/Enums/Iam.php b/src/Enums/Iam.php index 85c320e..45c652a 100644 --- a/src/Enums/Iam.php +++ b/src/Enums/Iam.php @@ -5,4 +5,5 @@ enum Iam: string { case INSTANCE_PROFILE = 'instance-profile'; + case MEDIA_CONVERT_ROLE = 'mediaconvert-role'; } diff --git a/src/Enums/StepResult.php b/src/Enums/StepResult.php index 3292b22..c8dfeae 100644 --- a/src/Enums/StepResult.php +++ b/src/Enums/StepResult.php @@ -15,6 +15,5 @@ enum StepResult case CUSTOM_MANAGED; case TIMEOUT; case SKIPPED; - case WOULD_SKIP; case MANIFEST_INVALID; } diff --git a/src/Steps/Build/ConfigureEnvAndVersionStep.php b/src/Steps/Build/ConfigureEnvAndVersionStep.php index 7d1f543..663c910 100644 --- a/src/Steps/Build/ConfigureEnvAndVersionStep.php +++ b/src/Steps/Build/ConfigureEnvAndVersionStep.php @@ -2,8 +2,11 @@ namespace Codinglabs\Yolo\Steps\Build; +use Codinglabs\Yolo\Aws; use Codinglabs\Yolo\Paths; use Illuminate\Support\Arr; +use Codinglabs\Yolo\Helpers; +use Codinglabs\Yolo\Enums\Iam; use Codinglabs\Yolo\Contracts\Step; use Illuminate\Filesystem\Filesystem; @@ -25,9 +28,26 @@ public function __invoke(array $options): void $this->filesystem->append( Paths::build(".env.$this->environment"), - PHP_EOL . - 'APP_VERSION=' . $appVersion . PHP_EOL . - 'ASSET_URL=' . Paths::assetUrl($appVersion) . PHP_EOL + $this->generateValues([ + 'APP_VERSION' => $appVersion, + 'ASSET_URL' => Paths::assetUrl($appVersion), + 'AWS_MEDIACONVERT_ROLE_ID' => sprintf( + 'arn:aws:iam::%s:role/service-role/%s', + Aws::accountId(), + Helpers::keyedResourceName(Iam::MEDIA_CONVERT_ROLE), + ), + ]) ); } + + protected function generateValues(array $values): string + { + $result = PHP_EOL . '# YOLO generated values' . PHP_EOL; + + foreach ($values as $key => $value) { + $result .= "$key=$value" . PHP_EOL; + } + + return $result; + } } diff --git a/src/Steps/Compute/SyncElasticTranscoderPipelineStep.php b/src/Steps/Compute/SyncElasticTranscoderPipelineStep.php deleted file mode 100644 index 1706100..0000000 --- a/src/Steps/Compute/SyncElasticTranscoderPipelineStep.php +++ /dev/null @@ -1,51 +0,0 @@ -createPipeline([ - 'Name' => Helpers::keyedResourceName(), - 'InputBucket' => Paths::s3AppBucket(), - 'OutputBucket' => Paths::s3AppBucket(), - 'Role' => 'arn:aws:iam::' . Aws::accountId() . ':role/Elastic_Transcoder_Default_Role', - // note: Elastic Transcoder does not appear to support tagging - // 'TagSpecifications' => [ - // [ - // 'ResourceType' => 'pipeline', - // ...Aws::tags([ - // 'Name' => Helpers::keyedResourceName(), - // ]), - // ], - // ], - ]); - - return StepResult::CREATED; - } - - return StepResult::WOULD_CREATE; - } - } -} diff --git a/src/Steps/Compute/SyncElasticTranscoderPresetStep.php b/src/Steps/Compute/SyncElasticTranscoderPresetStep.php deleted file mode 100644 index 05529cb..0000000 --- a/src/Steps/Compute/SyncElasticTranscoderPresetStep.php +++ /dev/null @@ -1,86 +0,0 @@ -createPreset([ - 'Name' => Helpers::keyedResourceName(), - 'Description' => Helpers::environment() . ' transcoding preset', - 'Container' => 'mp4', - 'Audio' => [ - 'Codec' => 'AAC', - 'SampleRate' => '44100', - 'BitRate' => '160', - 'Channels' => '2', - 'CodecOptions' => [ - 'Profile' => 'AAC-LC', - ], - ], - 'Video' => [ - 'Codec' => 'H.264', - 'CodecOptions' => [ - 'ColorSpaceConversionMode' => 'None', - 'InterlacedMode' => 'Progressive', - 'Level' => '3.1', - 'MaxReferenceFrames' => '3', - 'Profile' => 'main', - ], - - 'KeyframesMaxDist' => '90', - 'FixedGOP' => 'false', - 'BitRate' => '2200', - 'FrameRate' => '30', - 'MaxWidth' => '1280', - 'MaxHeight' => '720', - 'DisplayAspectRatio' => 'auto', - 'SizingPolicy' => 'ShrinkToFit', - 'PaddingPolicy' => 'NoPad', - ], - 'Thumbnails' => [ - 'Format' => 'png', - 'Interval' => '10', - 'MaxWidth' => '1280', - 'MaxHeight' => '720', - 'SizingPolicy' => 'ShrinkToFit', - 'PaddingPolicy' => 'NoPad', - ], - // note: Elastic Transcoder does not appear to support tagging - // 'TagSpecifications' => [ - // [ - // 'ResourceType' => 'preset', - // ...Aws::tags([ - // 'Name' => Helpers::keyedResourceName(), - // ]), - // ], - // ], - ]); - - return StepResult::CREATED; - } - - return StepResult::WOULD_CREATE; - } - } -} diff --git a/src/Steps/Ensures/EnsureEnvIsConfiguredCorrectlyStep.php b/src/Steps/Ensures/EnsureEnvIsConfiguredCorrectlyStep.php index dcb15d3..0d26f72 100644 --- a/src/Steps/Ensures/EnsureEnvIsConfiguredCorrectlyStep.php +++ b/src/Steps/Ensures/EnsureEnvIsConfiguredCorrectlyStep.php @@ -3,14 +3,15 @@ namespace Codinglabs\Yolo\Steps\Ensures; use Dotenv\Dotenv; +use Codinglabs\Yolo\Aws; use Codinglabs\Yolo\Paths; +use Codinglabs\Yolo\Helpers; use Codinglabs\Yolo\Manifest; -use Codinglabs\Yolo\AwsResources; +use Codinglabs\Yolo\Enums\Iam; use Codinglabs\Yolo\Contracts\Step; use Codinglabs\Yolo\Enums\StepResult; use Illuminate\Filesystem\Filesystem; use Codinglabs\Yolo\Exceptions\IntegrityCheckException; -use Illuminate\Contracts\Filesystem\FileNotFoundException; class EnsureEnvIsConfiguredCorrectlyStep implements Step { @@ -18,30 +19,56 @@ public function __construct(protected string $environment, protected $filesystem public function __invoke(): StepResult { - if (Manifest::get('aws.transcoder')) { - $this->checkTranscoderConfiguration(); + $dotenv = Dotenv::parse($this->filesystem->get(Paths::build('.env'))); + + $this->checkAppVersion($dotenv); + $this->checkAssetUrl($dotenv); + + if (Manifest::get('aws.mediaconvert')) { + $this->checkMediaConvertConfiguration($dotenv); } return StepResult::SYNCED; } - /** - * @throws IntegrityCheckException - * @throws FileNotFoundException - */ - protected function checkTranscoderConfiguration(): void + protected function checkAppVersion(array $dotenv): void { - $elasticTranscoderPipeline = AwsResources::elasticTranscoderPipeline(); - $elasticTranscoderPreset = AwsResources::elasticTranscoderPreset(); + if (empty($dotenv['APP_VERSION'])) { + $this->throwException($dotenv, 'APP_VERSION'); + } + } - $dotenv = Dotenv::parse($this->filesystem->get(Paths::build('.env'))); + protected function checkAssetUrl(array $dotenv): void + { + $expected = Paths::assetUrl($dotenv['APP_VERSION']); - if ($dotenv['AWS_TRANSCODER_PIPELINE'] !== $elasticTranscoderPipeline['Id']) { - throw new IntegrityCheckException("Transcoder pipeline ID {$dotenv['AWS_TRANSCODER_PIPELINE']} does not match {$elasticTranscoderPipeline['Id']}"); + if ($dotenv['ASSET_URL'] !== $expected) { + $this->throwException($dotenv, 'ASSET_URL', $expected); } + } - if ($dotenv['AWS_TRANSCODER_PRESET'] != $elasticTranscoderPreset['Id']) { - throw new IntegrityCheckException("Transcoder preset ID {$dotenv['AWS_TRANSCODER_PRESET']} does not match {$elasticTranscoderPreset['Id']}"); + protected function checkMediaConvertConfiguration(array $dotenv): void + { + $expected = sprintf( + 'arn:aws:iam::%s:role/service-role/%s', + Aws::accountId(), + Helpers::keyedResourceName(Iam::MEDIA_CONVERT_ROLE), + ); + + if ($dotenv['AWS_MEDIACONVERT_ROLE_ID'] !== $expected) { + $this->throwException($dotenv, 'AWS_MEDIACONVERT_ROLE_ID', $expected); + } + } + + /** + * @throws IntegrityCheckException + */ + protected function throwException(array $dotenv, string $key, ?string $expected = null): never + { + if ($expected === null) { + throw new IntegrityCheckException("$key {$dotenv[$key]} is not set"); } + + throw new IntegrityCheckException("$key {$dotenv[$key]} does not match $expected"); } } diff --git a/src/Steps/Ensures/EnsureTranscoderExistsStep.php b/src/Steps/Ensures/EnsureIamRolesExistStep.php similarity index 56% rename from src/Steps/Ensures/EnsureTranscoderExistsStep.php rename to src/Steps/Ensures/EnsureIamRolesExistStep.php index da2d840..0538d0b 100644 --- a/src/Steps/Ensures/EnsureTranscoderExistsStep.php +++ b/src/Steps/Ensures/EnsureIamRolesExistStep.php @@ -8,18 +8,17 @@ use Codinglabs\Yolo\Enums\StepResult; use Codinglabs\Yolo\Concerns\EnsuresResourcesExist; -class EnsureTranscoderExistsStep implements Step +class EnsureIamRolesExistStep implements Step { use EnsuresResourcesExist; public function __invoke(): StepResult { - if (Manifest::get('aws.transcoder') === null) { - return StepResult::SKIPPED; - } + $this->ensure(fn () => AwsResources::ec2Role()); - $this->ensure(fn () => AwsResources::elasticTranscoderPipeline()); - $this->ensure(fn () => AwsResources::elasticTranscoderPreset()); + if (Manifest::get('aws.mediaconvert')) { + $this->ensure(fn () => AwsResources::mediaConvertRole()); + } return StepResult::SUCCESS; } diff --git a/src/Steps/Iam/AttachRolePoliciesStep.php b/src/Steps/Iam/AttachEc2RolePoliciesStep.php similarity index 90% rename from src/Steps/Iam/AttachRolePoliciesStep.php rename to src/Steps/Iam/AttachEc2RolePoliciesStep.php index f0ab707..2d991f8 100644 --- a/src/Steps/Iam/AttachRolePoliciesStep.php +++ b/src/Steps/Iam/AttachEc2RolePoliciesStep.php @@ -8,11 +8,11 @@ use Codinglabs\Yolo\Contracts\Step; use Codinglabs\Yolo\Enums\StepResult; -class AttachRolePoliciesStep implements Step +class AttachEc2RolePoliciesStep implements Step { protected array $managedPolicies = [ - 'arn:aws:iam::aws:policy/AmazonElasticTranscoder_JobsSubmitter', 'arn:aws:iam::aws:policy/AmazonRekognitionReadOnlyAccess', + 'arn:aws:iam::aws:policy/AWSElementalMediaConvertFullAccess', 'arn:aws:iam::aws:policy/IVSFullAccess', ]; diff --git a/src/Steps/Iam/AttachRoleToInstanceProfileStep.php b/src/Steps/Iam/AttachEc2RoleToInstanceProfileStep.php similarity index 90% rename from src/Steps/Iam/AttachRoleToInstanceProfileStep.php rename to src/Steps/Iam/AttachEc2RoleToInstanceProfileStep.php index df358b1..a83f053 100644 --- a/src/Steps/Iam/AttachRoleToInstanceProfileStep.php +++ b/src/Steps/Iam/AttachEc2RoleToInstanceProfileStep.php @@ -10,12 +10,12 @@ use Codinglabs\Yolo\Enums\StepResult; use Codinglabs\Yolo\Exceptions\ResourceDoesNotExistException; -class AttachRoleToInstanceProfileStep implements Step +class AttachEc2RoleToInstanceProfileStep implements Step { public function __invoke(array $options): StepResult { try { - $instanceProfile = AwsResources::instanceProfile(); + $instanceProfile = AwsResources::ec2InstanceProfile(); $attached = ! empty($instanceProfile['Roles']) && $instanceProfile['Roles'][0]['RoleName'] === Helpers::keyedResourceName(exclusive: false); if (! Arr::get($options, 'dry-run')) { diff --git a/src/Steps/Iam/AttachMediaConvertRolePoliciesStep.php b/src/Steps/Iam/AttachMediaConvertRolePoliciesStep.php new file mode 100644 index 0000000..94160ab --- /dev/null +++ b/src/Steps/Iam/AttachMediaConvertRolePoliciesStep.php @@ -0,0 +1,40 @@ +managedPolicies as $policyArn) { + Aws::iam()->attachRolePolicy([ + 'RoleName' => $role['RoleName'], + 'PolicyArn' => $policyArn, + ]); + } + + return StepResult::SYNCED; + } + + return StepResult::WOULD_SYNC; + } +} diff --git a/src/Steps/Iam/SyncInstanceProfileStep.php b/src/Steps/Iam/SyncEc2InstanceProfileStep.php similarity index 92% rename from src/Steps/Iam/SyncInstanceProfileStep.php rename to src/Steps/Iam/SyncEc2InstanceProfileStep.php index a71c581..bce9e28 100644 --- a/src/Steps/Iam/SyncInstanceProfileStep.php +++ b/src/Steps/Iam/SyncEc2InstanceProfileStep.php @@ -11,14 +11,14 @@ use Codinglabs\Yolo\Enums\StepResult; use Codinglabs\Yolo\Exceptions\ResourceDoesNotExistException; -class SyncInstanceProfileStep implements Step +class SyncEc2InstanceProfileStep implements Step { public function __invoke(array $options): StepResult { $name = Helpers::keyedResourceName(Iam::INSTANCE_PROFILE, exclusive: false); try { - AwsResources::instanceProfile(); + AwsResources::ec2InstanceProfile(); if (! Arr::get($options, 'dry-run')) { Aws::iam()->tagInstanceProfile([ diff --git a/src/Steps/Iam/SyncRolePolicyStep.php b/src/Steps/Iam/SyncEc2RolePolicyStep.php similarity index 92% rename from src/Steps/Iam/SyncRolePolicyStep.php rename to src/Steps/Iam/SyncEc2RolePolicyStep.php index 249ce50..2fbc3d8 100644 --- a/src/Steps/Iam/SyncRolePolicyStep.php +++ b/src/Steps/Iam/SyncEc2RolePolicyStep.php @@ -10,7 +10,7 @@ use Codinglabs\Yolo\Enums\StepResult; use Codinglabs\Yolo\Exceptions\ResourceDoesNotExistException; -class SyncRolePolicyStep implements Step +class SyncEc2RolePolicyStep implements Step { public function __invoke(array $options): StepResult { @@ -27,13 +27,13 @@ public function __invoke(array $options): StepResult associative: true ); - $hasDifferences = Helpers::payloadHasDifferences($currentPolicyDocument, AwsResources::policyDocument()); + $hasDifferences = Helpers::payloadHasDifferences($currentPolicyDocument, AwsResources::ec2PolicyDocument()); if (! Arr::get($options, 'dry-run')) { if ($hasDifferences) { Aws::iam()->createPolicyVersion([ 'PolicyArn' => $policy['Arn'], - 'PolicyDocument' => json_encode(AwsResources::policyDocument()), + 'PolicyDocument' => json_encode(AwsResources::ec2PolicyDocument()), 'SetAsDefault' => true, ]); @@ -51,7 +51,7 @@ public function __invoke(array $options): StepResult Aws::iam()->createPolicy([ 'PolicyName' => Helpers::keyedResourceName(exclusive: false), 'Description' => 'YOLO managed EC2 policy', - 'PolicyDocument' => json_encode(AwsResources::policyDocument()), + 'PolicyDocument' => json_encode(AwsResources::ec2PolicyDocument()), ...Aws::tags(), ]); diff --git a/src/Steps/Iam/SyncRoleStep.php b/src/Steps/Iam/SyncEc2RoleStep.php similarity index 94% rename from src/Steps/Iam/SyncRoleStep.php rename to src/Steps/Iam/SyncEc2RoleStep.php index 70c1878..cfb19fe 100644 --- a/src/Steps/Iam/SyncRoleStep.php +++ b/src/Steps/Iam/SyncEc2RoleStep.php @@ -10,7 +10,7 @@ use Codinglabs\Yolo\Enums\StepResult; use Codinglabs\Yolo\Exceptions\ResourceDoesNotExistException; -class SyncRoleStep implements Step +class SyncEc2RoleStep implements Step { public function __invoke(array $options): StepResult { @@ -27,7 +27,7 @@ public function __invoke(array $options): StepResult Aws::iam()->updateAssumeRolePolicy([ 'RoleName' => $name, - 'PolicyDocument' => json_encode(AwsResources::rolePolicyDocument()), + 'PolicyDocument' => json_encode(AwsResources::ec2RolePolicyDocument()), ]); Aws::iam()->tagRole([ @@ -44,7 +44,7 @@ public function __invoke(array $options): StepResult Aws::iam()->createRole([ 'RoleName' => Helpers::keyedResourceName(exclusive: false), 'Description' => 'YOLO managed EC2 role', - 'AssumeRolePolicyDocument' => json_encode(AwsResources::rolePolicyDocument()), + 'AssumeRolePolicyDocument' => json_encode(AwsResources::ec2RolePolicyDocument()), ...Aws::tags(), ]); diff --git a/src/Steps/Iam/SyncMediaConvertRoleStep.php b/src/Steps/Iam/SyncMediaConvertRoleStep.php new file mode 100644 index 0000000..7a419a3 --- /dev/null +++ b/src/Steps/Iam/SyncMediaConvertRoleStep.php @@ -0,0 +1,63 @@ +updateRole([ + 'RoleName' => $name, + 'Description' => 'YOLO managed MediaConvert role', + ]); + + Aws::iam()->updateAssumeRolePolicy([ + 'RoleName' => $name, + 'PolicyDocument' => json_encode(AwsResources::mediaConvertPolicyDocument()), + ]); + + Aws::iam()->tagRole([ + 'RoleName' => $name, + ...Aws::tags(), + ]); + + return StepResult::SYNCED; + } + + return StepResult::WOULD_SYNC; + } catch (ResourceDoesNotExistException $e) { + if (! Arr::get($options, 'dry-run')) { + Aws::iam()->createRole([ + 'RoleName' => Helpers::keyedResourceName(Iam::MEDIA_CONVERT_ROLE), + 'Description' => 'YOLO managed MediaConvert role', + 'AssumeRolePolicyDocument' => json_encode(AwsResources::mediaConvertPolicyDocument()), + ...Aws::tags(), + ]); + + return StepResult::CREATED; + } + + return StepResult::WOULD_CREATE; + } + } +} diff --git a/src/Steps/Landlord/SyncQueueAlarmStep.php b/src/Steps/Landlord/SyncQueueAlarmStep.php index 68d38cb..fde49a3 100644 --- a/src/Steps/Landlord/SyncQueueAlarmStep.php +++ b/src/Steps/Landlord/SyncQueueAlarmStep.php @@ -27,7 +27,7 @@ public function __invoke(array $options): StepResult return StepResult::WOULD_SYNC; } - $snsTopic = AwsResources::topic(); + $snsTopic = AwsResources::alarmTopic(); Aws::cloudWatch()->putMetricAlarm([ 'ActionsEnabled' => true, diff --git a/src/Steps/Network/SyncSnsTopicStep.php b/src/Steps/Network/SyncSnsAlarmTopicStep.php similarity index 91% rename from src/Steps/Network/SyncSnsTopicStep.php rename to src/Steps/Network/SyncSnsAlarmTopicStep.php index 5198595..1d08be9 100644 --- a/src/Steps/Network/SyncSnsTopicStep.php +++ b/src/Steps/Network/SyncSnsAlarmTopicStep.php @@ -10,12 +10,12 @@ use Codinglabs\Yolo\Enums\StepResult; use Codinglabs\Yolo\Exceptions\ResourceDoesNotExistException; -class SyncSnsTopicStep implements Step +class SyncSnsAlarmTopicStep implements Step { public function __invoke(array $options): StepResult { try { - AwsResources::topic(); + AwsResources::alarmTopic(); return StepResult::SYNCED; } catch (ResourceDoesNotExistException $e) { diff --git a/src/Steps/Stage/CreateWebGroupCpuAlarmsStep.php b/src/Steps/Stage/CreateWebGroupCpuAlarmsStep.php index e57c75c..8130f37 100644 --- a/src/Steps/Stage/CreateWebGroupCpuAlarmsStep.php +++ b/src/Steps/Stage/CreateWebGroupCpuAlarmsStep.php @@ -61,7 +61,7 @@ public function __invoke(array $options): StepResult sprintf('web-cpu-critical-alarm-%s', Str::random(8)), exclusive: false ); - $snsTopic = AwsResources::topic(); + $snsTopic = AwsResources::alarmTopic(); Aws::cloudWatch()->putMetricAlarm([ 'ActionsEnabled' => true, @@ -94,7 +94,7 @@ public function __invoke(array $options): StepResult } return Arr::get($options, 'update') - ? StepResult::WOULD_SKIP + ? StepResult::SKIPPED : StepResult::WOULD_CREATE; } } diff --git a/src/Steps/Standalone/SyncQueueAlarmStep.php b/src/Steps/Standalone/SyncQueueAlarmStep.php index f33576b..a337a5f 100644 --- a/src/Steps/Standalone/SyncQueueAlarmStep.php +++ b/src/Steps/Standalone/SyncQueueAlarmStep.php @@ -23,7 +23,7 @@ public function __invoke(array $options): StepResult // always sync the alarm with the desired state. } - $snsTopic = AwsResources::topic(); + $snsTopic = AwsResources::alarmTopic(); if (Arr::get($options, 'dry-run')) { return StepResult::WOULD_SYNC; diff --git a/src/Steps/Tenant/SyncQueueAlarmStep.php b/src/Steps/Tenant/SyncQueueAlarmStep.php index 540858a..e341227 100644 --- a/src/Steps/Tenant/SyncQueueAlarmStep.php +++ b/src/Steps/Tenant/SyncQueueAlarmStep.php @@ -23,7 +23,7 @@ public function __invoke(array $options): StepResult // always sync the alarm with the desired state. } - $snsTopic = AwsResources::topic(); + $snsTopic = AwsResources::alarmTopic(); if (Arr::get($options, 'dry-run')) { return StepResult::WOULD_SYNC;