Web Programming, Linux System Administation, and Entrepreneurship in Athens Georgia

Category: PHP (Page 1 of 3)

AWS CodeDeploy Troubleshooting

CodeDeploy with AutoScalingGroups is a bit of a complex mess to get working correctly. Especially with an app that has been working and needs to be updated for more modern functionality

Update the startups scripts with the latest versions from https://github.com/aws-samples/aws-codedeploy-samples/tree/master/load-balancing/elb-v2

I found even the latest scripts there still not working. My instances were starting up then dying shortly afterward. CodeDeploy was failing with the error


LifecycleEvent - ApplicationStart
Script - /deploy/scripts/4_application_start.sh
Script - /deploy/scripts/register_with_elb.sh
[stderr]Running AWS CLI with region:
[stderr][FATAL] Unable to get this instance's ID; cannot continue.

Upon troubleshooting, I found that common_functions.sh has the get_instance_id() function that was running this curl command to get the instance ID


curl -s http://169.254.169.254/latest/meta-data/instance-id

Running that command by itself while an instance was still running returned nothing, which is why it was failing.

It turns out that newer instances use IMDSv2 by default, and it is required (no longer optional). With that configuration, this curl command will fail. In order to fix, this, I replaced the get_instance_id() function with this version:

# Usage: get_instance_id
#
#   Writes to STDOUT the EC2 instance ID for the local instance. Returns non-zero if the local
#   instance metadata URL is inaccessible.

get_instance_id() {
    TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" -s -f)
    if [ $? -ne 0 ] || [ -z "$TOKEN" ]; then
        echo "[FATAL] Failed to obtain IMDSv2 token; cannot continue." >&2
        return 1
    fi

    INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id -s -f)
    if [ $? -ne 0 ] || [ -z "$INSTANCE_ID" ]; then
        echo "[FATAL] Unable to get this instance's ID; cannot continue." >&2
        return 1
    fi

    echo "$INSTANCE_ID"
    return 0
}

This version uses the IMDSv2 API to get a token and uses that token to get the instance-id

With that code replaced, the application successfully registered with the Target Group and the AutoScaling group works correctly

Alternatively (and for troubleshooting), I was able to make IMDSv2 Optional using the AWS Console, and via CloudFormation with this part of the Launch Template:

Resources:
  MyLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: my-launch-template
      LaunchTemplateData:
        ImageId: ami-1234567890abcdef0
        InstanceType: t4g.micro
        MetadataOptions:
          HttpTokens: optional

Scripts published for calculating Sales Tax in Texas from Stripe Transaction exports

Following up from my previous complaints about Texas collecting back Sales Tax for Saas companies,, I put quite a bit of time into writing some PHP scripts to calculate the Texas Sales Tax due and complete their forms.

Looking through the actual Stripe transaction detail and determining the sales tax due will save our company tens of thousands of dollars from the original estimated figures that our accountant calculated.

I’m releasing some of the PHP scripts that I wrote for this on GitHub in case anybody else may find them useful. They are pretty plain PHP, so hopefully are straightforward enough to follow.

Head on over to https://github.com/bchecketts/stripe-sales-tax-aid if that would be useful for you. Comment below or make Github Issues if you have something to share.

Unexpected Behavior with PHP DateTime::createFromFormat(‘U.u’)

I recently came across what felt like a Bug in PHP and was about to file a bug report. I found a couple of people talking about workarounds, but no explanation about why this “bug” exists and is allowed to persist. Hopefully this post helps to explain this unintuitive behavior.

The problem has to do with the PHP DateTime::createFromFormat() function. The desire here is usually to create a DateTime object that represents a precise point in time, up to 1/100,000th of a second. This code would normally work as expected:


$time = microtime(true);
$dateTimeObj = DateTime::createFromFormat('U.u', $time);
echo $dateTimeObj->format('Y-m-d H:i:s.u')."\n";

However, I was observing a problem that occurred infrequently that said

PHP Fatal error: Uncaught Error: Call to a member function format() on bool

So somehow, despite working the vast majority of the time, the $dateTimeObj would sometimes return a boolean (false) instead of a DateTime object.

After some looking into the failed cases, I found that the time 1696832681.000019 return false, but 1696832681.000119 would work as expected. The difference there is 100 microseconds apart. Clearly 0.000019 is close to zero, and somewhere being rounded down, and unexpectedly causing a problem. The DateTimeImmutable::GetLastErrors function tells me that the error is about “Data missing”


calling createFromFormat('U.u') with
float(1696832681.000049)
Array
(
[warning_count] => 0
[warnings] => Array
(
)

[error_count] => 1
[errors] => Array
(
[10] => Data missing
)

)

In order to understand what is occurring, you need to notice that the second argument for DateTime::createFromFormat is expected to be a string. So when using a float as the second argument, PHP internally converts it to a string first. And converting a high precision float to a string in this case, results it in rounding the float “1696832681.000049” to the string “1696832681”. Thus, the createFromFormat function is complaining about the “Data Missing” because it is expecting to see the period and microsecond portion of the string.

The fix is fortunately, very simple. Simply wrap the float around number_format($float, 6, '.', '') which will return a string representation including the six decimal places intended. It’s not a elegant looking, but it doesn’t suffer from the occasional problem of returning false and having a fatal error!

PHP Sessions with Redis Cluster (using AWS Elasticache)

I’ve recently been moving some of our project from a single Redis server (or server with a replica) to the more modern Redis Cluster configuration. However, when trying to set up PHP sessions to use the cluster, I found there wasn’t a lot of documentation or examples. This serves as a walk-through for setting up PHP sessions to use a redis Cluster, specifically with Elasticache on AWS.

First, create your Elasticache Redis Instance like so. Note the “Cluster Mode Enabled” is what causes redis to operate in Cluster mode.

AWS Elasticache Redis Creation

Once there servers are launched, make note of the Configuration Endpoint which should look something like: my-redis-server.dltwen.clustercfg.usw1.cache.amazonaws.com:6379

Finally, use these settings in your php.ini file. The exact location of this file will depend on your OS, but on modern Ubuntu instances, You can place it in /etc/php/7.0/apache2/conf.d/30-redis-sessions.ini

Note the special syntax for the save_path where is has seed[]=. You only need to put the main cluster configuration endpoint here. Not all of the individual instances as other examples online appear to use.


session.save_handler = rediscluster
session.save_path = "seed[]=my-redis-server.dltwen.clustercfg.usw1.cache.amazonaws.com:6379"
session.gc_maxlifetime = 1296000

That’s it. Restart your webserver and sessions should now get saved to your Redis cluster.

IIn the even that something goes wrong, you might see something like this in your web server log files:


PHP Warning: Unknown: Failed to write session data (redis). Please verify that the current setting of session.save_path is correct (tcp://my-redis-server.dltwen.clustercfg.use1.cache.amazonaws.com:6379) in Unknown on line 0

Getting Ubuntu 14.04 php5enmod to understand module priority

Usage of Debian’s php5enmod module doesn’t seem to be documented anywhere except from the command line when calling it without any arguments:

user@host:~# php5enmod
WARNING:
usage: php5enmod [ -s ALL|sapi_name ] module_name [ module_name_2 ]

Unfortunately, that provides no information on how to customize the priority of a module when enabling it. Some others seem to think that you should be able to provide a priority level on the command line, but that doesn’t work.

It took some digging into the bash scripts to figure out how to make it work. The trick is to add a comment in the .ini file for the module. The comment must contain a very specific format of:


zend_extension = /usr/lib/php5/20121212/ioncube_loader_lin_5.5.so
; priority=1

The ‘priority’ line must be in that format exactly and most not contain any other spaces or characters. The line must start with a semicolon, followed by a space, followed by priority=, and finally the desired priority level. The only space on the line must be between the semicolon and the word ‘priority’.

Fix For Amazon API Error: Your request is missing required parameters. Required parameters include AssociateTag.

Users of Amazon’s Product Advertising API will begin seeing the following error as of about November 1st:

Your request is missing required parameters. Required parameters include AssociateTag.

This is due to changing requirements to the Product Advertising API. The AssociateTag parameter is now a required and validated field. You can see in the list of changes that it is the first thing mentioned.

These changes are made to keep the Product Advertising API in line with its purpose of driving affiliate sales through the Amazon.com website. The AssociateTag is a tag generated from your Amazon Associates account.

So far, it looks like the tag is not entirely validated to your account. I’ve had luck using fictitious tags such as aztag-20.

KnitMeter.com Has Been Upgraded

KnitMeter Logo

KnitMeter Has Been Upgraded

KnitMeter.com was originally started over four years ago in December of 2007 as a small project that my wife thought would be useful. Since then, the site hasn’t changed much, but it has managed to grow to thousands of users who have knit nearly 20 thousand miles of yarn. I’ve received numerous requests and have finally gotten a chance to impliment what many of you have been requesting for a while now. New features on the site include:

  • Users can now add entries for knitting, crocheting, and spinning
  • Completely new and modernized design and logo
  • You can customize your widgets directly on KnitMeter.com rather than editing the code for the widget on your website
  • The website and the KnitMeter Facebook Application are now completely integrated. Entries added in one will be displayed and counted in the other
  • The Facebook application can (again) publish your entries to your news feed, but only when you tell it to
  • You can chose to make your profile public, which will display some of the most recent entries on the KnitMeter home page with a link to your website
  • Added several new timeframes, including specific calendar years (ie: I knit 4.3 miles in 2010)
  • Numerous technical changes that should make the site faster to use and make it easier to make future changes

These new features have been rolled out over the past couple of weeks. I appreciate the patience of those who have dealt with a few bugs over that time, and I believe that everything should be pretty bug-free now. I encourage you to check out the new site and to start adding up the mileage for your own projects. The next major milestone will be when we have gone through enough yarn to go around the earth (about 24,901 miles). At the present rate, we should hit that figure in about 3-5 months.

Happy Knitting, Crocheting, and Spinning,
Brandon Checketts
KnitMeter.com

Website Performance: Tables Versus CSS

Most website designers have been using CSS for page layout for several years now, but I occasionally see some websites that continue to use HTML tables for layout. As I’ve been focusing on website performance lately, I’ve found some references that modern browsers render sites using tables for layout slower than they do sites that use CSS. I decided to investigate and confirmed that there are many possible situations where sites using large tables will appear to load much slower than those using CSS. I put together two pages to confirm:

This page uses <div> elements for layout
and
This pages uses a large table for layout

On both pages I’ve added a 5-second sleep near the end of the page to show what might happen if the server was slow, if there were network problems, or any other number of things may have happened.

Notice that the page created using a table changes a lot after the delay. I’ve tried it in Firefox 3 which extends the main (yellow) content section all of the way to the right until it receives the rest of the document, at which point it has to shrink that part to make room for the section on the right. Internet Explorer behaves even worse. It leaves a blank white page until after the delay, at which point it draws the whole table.

By contrast, the page created with CSS positioning shows all of the content above the delay and has it in the correct position. When the rest of the document is sent it just fills in the appropriate content, but doesn’t have to re-arrange anything on the page.

Script to Import Static Pages into GetSimple CMS

I’ve recently been impressed with a very simple Content Management System called GetSimple. It provides just the very basics that allows a user to edit their own website content. For brochure sites with owners who don’t want the complexity of a larger CMS, I think it is pretty ideal.

When I develop a site though, I typically have a header and footer, and then all of the content pages exist as PHP files that simply include that header and footer. Converting a static site like that into the CMS takes a bunch of copy/pasting. I always try to avoid such tedious jobs, and so developed a script that will import those static pages into a GetSimple installation.

To run this script, I wanted to import a bunch of files in a ‘static’ directory where I had moved all of the static files to. I then ran this from the command line to import all of the content into GetSimple

# for file in `find static -type f`
> do
> ./getsimple_import_file.php $file
> done

The script is available as getsimple_import_file.php

It takes a little configuration before running it. It works by simulating the data that you would submit when creating the page through the web interface, so we have to fake the necessary session cookie. Uncomment the bit in the middle that will display your cookie and run the script once. You’ll need to copy your cookie name and value into the script before doing any actual imports.

Once you’ve done that, you will probably want to change the regular expression that attempts to grab the page title from your file. You may also want to manipulate how it figures the URL to use.

Feel free to post comments here if you found this useful, or made any changes you’d like to share with other users

Enabling HTTP Page Caching with PHP

I’ve been doing a lot of work on BookScouter.com lately to reduce page load time and generally increase the performance of the website for both users and bots. One of the tips that the load time analyzer points out is to enable an expiration time for static content. That is easy enough for images and such by using an Apache directive such as:

    ExpiresActive On
    ExpiresByType image/gif A2592000
    ExpiresByType image/jpg A2592000
    ExpiresByType image/png A2592000

But pages generated with PHP by default have the Pragma: no-cache header set, so that the users’ browsers do not cache the content at all. In most cases, even hitting the back button will generate another request to the server which must be completely processed by the script. You may be able to cache some of the most intensive operations inside your script, but this solution will eliminate that request completely. Simply add this code to the top of any page that contains semi-static content. It effectively sets the page expiration time to one hour in the future. So if a visitor hits the same URL within that hour, the page is served locally from their browser cache instead of making a trip to the server. It also sends an HTTP 304 (Not Modified) response code if the user requests to reload the page within the specified time. That may or may-not be desired based on your site.

$expire_time = 60*60; // One Hour
header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + $expire_time));
header("Cache-Control: max-age={$expire_time}");
header('Last-Modified: '.gmdate('D, d M Y H:i:s \G\M\T', time()));
header('Pragma: public');

if ((!empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) && (time() - strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < = $expire_time)) {
    header('HTTP/1.1 304 Not Modified');
    exit;
}   
« Older posts

© 2025 Brandon Checketts

Theme by Anders NorenUp ↑