Category: PHP


PHP application logging with Amazon CloudWatch Logs and Monolog

by Joseph Fontes | on | in PHP | | Comments

Logging and information debugging can be approached from a multitude of different angles. Whether you use an application framework or coding from scratch it’s always comforting to have familiar components and tools across different projects. In our examples today, I am going to enable Amazon CloudWatch Logs logging with a PHP application. To accomplish this, I wanted to use an existing solution that is both already popular and well used, and that is standards compliant. For these reasons, we are going to use the open source log library, PHP Monolog (https://github.com/Seldaek/monolog).

PHP Monolog

For those who work with a new PHP application, framework, or service, one of the technology choices that appears more frequently across solutions is the use of Monolog for application logging. PHP Monolog is a standards-compliant PHP library that enables developers to send logs to various destination types including, databases, files, sockets, and different services. Although PHP Monolog predates the standards for PHP logging defined in PSR-3, it does implement the PSR-3 interface and standards. This makes Monolog compliant with the common interface for logging libraries. Using Monolog with CloudWatch Logs creates a PSR-3 compatible logging solution. Monolog is available for use with a number of different applications and frameworks such as Laravel, Symfony, CakePHP, and many others. Our example today is about using PHP Monolog to send information to CloudWatch Logs for the purpose of application logging and to build a structure and process that enables the use of our application data with CloudWatch alarms and notifications. This enables us to use logs from our application for cross-service actions such as with Amazon EC2 Auto Scaling decisions.

Amazon CloudWatch Logs

As a customer-driven organization, AWS is constantly building and releasing significant features and services requested by AWS customers and partners. One of those services that we highlight today is Amazon CloudWatch Logs. CloudWatch Logs enables you to store log file information from applications, operating systems and instances, AWS services, and various other sources. An earlier blog post highlighted the use of CloudWatch Logs with various programming examples.

Notice in the blog post that there is a PHP example that uses CloudWatch Logs to store an entry from an application. You can use this example and extend it as a standalone solution to provide logging to CloudWatch Logs from within your application. With our examples, we’ll enhance this opportunity by leveraging PHP Monolog.

Implementing Monolog

To begin using Monolog, we install the necessary libraries with the use of Composer (https://getcomposer.org/). The instructions below install the AWS SDK for PHP, PHP Monolog, and an add-on to Monolog that enables logging to CloudWatch Logs.

curl -sS https://getcomposer.org/installer | php
php composer.phar require aws/aws-sdk-php
php composer.phar require monolog/monolog
php composer.phar require maxbanton/cwh:^1.0

Alternatively, you can copy the following entry to the composer.json file and install it via the php composer.phar install command.

{
    "minimum-stability": "stable",
    "require": {
        "aws/aws-sdk-php": "^3.24",
        "aws/aws-php-sns-message-validator": "^1.1",
        "monolog/monolog": "^1.21",
        "maxbanton/cwh": "^1.0"
    }
}

Local logging

Now that PHP Monolog is available for use, we can test the implementation. We start with an example of logging to a single file.

require "vendor/autoload.php";

use Monolog\Logger;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;

$logFile = "testapp_local.log";

$logger = new Logger('TestApp01');
$formatter = new LineFormatter(null, null, false, true);
$infoHandler = new StreamHandler(__DIR__."/".$logFile, Logger::INFO);
$infoHandler->setFormatter($formatter);
$logger->pushHandler($infoHandler);
$logger->info('Initial test of application logging.');

In the previous example, we start by requiring the composer libraries we installed earlier. The new Logger line sets the channel name as “TestApp01”. The next line creates a new LineFormatter that removes brackets around unused log items. The next line establishes the destination as the file name we identified, testapp_local.log, and associates that with the INFO log level. Next, we apply the format to our stream handler. Then we add the stream handler with the updated format to the handler list. Finally, a new message is logged with the log level of INFO. For information about log levels and different handlers, see the Monolog GitHub page and IETF RFC 5424 and PSR-3.

We can now view the contents of the log file to ensure functionality:

Syslog logging

Now that we are able to write a simple log entry to a local file, our next example uses the system Syslog to log events.

$logger = new Logger($appName);

$localFormatter = new LineFormatter(null, null, false, true);
$syslogFormatter = new LineFormatter("%channel%: %level_name%: %message% %context% %extra%",null,false,true);

$infoHandler = new StreamHandler(__DIR__."/".$logFile, Logger::INFO);
$infoHandler->setFormatter($localFormatter);

$warnHandler = new SyslogHandler($appName, $facility, Logger::WARNING);
$warnHandler->setFormatter($syslogFormatter);

$logger->pushHandler($warnHandler);
$logger->pushHandler($infoHandler);

$logger->info('Test of PHP application logging.');
$logger->warn('Test of the warning system logging.');

Here we can see that the format of the syslog messages has been changed with the value, $syslogFormatter. Because syslog provides a date/time with each log entry, we don’t need to include these values in our log text. The syslog facility is set to local0 with all WARNING messages sent to syslog with the INFO level messages and WARNING level messages logged to our local file. You can find additional information about Syslog facilities and log levels on the Syslog Wikipedia page.

Logging to CloudWatch Logs

Now that you’ve seen the basic use of Monolog, let’s send some logs over to CloudWatch Logs. We can use the Amazon Web Services CloudWatch Logs Handler for Monolog library to integrate Monolog with CloudWatch Logs. In our example, an authentication application produces log information.

use Aws\CloudWatchLogs\CloudWatchLogsClient;
use Maxbanton\Cwh\Handler\CloudWatch;
use Monolog\Logger;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;

$logFile = "testapp_local.log";
$appName = "TestApp01";
$facility = "local0";

// Get instance ID:
$url = "http://169.254.169.254/latest/meta-data/instance-id";
$instanceId = file_get_contents($url);

$cwClient = new CloudWatchLogsClient($awsCredentials);
// Log group name, will be created if none
$cwGroupName = 'php-app-logs';
// Log stream name, will be created if none
$cwStreamNameInstance = $instanceId;
// Instance ID as log stream name
$cwStreamNameApp = "TestAuthenticationApp";
// Days to keep logs, 14 by default
$cwRetentionDays = 90;

$cwHandlerInstanceNotice = new CloudWatch($cwClient, $cwGroupName, $cwStreamNameInstance, $cwRetentionDays, 10000, [ 'application' => 'php-testapp01' ],Logger::NOTICE);
$cwHandlerInstanceError = new CloudWatch($cwClient, $cwGroupName, $cwStreamNameInstance, $cwRetentionDays, 10000, [ 'application' => 'php-testapp01' ],Logger::ERROR);
$cwHandlerAppNotice = new CloudWatch($cwClient, $cwGroupName, $cwStreamNameApp, $cwRetentionDays, 10000, [ 'application' => 'php-testapp01' ],Logger::NOTICE);

$logger = new Logger('PHP Logging');

$formatter = new LineFormatter(null, null, false, true);
$syslogFormatter = new LineFormatter("%channel%: %level_name%: %message% %context% %extra%",null,false,true);
$infoHandler = new StreamHandler(__DIR__."/".$logFile, Logger::INFO);
$infoHandler->setFormatter($formatter);

$warnHandler = new SyslogHandler($appName, $facility, Logger::WARNING);
$warnHandler->setFormatter($syslogFormatter);

$cwHandlerInstanceNotice->setFormatter($formatter);
$cwHandlerInstanceError->setFormatter($formatter);
$cwHandlerAppNotice->setFormatter($formatter);

$logger->pushHandler($warnHandler);
$logger->pushHandler($infoHandler);
$logger->pushHandler($cwHandlerInstanceNotice);
$logger->pushHandler($cwHandlerInstanceError);
$logger->pushHandler($cwHandlerAppNotice);

$logger->info('Initial test of application logging.');
$logger->warn('Test of the warning system logging.');
$logger->notice('Application Auth Event: ',[ 'function'=>'login-action','result'=>'login-success' ]);
$logger->notice('Application Auth Event: ',[ 'function'=>'login-action','result'=>'login-failure' ]);
$logger->error('Application ERROR: System Error');

In this example, application authentication events are passed as a PHP array and presented in CloudWatch Logs as JSON. The events with a result of login-success and login-failure are sent to both the log stream associated with the instance ID and to the log stream associated with the application name.

 

Using these different stream locations, we can create metrics and alarms at either a per-instance level or per-application level. Let’s assume that we want to create a metric for total number of users logged into our application over the past five minutes. Select your event group and then choose Create Metric Filter.

On the next page, we can create our filter and test in the same window. For the filter data, we use the JSON string from the log entry. Enter the following string to extract all the successful logins.

{ $.result = login-success }

Below, we can see the filter details. I updated the Filter Name to a value that’s easy to identify. The Metric Namespace now has a value associated with the application name and the metric name reflects the number of login-success values.

 

We could now create an alarm to send a notification or perform some action (such as an Amazon EC2 scaling decision), based on this information being received via CloudWatch Logs.

With these values, we would receive an alert each time there were more than 50 successful logins within a five-minute period.

Laravel logging

Monolog is used as the logging solution for a number of PHP applications and frameworks, including, the popular Laravel PHP framework. In this example, we’ll show the use of Monolog with CloudWatch Logs within Laravel. Our first step is to find out the current log settings for our Laravel application. If you open config/app.php within your application root, you see various log settings. By default, Laravel is set to log to a single log file using the baseline log level of debug.

Next, we add the AWS SDK for PHP as a service provider within Laravel using instructions and examples from here.

You also want to add the Monolog library for CloudWatch Logs to the composer.json file for inclusion in the application, as shown.

You now need to extend the current Laravel Monolog configuration with your custom configuration. You can find additional information about this step on the Laravel Error and Logging page. The following is an example of this addition to the bootstrap/app.php file.

use Maxbanton\Cwh\Handler\CloudWatch;

$app->configureMonologUsing( function($monolog) {

    $cwClient = App::make('aws')->createClient('CloudWatchLogs');
    $cwGroupName = env('AWS_CWL_GROUP', 'laravel-app-logs');
    $cwStreamNameApp = env('AWS_CWL_APP', 'laravel-app-name');
    $cwTagName = env('AWS_CWL_TAG_NAME', 'application');
    $cwTagValue = env('AWS_CWL_TAG_VALUE', 'laravel-testapp01');
    $cwRetentionDays = 90;
    $cwHandlerApp = new CloudWatch($cwClient, $cwGroupName, $cwStreamNameApp, $cwRetentionDays, 10000, [ $cwTagName => $cwTagValue ] );

    $monolog->pushHandler($cwHandlerApp);
});

For testing purposes, we add a logging call to a test route in routes/web.php.

Route::get('/test', function () {
    Log::warning('Clicking on test link!!!');
    return view('test');
});

When the test route is invoked, the logs now show in CloudWatch Logs.

Conclusion

In our examples, we’ve shown how to use PHP Monolog to log to a local file, syslog, and CloudWatch Logs. We have also demonstrated the integration of Monolog with CloudWatch Logs within a popular PHP application framework. Finally, we’ve shown how to create CloudWatch Logs metric filters and apply those to CloudWatch Alarms that make the data from the logs actionable with notifications, as well as scaling decisions. CloudWatch Logs provides a central logging capability for your PHP applications and, combined with Monolog, ensures the availability of the library for use within established projects and custom engagements.

Automating the Deployment of Encrypted Web Services with the AWS SDK for PHP (Part 2)

by Joseph Fontes | on | in PHP | | Comments

In the first post of this series, we focused on how to use Amazon Route 53 for domain registration and use Amazon Certificate Manager (ACM) to create SSL certificates. With our newly registered domain available for use, we can proceed to deploy and configure the services we need to host the www.dev-null.link website across an encrypted connection. Once complete, the infrastructure configuration will reflect the diagrams below.

Diagram 1

Diagram 2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

The first diagram shows the use of Route 53 to route traffic between AWS Elastic Beanstalk environments across multiple regions. The second example adds Amazon CloudFront support to the design.

AWS Elastic Beanstalk

Our first step is to create the Elastic Beanstalk application, which will provide the necessary infrastructure to host our website. The following is the order of the methods used for the AWS Elastic Beanstalk deployment:

  • createApplication
  • createApplicationVersion
  • createConfigurationTemplate
  • createEnvironment

We start by creating the Elastic Beanstalk application.

$ebCreateApplicationData = [ 'ApplicationName' => "DevNullDemo",
            'Description' => "Demo application for ACM deployment" ];

$ebCreateApplicationResult = $ebClient->createApplication($ebCreateApplicationData);

print_r($ebCreateApplicationResult);

Result

…
[Application] => Array
	(
	[ApplicationName] => DevNullDemo
	[Description] => Demo application for ACM deployment
…

Now we’ll create the initial version of the application and name it DevNullDemo. You can use the application archive of your choice, although a simple PHP demo site is available here.

$ebCreateAppVersionData = [ 'ApplicationName' => "DevNullDemo",
                            'VersionLabel' => 'v1',
                            'Description' => 'Initial Create',
                            'SourceBundle' => [ 'S3Bucket' => 'pub-materials',
                                                'S3Key' => 'Sample-App.zip' ] ];

$ebCreateAppVersionResult = $ebClient->createApplicationVersion($ebCreateAppVersionData);

print_r($ebCreateAppVersionResult);

Result

…
            [ApplicationVersion] => Array
                (
                    [ApplicationName] => DevNullDemo
                    [Description] => Initial Create
                    [VersionLabel] => v1
                    [SourceBundle] => Array
                        (
                            [S3Bucket] => pub-materials
                            [S3Key] => Sample-App.zip
                        )
                    [Status] => UNPROCESSED
                )
…

Next, we need to create an Elastic Beanstalk configuration template. This template requires the selection of an Elastic Beanstalk solution stack prior to calling the method, createConfigurationTemplate. The solution stack is the platform you choose to run your application within Elastic Beanstalk. You can find a list of available solution stack choices by using the listAvailableSolutionStacks method.

$ebSolutionStacks = $ebClient->listAvailableSolutionStacks();
print_r($ebSolutionStacks);

Result

…
            [SolutionStacks] => Array
                (
                    [0] => 64bit Windows Server Core 2012 R2 v1.2.0 running IIS 8.5
…
                    [5] => 64bit Amazon Linux 2016.03 v2.1.6 running Java 7
…
                    [9] => 64bit Amazon Linux 2014.03 v1.1.0 running Node.js
                    [14] => 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0
                    [15] => 64bit Amazon Linux 2015.09 v2.0.6 running PHP 5.6
                    [16] => 64bit Amazon Linux 2015.09 v2.0.4 running PHP 5.6
…
                    [14] => Array
                        (
                            [SolutionStackName] => 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0
                            [PermittedFileTypes] => Array
                                (
                                    [0] => zip
                                )

                        )
…

For our demonstration, we’ll use the, 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0 solution stack.

$ebConfigTemplateData = [ 'ApplicationName' => "DevNullDemo",
                          'TemplateName' => 'DevNullDemoTemplate',
                          'SolutionStackName' => '64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0',
                          'Description' => 'EB Environment template for blog deployment.' ];

$ebConfigTemplateCreateResult = $ebClient->createConfigurationTemplate($ebConfigTemplateData);

print_r($ebConfigTemplateCreateResult);

Result

…
        (
            [SolutionStackName] => 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0
            [ApplicationName] => DevNullDemo
            [TemplateName] => DevNullDemoTemplate
            [Description] => EB Environment template for blog deployment.
…

Now we can create and start the infrastructure by using the createEnvironment method. The following example sets additional options such as instance type and ACM SSL certificate. You need to replace the [CERTIFICATEARN] value with the AWS ACM certificate ARN created in part 1 of this series. You can also find this value by using the AWS ACM listCertificates method. For these examples, we’ve created a certificate across multiple regions for the host name eb.dev-null.link, in addition to the previously created www.dev-null.link certificate.

$ebCreateEnvData = [ 'ApplicationName' => "DevNullDemo",
                        'EnvironmentName' => "DevNullEnv",
                        'Description' => "Demo environment for ACM EB deployment.",
                        'TemplateName' => "DevNullDemoTemplate",
                        'VersionLabel' => 'v1',
                        'OptionSettings' => [

                                [ 'Namespace' => 'aws:elb:listener:443',
                                  'OptionName' => 'ListenerProtocol',
                                  'Value' => 'HTTPS' ],

                                [ 'Namespace' => 'aws:elb:listener:443',
                                  'OptionName' => 'SSLCertificateId',
                                  'Value' => '[CERTIFICATEARN]' ],

                                [ 'Namespace' => 'aws:elb:listener:443',
                                  'OptionName' => 'InstancePort',
                                  'Value' => '80' ],

                                [ 'Namespace' => 'aws:elb:listener:443',
                                  'OptionName' => 'InstanceProtocol',
                                  'Value' => 'HTTP' ],

                                [ 'Namespace' => 'aws:autoscaling:launchconfiguration',
                                  'OptionName' => 'InstanceType',
                                  'Value' => 't2.nano' ],

                                ],
                        'Tier' => [ 'Name' => 'WebServer',
                                    'Type' => 'Standard',
                                    'Version' => ' ' ],
                        ];

$ebCreateEnvData = $ebClient->createEnvironment($ebCreateEnvData);

print_r($ebCreateEnvData);

Result

…
            [EnvironmentName] => DevNullEnv
            [EnvironmentId] => e-fnvhjptdjd
            [ApplicationName] => DevNullDemo
            [VersionLabel] => v1
            [SolutionStackName] => 64bit Amazon Linux 2016.03 v2.1.7 running PHP 7.0
            [Description] => Demo environment for ACM EB deployment.

            [Status] => Launching
            [Health] => Grey
            [Tier] => Array
                (
                    [Name] => WebServer
                    [Type] => Standard
                    [Version] =>
                )
…

As the results show, the current status of our environment is Launching. We can periodically check the status with the describeEnvironments method.

$ebDescEnvResult = $ebClient->describeEnvironments();

foreach($ebDescEnvResult['Environments'] as $ebEnvList) {
        print "Name:\t".$ebEnvList['EnvironmentName']."\n";
        print "ID:\t".$ebEnvList['EnvironmentId']."\n";
        print "CNAME:\t".$ebEnvList['CNAME']."\n";
        print "Status:\t".$ebEnvList['Status']."\n\n";
}

Result

Name:  	DevNullEnv
ID:    	[ID]
CNAME: 	DevNullEnv.[ID].[Region].elasticbeanstalk.com
Status:	Ready

When the environment has a status of Ready, we can proceed to create the necessary DNS records. You can also check that the site is functional by pasting the CNAME value into a web browser. Be sure to record this CNAME value so you can use it later.

Demo App

You will want to repeat this process across additional AWS Regions to demonstrate latency-based DNS resolution.

Amazon Route 53

Our next step is to create a Route 53 hosted zone. This hosted zone will define a domain name (or subdomain) for which we are authoritative and, thus, allowed to create DNS records. We’ll start with the createHostedZone method.

$route53Client = $sdk->createRoute53();

$route53Data = [ 'Name' => "dev-null.link",
            'CallerReference' => "BLOGPOSTREF001",
            'HostedZoneConfig' => [ 'Comment' => "AWS SDK sample dev-null.link" ] ];

$route53Result = $route53Client->createHostedZone($route53Data);

Result

            [HostedZone] => Array
                (
                    [Id] => /hostedzone/[Amazon Route 53 Zone ID]
                    [Name] => dev-null.link.
                    [CallerReference] => BLOGPOSTREF001
                    [Config] => Array
                        (
                            [Comment] => AWS SDK sample dev-null.link
                            [PrivateZone] =>
                        )

                    [ResourceRecordSetCount] => 2
                )
…
            [DelegationSet] => Array
                (
                    [NameServers] => Array
                        (
                            [0] => ns-999.awsdns-60.net
		…
)
                )

You should copy the ID of the hosted zone from this result. You can also find a list of all hosted zone ID values by using the listHostedZones method.

$route53ListResult = $route53Client->listHostedZones();

foreach($route53ListResult['HostedZones'] as $zoneItem) {
        print "Name:\t".substr($zoneItem['Name'],0,-1)."\n";
        print "ID:\t".$zoneItem['Id']."\n\n";
}

Result

Name:  	dev-null.link
ID:    	/hostedzone/[Amazon Route 53 Zone ID]

We’ll now create a new DNS entry so that our website is visible via web browser with the eb.dev-null.link host name. For this, we need to use the CNAME value from our Elastic Beanstalk application.

$currentDate = date("r");
$hostedZoneId = "/hostedzone/[Amazon Route 53 Zone ID]";
$subDomain = "eb.dev-null.link";
$ebCname = "DevNullEnv.[EB ID].[Region].elasticbeanstalk.com";

$recordComment = "Created $subDomain record on $currentDate";

$route53RecordData = [ 'HostedZoneId' => $hostedZoneId,
                    'ChangeBatch' => [ 'Comment' => $recordComment,
                    'Changes' => [
                                       [ 'Action' => 'CREATE',
                                         'ResourceRecordSet' => [ 'Name' => $subDomain,
                                                                  'Type' => 'CNAME',
                                                                  'TTL' => 60,
                                                                  'ResourceRecords' => [ [ 'Value' => $ebCname ] ] ]
] ] ] ];

$route53ChangeResult = $route53Client->changeResourceRecordSets($route53RecordData);

print_r($route53ChangeResult);

Result

…
            [ChangeInfo] => Array
                (
                    [Id] => /change/[ChangeInfo ID]
                    [Status] => PENDING
…
                    [Comment] => Created test.jf.unicorn.rentals record on Thu, 15 Sep 2016 12:25:07 -0700
                )
…

As we can see from the result, the status of the change is PENDING. We can check the status with the getChange method using the value of the ChangeInfo ID.

$route53ChangeData = [ 'Id' => "/change/[ChangeInfo ID]" ];
$route53ChangeResult = $route53Client->getChange($route53ChangeData);
print_r($route53ChangeResult);

Result

…
            [ChangeInfo] => Array
                (
                    [Id] => /change/<ChangeInfo ID>
                    [Status] => INSYNC
…
                    [Comment] => Created test.jf.unicorn.rentals record on Thu, 15 Sep 2016 12:25:07 -0700
                )
…

Now that our change has the status of INSYNC, we can view the secure URL in our browser window with the URL https://eb.dev-null.link/.
Demo App

Deploying Across Multiple Regions

This deployment is now serving our website across an encrypted connection in a single AWS Region. For those who would like to use multiple regions, we can expand our current configuration. An AWS ACM certificate is needed in each AWS Region used for the deployment. Because we’ll be using CloudFront, we have to ensure that a certificate is created in the us-east-1 region because CloudFront will source the available AWS ACM certificates from there. You can reference the previous blog post for instructions on creating an AWS ACM certificate in additional regions. Next, run the Elastic Beanstalk creation methods shown earlier in each additional region where you want to deploy the application. Be sure to record the CNAME value for each environment.

After we have all of the necessary Elastic Beanstalk environments running, we need to delete the Route 53 resource record for eb.dev-null.link so that we can replace it with a latency-based record set.

$recordComment = "Deleted $subDomain record on $currentDate";

$route53RecordData = [ 'HostedZoneId' => $hostedZoneId,
                    'ChangeBatch' => [ 'Comment' => $recordComment,
                    'Changes' => [
                                       [ 'Action' => 'DELETE',
                                         'ResourceRecordSet' => [ 'Name' => $subDomain,
                                                                  'Type' => 'CNAME',
                                                                  'TTL' => 60,
                                                                  'ResourceRecords' => [ [ 'Value' => $ebCname ] ] ]
] ] ] ];

$route53ChangeResult = $route53Client->changeResourceRecordSets($route53RecordData);

You might notice that the method and instructions to delete the record are almost identical to the instructions used to create the record. They even use the same method, changeResourceRecordSets.

Result

…
            [ChangeInfo] => Array
                (
                    [Id] => /change/[ChangeInfo ID]
                    [Status] => PENDING

                    [Comment] => Deleted www.dev-null.link record on Thu, 15 Sep 2016 14:58:51 -0700
                )
…

Our next step is to add the latency-based routing rules. This example provides the CNAME of the Elastic Beanstalk environment via the describeEnvironments method.

$currentDate = date("r");
$hostedZoneId = "HOSTED ZONE ID";

$ebDescEnvData = [ 'EnvironmentNames' => [ 'DevNullEnvProd' ] ];
$ebDescEnvResult = $ebClient->describeEnvironments($ebDescEnvData);

$ebCname = $ebDescEnvResult['Environments'][0]['CNAME'];

$recordComment = "Created www record on $currentDate";

$route53RecordData = [ 'HostedZoneId' => $hostedZoneId,
                    'ChangeBatch' => [ 'Comment' => $recordComment,
                    'Changes' => [
                                       [ 'Action' => 'CREATE',
                                         'ResourceRecordSet' => [
                                                'Name' => "eb.dev-null.link",
                                                'Type' => 'CNAME',
                                                'TTL' => 60,
                                                'Region' => $region,
                                                'SetIdentifier' => str_replace("-","",$region),
                                                'ResourceRecords' => [ [ 'Value' => $ebCname ], ],
                                                ],
                                        ],
                                ],
                        ],
                ];

$route53ChangeResult = $route53Client->changeResourceRecordSets($route53RecordData);

Result

…
            [ChangeInfo] => Array
                (
                    [Id] => /change/[ChangeInfo ID]
                    [Status] => PENDING
…

Demo App

In the Route 53 console, we can see there are two CNAMEs that now resolve the host name, eb.dev-null.link. When a user visits the website, the URL returned will correspond to the lower of the record latencies.

Amazon CloudFront

To enhance the user experience, we’ll now configure and deploy a content delivery network solution named Amazon CloudFront. This AWS service provides content delivery acceleration for the media provided with our web application. Each CloudFront deployment is composed of a CloudFront distribution with each distribution having one or more origins. An origin defines the mapping of a URL to a particular destination. The host name, www.dev-null.link, will resolve to our CloudFront distribution, which will then serve pages from the backend eb.dev-null.link load-balanced site. We first create our distribution with the createDistribution method. The value of $cfCreateDistData can be found in this Github Gist.

$cfCreateDistResult = $cfClient->createDistribution($cfCreateDistData);
print_r($cfCreateDistResult);

Result

Aws\Result Object
(
    [data:Aws\Result:private] => Array
        (
            [Distribution] => Array
                (
                    	[Id] => [CF ID Value]
[DomainName] => [CloudFront ID].cloudfront.net
…

Once complete, we need to save the value of DomainName from the result returned. Next, we’ll create a new Route 53 CNAME record that points www.dev-null.link to our CloudFront distribution.

$subDomain = "www.dev-null.link";
$cfCname = "[CloudFront ID].cloudfront.net";

$recordComment = "Created $subDomain record on $currentDate";

$route53RecordData = [ 'HostedZoneId' => $hostedZoneId,
                    'ChangeBatch' => [ 'Comment' => $recordComment,
                    'Changes' => [
                                       [ 'Action' => 'CREATE',
                                         'ResourceRecordSet' => [ 'Name' => $subDomain,
                                                                  'Type' => 'CNAME',
                                                                  'TTL' => 60,
                                                                  'ResourceRecords' => [ [ 'Value' => $cfCname ] ] ]
] ] ] ];

$route53ChangeResult = $route53Client->changeResourceRecordSets($route53RecordData);

Result

Aws\Result Object
(
    [data:Aws\Result:private] => Array
        (
            [ChangeInfo] => Array
                (
[Status] => PENDING
…

Once complete, we can test the new deployment by navigating to the URL (https://www.dev-null.link/) in a web browser.

Conclusion

With our infrastructure configuration completed, we now have a globally load balanced web application that’s accessible via encrypted communications. We’ve shown we can use the AWS SDK for PHP to automate these deployments, which provides the agility to reproduce these environments for customers on demand. Next, we’ll continue this series by reviewing deployments that use Amazon S3 for static content hosting, and the AWS Application Load Balancer and Elastic Load Balancing with Amazon EC2 instance deployments.

Automating the Deployment of Encrypted Web Services with the AWS SDK for PHP

by Joseph Fontes | on | in PHP | | Comments

Having worked in the web hosting space, one of the areas I find so fun about AWS is the ease of automating tasks that have historically been quite disjointed.  The process of supporting a customer request to register a domain, create or update DNS entries, configure the load balancer, deploy servers, etc., had me working across a multitude of systems, interfaces, and APIs.  Now, with the release of AWS Certificate Manager (ACM) in addition to existing AWS services, AWS provides all the tools and capabilities needed to support the provisioning of these services within customer accounts.

In this three-part series of posts, we’ll review how to use the AWS SDK for PHP to automate web service deployment, domain registration, DNS administration, and SSL certificate generation and assignment.  Using the examples outlined in these posts, as well as other features and functions of the AWS SDK for PHP, you’ll learn how to programmatically create a process for automatically purchasing a domain and then deploying an HTTPS (SSL-secured) web service on AWS by using either Amazon EC2 or AWS Elastic Beanstalk.

The examples in this post focus on using Amazon Route 53 to automate the registration of domain names and DNS administration.  Next, we’ll showcase how to use ACM to create and manage SSL certificates.  In subsequent posts, we will show how to automate the setup of encrypted HTTPS web services with the new domain and newly created certificates on Elastic Beanstalk.  Then, we’ll show how to automate deployments to EC2 and Elastic Load Balancing.  Once complete, we’ll have two web application stacks.  We’ll run the www.dev-null.link site from Elastic Beanstalk, and use EC2 and Elastic Load Balancing to run the second web application stack.  The following diagrams illustrate the final designs.

   

Amazon Route53 Domain Registration

The first task in building the web infrastructure is to identify and register an available domain name.  We can use the AWS SDK for PHP to check domain name availability.  We will use a method called checkDomainAvailability, which is part of the Route 53 Domains client.  We can automate the process of testing domains until we have a name that meets our application’s needs and is also available for registration.  The example below loops through an array of domain names, listing their current status for registration.

$route53Client = $sdk->createRoute53Domains();

$domainNames = [ "test.com", "dev.com", "dev-null.link", "null38.link" ];

foreach($domainNames as $domainNameElement) {
        $route53CheckDomainAvailData = [ 'DomainName' => $domainNameElement ];
        $route53CheckDomainResults = $route53Client->checkDomainAvailability($route53CheckDomainAvailData);
        print "Domain $domainNameElement is ".$route53CheckDomainResults['Availability']."n";
}

You can view the results of the check below.

There are two domain names available for registration.  In this example, we’ll register the domain dev-null.link.  This name contains the “.link” top-level domain (TLD), and the “dev-null” second-level domain. Now, register the domain by using the registerDomain method.  The registration has several required fields that we need to complete. These requirements are specific to each top level domain. For this example, we can use the following data ( provided in this Github Gist):
$route53DomainRegData = [
    'AdminContact' => [
        'AddressLine1' => $address1,
        'AddressLine2' => $address2,
        'City' => $city,
        'ContactType' => 'PERSON',
        'CountryCode' => 'US',
.....

$route53Client = $sdk->createRoute53Domains();
$route53CreateDomRes = $r53Client->registerDomain($route53DomainRegData);

print_r($route53DomainRegData);

Notice that the PhoneNumber data element must be in the format of  “+1.1231231212” to be valid.

We can now register the domain as follows.
[user@dev1 scripts]# php aws-route53-register-domain.php 
...
            [statusCode] => 200 
            [effectiveUri] => https://route53domains.us-east-1.amazonaws.com 
...

While we wait for the registration process to finish, we can check on the status of the domain.  First, use the listOperations method to print the list of current operations, and then enter the operation number into the getOperationDetail method.  Let’s look at all of the pending operations with the code.

$route53ListOperationsResults = $route53Client->listOperations();
print_r($route53ListOperationsResults);

$route53OperDetails = [ 'OperationId' => $operationId ];
$route53OperResults = $route53Client->getOperationDetail($route53OperDetails);
print_r($route53OperResults);

Result

[user@dev1 scripts]#
[user@dev1 scripts]# php aws-route53-list-operations.php
…
[Status] => IN_PROGRESS
                    [Type] => REGISTER_DOMAIN
…

AWS Certificate Manager

With ACM, we no longer have to worry about certificate expirations, securing the certificate private keys, copying self-signed CA certificates to clients, making sure servers all have the right certificate, or even the cost of a managed SSL certificate.  AWS provides managed SSL certificates at no cost.  Also, AWS handles the responsibility of renewing the certificate and placing it on the devices used to terminate SSL connections.  ACM can be used across managed AWS services such as ELB, Amazon CloudFront, and Elastic Beanstalk.

ACM in Action

Let’s go through how to create multiple certificates to secure connections to different websites.  We must first request a new certificate with the corresponding public and private keys.  The requestCertificate method automates this process.

The following example shows how to generate the certificate for our first domain.

$acmClient = $sdk->createAcm();

$acmRequestCertData = [ 'DomainName' => "www.dev-null.link",
    'DomainValidationOptions' => [
        [
            'DomainName' => "www.dev-null.link",
            'ValidationDomain' => "dev-null.link",
        ],
    ],
    'IdempotencyToken' => 'TOKENSTRINGDEVNULL01',
    'SubjectAlternativeNames' => ['dev-null.link', 'images.dev-null.link'],
];

$acmRequestCertResults = $acmClient->requestCertificate($acmRequestCertData);
print_r($acmRequestCertResults);

Result

[user@dev1 scripts]# php aws-acm-requestCertificate.php
...
    [CertificateArn] => arn:aws:acm:us-east-1:ACCOUNTID:certificate/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
    [@metadata] => Array
        (
            [statusCode] => 200
...
 

SubjectAlternativeNames identifies other DNS entries that the certificate should cover.  In this example, they are all subdomains of dev-null.link but can also include other domain names that could be used synonymously with our requested domain.  You can repeat this call for any additional certificates you need.  Be sure to update the value of IdempotencyToken for each certificate request created.  You should save the value of CertificateArn because you’ll need to use it later.

We want to secure the primary hostname www.dev-null.link.  ACM requires validation for the domain from an email address that is tied to the registration information.  Domain validation requests are sent to multiple locations.  These validation emails are sent to domain email addresses in the following order: admin@dev-null.link, administrator@dev-null.link, hostmaster@dev-null.link, postmaster@dev-null.link, and webmaster@dev-null.link.  In addition, a validation request is also sent to the email contacts for the Administrative, Technical, and Domain Registrant.  The following figure shows a copy of the received email.

When you click the link in the email, you’re taken to the page shown below.

Next, click I Approve.  A confirmation page appears that you can save for your records.

Let’s now use the listCertificates and describeCertificate methods to show all of the certificates we’ve generated.

$acmListCertResults = $acmClient->listCertificates();
print_r($acmListCertResults);

Result

[user@dev1 scripts]# php aws-acm-list.php
...
    [CertificateSummaryList] => Array
...
                    [CertificateArn] => arn:aws:acm:us-east-1:ACCOUNTID:certificate/CERTIFICATE-ID
                    [DomainName] => www.dev-null.link
...
                    [CertificateArn] => arn:aws:acm:us-east-1:ACCOUNTID:certificate/CERTIFICATE-ID
                    [DomainName] => api.dev-null.link
...
 
You can view details about the certificates by calling the describeCertificates method with the CertificateARN received from the 
previous listCertificates call.
$certificateArn = $acmListCertResults['CertificateSummaryList'][0]['CertificateArn'];

$acmDescribeCertData = [ 'CertificateArn' => $certificateArn ];
$acmDescribeCertResults = $acmClient->describeCertificate($acmDescribeCertData);

print_r($acmDescribeCertResults);

You can view the full output here with abbreviated output shown below.

[Certificate] => ...
Array
(
        [CertificateArn] => CERTIFICATE-ARN
        [DomainName] => www.dev-null.link
        [SubjectAlternativeNames] => Array
        (
                [0] => www.dev-null.link
                [1] => dev-null.link
                [2] => images.dev-null.link
        )
...

Finally, view the full certificate with the certificate chain.

$acmGetCertificateData = [ 'CertificateArn' => $certificateArn ];

$acmGetCertificateResults = $acmClient->getCertificate($acmGetCertificateData);

print_r($acmGetCertificateResults);
Result
[Certificate] => -----BEGIN CERTIFICATE-----
…
-----END CERTIFICATE-----

 [CertificateChain] => -----BEGIN CERTIFICATE-----
…
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
…
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
…
-----END CERTIFICATE-----
    [@metadata] => Array
        (
            [statusCode] => 200
…
This code will return the public certificate and the chain of trust leading to the public CA that is signing the certificate allowing web browsers to trust the website being visited.
To get more information about the certificate and the request process, we can use the describeCertificate method.  This method takes the certificateArn value as input and produces information about the referenced certificate.  This information includes the list of email addresses that received validation emails, the encryption algorithm used, the certificate creation and expiration dates, and the full list of host names covered by the certificate.  Looking forward, we can delete certificates and resend the validation email with the available ACM methods.
Now that we’ve covered the setup, configuration, and deployment of ACM and DNS, we now have a registered domain available for use.  In the next post, we’ll review how to use the domain, dev-null.link, for our website.  We’ll deploy an HTTPS website that is secured with SSL/TLS by using the AWS SDK with ELB, Amazon EC2, and Elastic Beanstalk.  We’ll also cover creating and deleting Amazon Route 53 resource records, and how to assign our ACM certificate to newly created load balancers.

 

 

Concurrency in Version 3 of the AWS SDK for PHP

by Jonathan Eskew | on | in PHP | | Comments

From executing API commands in the background to grouping commands and waiters into concurrent pools, version 3 of the AWS SDK for PHP lets you write asynchronous code that blocks only when you need it to. In this blog post, I’ll show you how to take advantage of some of the SDK’s concurrency abstractions, including promises, command pools, and waiters.

Promises

The AWS SDK for PHP makes extensive use of promises internally, relying on the Guzzle Promises library. Each operation defined on an API supported by the SDK has an asynchronous counterpart that can be invoked by tacking Async to the name of the operation. An asynchronous operation will immediately return a promise that will be fulfilled with the result of the operation or rejected with an error:

$s3Client = new AwsS3S3Client([ 
    ‘region’ => ‘us-west-2’,
    ‘version’ => ‘2006-03-01’,
]);

// This call will block until a response is obtained
$buckets = $s3Client->listBuckets();

// This call will not block
$promise = $s3Client->listBucketsAsync();

// You can provide callbacks to invoke when the request finishes
$promise->then(
    function (AwsResultInterface $buckets) {
        echo ‘My buckets are: ‘
             . implode(‘, ‘, $buckets->search(‘Buckets[].Name’));
    },
    function ($reason) {
        echo "The promise was rejected with {$reason}";
    }
);

The Guzzle promises returned by asynchronous operations have a wait method that you can call if you need to block until an operation is completed. In fact, synchronous operations like $s3Client->listBuckets() are calling asynchronous operations under the hood and then waiting on the result.

Where promises really shine, though, is in groupings. Let’s say you need to upload ten files to an Amazon S3 bucket. Rather than simply loop over the files and upload them one by one, you can create ten promises and then wait for those ten requests to be complete:

$filesToUpload = [
    ‘/path/to/file/1.ext’,
    ‘/path/to/file/2.ext’,
    …
    ‘/path/to/file/10.ext’,
];
$promises = [];
foreach ($filesToUpload as $path) {
    $promises []= $s3Client->putObjectAsync([
        ‘Bucket’ => $bucketName,
        ‘Key’ => basename($path),
        ‘SourceFile’ => $path,
    ]);
}
// Construct a promise that will be fulfilled when all
// of its constituent promises are fulfilled
$allPromise = GuzzleHttpPromiseall($promises);
$allPromise->wait();

Rather than taking ten times as long as uploading a single file, the asynchronous code above will perform the uploads concurrently. For more information about promises, see the AWS SDK for PHP User Guide.

Waiters

Some AWS operations are naturally asynchronous (for example, those in which a successful response means that a process has been started, but is not necessarily complete). Provisioning Amazon EC2 instances or S3 buckets are gppd examples. If you were starting a project that required three S3 buckets and an Amazon ElastiCache cluster, you might start out by provisioning those resources programmatically:

$sdk = new AwsSdk([‘region’ => ‘us-west-2’, ‘version’ => ‘latest’]);
$elasticacheClient = $sdk->get(‘elasticache’);
$s3Client = $sdk->get(‘s3’);
$promises = [];
for ($i = 0; $i < 3; $i++) {
    $promises []= $s3Client->createBucket([
        ‘Bucket’ => “my-bucket-$i”,
    ]);
}
$cacheClusterId = uniqid(‘cache’);
$promises []= $elasticacheClient->createCacheCluster([
    ‘CacheClusterId’ => $cacheClusterId,
]);
$metaPromise = GuzzleHttpPromiseall($promises);
$metaPromise->wait();

Waiting on the $metaPromise will block only until all of the requests sent by the createBucket and createCacheCluster operations have been completed.You would need to use a waiter to block until those resources are available. For example, you can wait on a single bucket with an S3Client’s waitUntil method:

$s3Client->waitUntil(‘BucketExists’, [‘Bucket’ => $bucketName]);
Like operations, waiters can also return promises, allowing you to compose meta-waiters from individual waiters:
$waiterPromises = [];
for ($i = 0; $i < 3; $i++) {
    // Create a waiter
    $waiter = $s3Client->getWaiter(‘BucketExists’, [
        ‘Bucket’ => “my-bucket-$i”,
    ]);
    // Initiate the waiter and retrieve a promise.
    $waiterPromises []= $waiter->promise();
}
$waiterPromises []= $elasticacheClient
    ->getWaiter(‘CacheClusterAvailable’, [
        ‘CacheClusterId’ => $cacheClusterId,
    ])
    ->promise();
// Composer a higher-level promise from the individual waiter promises
$metaWaiterPromise = GuzzleHttpPromiseall($waiterPromises);
// Block until all waiters have completed
$metaWaiterPromise->wait();

Command Pools

The SDK also allows you to use command pools to fine-tune the way in which a series of operations are performed concurrentl. Command pools are created with a client object and an iterable list of commands, which can be created by calling getCommand with an operation name on any SDK client:

// Create an S3Client
$s3Client = new AwsS3S3Client([
    ‘region’ => ‘us-west-2’,
    ‘version’ => ‘latest’,
]);

// Create a list of commands
$commands = [
    $s3Client->getCommand(‘ListObjects’, [‘Bucket’ => ‘bucket1’]),
    $s3Client->getCommand(‘ListObjects’, [‘Bucket’ => ‘bucket2’]),
    $s3Client->getCommand(‘ListObjects’, [‘Bucket’ => ‘bucket3’]),
];

// Create a command pool
$pool = new AwsCommandPool($s3Client, $commands);

// Begin asynchronous execution of the commands
$promise = $pool->promise();

// Force the pool to complete synchronously
$promise->wait();

How is this different from gathering promises from individual operations, such as by calling $s3Client->listObjectsAsync(…)? One key difference is that no action is taken until you call $pool->promise(), whereas requests are dispatched immediately when you call $s3Client->listObjectsAsync(…). The CommandPool defers the initiation of calls until you explicitly tell it to do so. In addition, by default, a command pool will limit concurrency to 25 operations at a time by default, This simultaneous operation limit can be tuned according to your project’s needs. For a more complex example see CommandPool in the AWS SDK for PHP User Guide.

With promises, waiters, and command pools, version 3 of the AWS SDK for PHP makes it easy to write asynchronous or concurrent code! We welcome your feedback.

Automating the Deployment of AWS Config with the AWS SDK for PHP

by Jonathan Eskew | on | in PHP | | Comments

My colleague Joseph Fontes, an AWS Solutions Architect, wrote the guest post below to discuss automation strategies for AWS Config.


There are times when you need to automate the deployment of services either in your account or within external accounts.  When I recently had to enable AWS Config support in remote accounts, I approached this task the way many others do….by opening the AWS SDK for PHP Reference Guide!

To complete this task, we will need three AWS Config methods: putConfigurationRecorder(), putDeliveryChannel(), and startConfigurationRecorder().  Before making the call to putDeliveryChannel(), we need to create our Amazon S3 bucket destination and identify an AWS SNS topic. 

Let’s instantiate the clients we will need for this exercise.  The client creation will be slightly different for those familiar with creating clients in version 2 of the AWS SDK for PHP.

$s3Client = $sdk->createS3();
$iamClient = $sdk->createIam();
$confClient = $sdk->createConfigService();
$snsClient = $sdk->createSns();

Let’s next create an S3 bucket and destination for our AWS Config logs.  Remember that S3 buckets must be globally unique. We cannot simply use a name like “logs.”  For this reason, our naming convention will use our account number:

$accountID = "XXXXXXXXXXXX";
$s3Bucket = $accountID."-config";
$role = "AWS-Config-Role-IAM";

$s3Res = $s3Client->listBuckets([]);

$s3ResA = $s3Res->toArray();

if(bucketExists($sdk,$s3Bucket,$s3ResA) == 0) {
    $s3Data = [
        'ACL' => 'private',
        'Bucket' => $s3Bucket, 
        'LocationConstraint' => $region
    ];
    $s3Res = $s3Client->createBucket($s3Data);
    $s3ResA = $s3Res->toArray();
    print_r($s3ResA);
    print "Waiting for bucket to become available...";
    $s3Client->waitUntil('BucketExists', [
        'Bucket' => $s3Bucket
    ]);
}

In the preceding example, I have written a function to test if the bucket exists.  This is a completely optional step. The full code will be made available for download.

The call for createBucket() with Amazon S3 followed by the waitUntil() method delays script execution until the S3 bucket is available for use. 

We now need to create the IAM role AWS Config uses to access the S3 bucket.  We need an assume role policy to reference during the call.  I have created a text file, policy.json, with the following contents:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "config.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Create a local copy of the policy.json file.

Next, we need to create an additional policy file that gives the AWS Config service permissions to perform required tasks in your account. Download the following file and place it in your running directory:

config-policy.json

We can now create the IAM role for the AWS Config service:

$config_topic = "ConfigTopicSNS";
$policy_info = file_get_contents('config-policy.json');
$replace_sns = ":".$AccountID.":".$config_topic;
$replS3Bucket = "XXXXXXXXXXXXXXXXXXXXXXXXXXX";
$replSNS = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYY";
$replS3Bucket2 = "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ";
$policy_info = str_replace($replS3Bucket,$s3Bucket,$policy_info);
$policy_info = str_replace($replSNS,$replace_sns,$policy_info);
$policy_info = str_replace($replS3Bucket2,$s3Bucket,$policy_info);

$iamData = [
    'RoleName' => $role,
    'AssumeRolePolicyDocument' => file_get_contents('policy.json')
];

$iamRes = $iamClient->createRole($iamData);
$iamResA = $iamRes->toArray();

$roleARN = $iamResA['Role']['Arn'];
$iamData = [
    'PolicyDocument' => $policy_info,
    'PolicyName' => 'NEWConfigPolicy',
    'Description' => "config policy via sdk"
];
$iamRes = $iamClient->createPolicy($iamData);
$iamResA = $iamRes->toArray();

$confPolicyArn = $iamResA['Policy']['Arn'];

$iamData = [
    'PolicyArn' => $ConfPolicyArn,
    'RoleName' => $Role
];
$iamRes = $iamClient->attachRolePolicy($iamData);
$iamResA = $iamRes->toArray();

This portion imports the trust policy defined by the local file, policy.json, along with the permissions policy identified by the local file, config-policy.json.  The permissions policy is modified upon read to ensure proper identifiers are used in the script.

Let’s create the SNS topic.

$snsTopic = 'ConfigTopicSNS';

$snsData = ['Name' => $config_topic];
$snsRes = $snsClient->createTopic($snsData);
$snsResA = $snsRes->toArray();

$snsTopicARN = $snsResA['TopicArn'];

We now have to call the putConfigurationRecorder() method.  This creates a new configuration recorder that will identify the changes we want to record.  In this example, we want to record all changes.  Readers can be more prescriptive in their recordings by identifying resources types.  You’ll find a list of supported resource types here:

http://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html#supported-resources

$confData = [
    'ConfigurationRecorder' => [
        'name' => 'default',
        'recordingGroup' => [
            'allSupported' => true,
        ],
        'roleARN' => $roleARN,
    ],
];

$confClient->putConfigurationRecorder($confData);

Now that we know what we are going to record, we have to identify where we will send the recordings.  The following shows the putDeliveryChannel() method, which needs the S3 bucket (created earlier) to store the recordings and the SNS topic, which will alert to configuration changes.

$confData = [
    'DeliveryChannel' => [
        'name' => 'default',
        's3BucketName' => $s3Bucket,
        'snsTopicARN' => $snsTopicARN,
    ],
];

$confClient->putDeliveryChannel($confData);

Finally, now that we have our recording configuration and methods for delivery defined, we have to start recording the changes:

$confData = ['ConfigurationRecorderName' => 'default' ];

$confClient->startConfigurationRecorder($confData);

Now that changes to infrastructure within this region of our account are being recorded with notifications sent, we can be notified of and record infrastructure updates.  We can then use additional SNS subscriptions to process the list of items in our infrastructure that have changed, review them for root cause analysis in the event of service issues, use centralized logging along with event correlation to look for system anomalies, and so on.

You can review the processing of Amazon SNS notifications here:

http://blogs.aws.amazon.com/php/post/Tx15276Q7B4NUO0/Receiving-Amazon-SNS-Messages-in-PHP


 

 

Receiving Amazon SNS Messages in PHP

by Jonathan Eskew | on | in PHP | | Comments

A little over a year ago, we announced a new feature in the AWS SDK for PHP that allowed customers to validate inbound SNS messages. In the latest version of the SDK, this functionality is now available in its own package, one that does not depend on the SDK, so it’s simpler to listen to SNS topics with lightweight web services. In this blog post, I’ll show you how to use the Amazon SNS message validator for PHP to parse messages in a single-file web application.

About SNS

Amazon Simple Notification Service (Amazon SNS) is a fast and fully managed push messaging service that can deliver messages over email, SMS, mobile push, and HTTP/HTTPS endpoints.

With Amazon SNS, you can set up topics to publish custom messages to subscribed endpoints. SNS messages are used by many of the other AWS services to communicate information asynchronously about your AWS resources. Some examples include:

  • Configuring Amazon Glacier to notify you when a retrieval job is complete.
  • Configuring AWS CloudTrail to notify you when a new log file has been written.
  • Configuring Amazon Elastic Transcoder to notify you when a transcoding job changes status (e.g., from “Progressing” to “Complete”)

Receiving SNS Messages and Verifying Their Signature

Using the SNS Message Validator’s Message class, you can easily parse raw POST data from SNS:

<?php

require 'path/to/vendor/autoload.php';

$message = AwsSnsMessage::fromRawPostData();
echo $message->get('Message');

Amazon SNS sends different types of messages, including SubscriptionConfirmation, Notification, and UnsubscribeConfirmation. The formats of these messages are described in the Appendix: Message and JSON Formats section of the Amazon SNS Developer Guide.

Messages from Amazon SNS are signed. As a best practice, you should use the MessageValidator class to verify the signature and ensure a message was sent from Amazon SNS.

<?php

use AwsSnsMessage;
use AwsSnsMessageValidator;

$message = Message::fromRawPostData();

// Validate the message
$validator = new MessageValidator();
$validator->validate($message);

Instances of AwsSnsMessageValidator have two methods for validating messages, both of which take an instance of AwsSnsMessage as their only argument. validate (shown above) will throw an AwsSnsExceptionInvalidSnsMessageException. isValid will return a Boolean — true for valid messages and false for invalid ones.

Confirming a Subscription to a Topic

In order for an HTTP(S) endpoint to receive messages, it must first be subscribed to an SNS topic. Subscriptions are confirmed over HTTP(S), so you’ll need to create and deploy an endpoint before you set up a subscription. SubscriptionConfirmation messages provide a URL you can use to confirm the subscription.

$message = AwsSnsMessage::fromRawPostData();

// Validate the message
$validator = new MessageValidator();
if ($validator->isValid($message)) {
    file_get_contents($message->get('SubscribeURL'));
}

Handling Notifications

Let’s put it all together and add some extra code for handling both notifications and subscription control messages.

<?php

if ('POST' !== $_SERVER['REQUEST_METHOD']) {
    http_response_code(405);
    die;
}

require 'path/to/vendor/autoload.php';

try {
    $message = AwsSnsMessage::fromRawPostData();
    $validator = new AwsSnsMessageValidator();
    $validator->validate($message);

    if (in_array($message['Type'], ['SubscriptionConfirmation', 'UnsubscribeConfirmation']) {
        file_get_contents($message['SubscribeURL']);
    }

    $log = new SplFileObject('../messages.log', 'a');
    $log->fwrite($message['Message'] . "n");
} catch (Exception $e) {
    http_response_code(404);
    die;
}

Conclusion

As you can see, receiving, verifying, and handling Amazon SNS messages is simple. Setting up your application to receive SNS messages will allow you to create applications that can handle asynchronous communication from AWS services and other parts of your application.

AWS Workshop and Hackathon at PNWPHP

by Jeremy Lindblom | on | in PHP | | Comments

In September, the Pacific Northwest PHP Conference (PNWPHP) is happening in Seattle. It’s just down the street from us, and we decided to partner with the them to host an AWS Workshop and Hackathon on September 10th, 2015.

The workshop portion will serve as kind of AWS bootcamp for PHP developers, and will include a few presentations about AWS services and architecture, the AWS SDK for PHP, and running PHP applications on AWS. You can see a full list of the presentations and speakers on the PNWPHP website.

The hackathon portion will allow people to team up and create something using AWS services and the SDK. Like most hackathons, there will be food and prizes involved. Hackathon participants will also receive AWS credits through the AWS Activate program to cover the costs of the services they will be using during the hackathon.

Tickets for AWS Workshop and Hackathon are sold separately from the main PNWPHP conference, so whether you end up attending the main conference or not, you still have the opportunity to join us at our workshop/hackathon. In fact, you can use the discount code "AWSHACK" to get your AWS Workshop and Hackathon ticket for a 50% discount. Head to the PNWPHP registration page to get your ticket.

Whether you are a Seattle native, or you are in town for PNWPHP, we hope to see you at our special AWS Workshop and Hackathon.

Reduce Composer Issues on Elastic Beanstalk

by Jeremy Lindblom | on | in PHP | | Comments

During the past couple of months, we’ve had a few reports from customers where they have experienced PHP application deployment failures on AWS Elastic Beanstalk related to parsing exceptions being thrown by Composer. In case you have recently run into the issue yourself, we would like to briefly describe why it is happening and how you can circumvent the issue.

The issue

The issue occurs when a project or its dependencies expresses its requirements using newer Composer syntax features like the carat (^) operator. For users of the AWS SDK for PHP, the error looks something like this:

[RuntimeException] Could not load package aws/aws-sdk-php in http://packagist.org:
[UnexpectedValueException] Could not parse version constraint ^5.3: Invalid version string "^5.3"

We also observed the issue with some versions of the Laravel framework and a few other libraries. The issue comes up when you are using older Elastic Beanstalk stacks with your applications. The older stacks have an old version of Composer included on the underlying Amazon Machine Image (AMI) that does not support some of the latest Composer features like the carat (^) and new OR (||) syntax.

The solution

There are 3 different ways to solve this issue.

  1. Upgrade your application to use the latest Elastic Beanstalk solution stack. The latest solution stacks for PHP have a more recent version of Composer that supports the new syntax features.
  2. Use Elastic Beanstalk configuration files (.ebextension). You can create a file ending in .config inside your .ebextension directory that allows you to perform a Compose self-update command before installing your dependencies. For example, name the file 01composer.config and add the following configuration:

    commands:
      01updateComposer:
        command: export COMPOSER_HOME=/root && /usr/bin/composer.phar self-update
    
    option_settings:
      - namespace: aws:elasticbeanstalk:application:environment
        option_name: COMPOSER_HOME
        value: /root
  3. Install your dependencies locally. One way to avoid issues with Composer during deployment is to bypass the whole Composer workflow entirely by creating deployments of your application with the dependencies pre-installed.

The conclusion

We hope that this short blog post will be helpful if you happen to run into this issue. If this article does not solve your problem or you are running into other issues, please contact AWS Support or ask for help on the Elastic Beanstalk forum.

AWS SDK for PHP Office Hour

by Jeremy Lindblom | on | in PHP | | Comments

The AWS SDKs and Tools team invites you to the first-ever online office hour hosted by the maintainers of the AWS SDK for PHP. It will be held via Google Hangouts at 10:30-11:30am PDT (UTC -7:00) on Monday 6/29. If you don’t have one already, you will need to create an account with Google to join the video chat.

This first office hour will be driven by customer questions. We expect to focus on questions about the SDK, but any questions related to PHP development on AWS are welcome. We’re excited to meet you and help you be successful in developing PHP applications on AWS!

Please register for the event, add it to your calendar, and join the office hour next Monday.

Updated Framework Modules for V3

by Jeremy Lindblom | on | in PHP | | Comments

Last month, we announced that Version 3 of the AWS SDK for PHP was generally available. We’ve now updated all of our framework-specific modules with releases that support Version 3 (V3) of the SDK. Take a look!

We’ve also updated our AWS Resource APIs for PHP library which we previewed in December. Now that V3 of the SDK is stable, we will be working on adding features and documentation to this library over the coming weeks.

As always, we appreciate your feedback on any of our open source packages. Check out these updates and let us know what you think. :-)

P.S. We’d like to give a special thanks to Graham Campbell and Michaël Gallego for their contributions to the Laravel and ZF2 packages, respectively.