This solution consisted of a Route53 hosted zone with A-records directing traffic to an AWS EIP (Elastic IP Address) hosted on a firewall appliance (Fortinet). The firewall had a VIP rule to forward requests received on that EIP to the Apache reverse proxy.
A regular activity I had to perform for one of my clients was setting up apache reverse proxy services for their development environments. This involved defining a subdomain name for an application, creating an A-record on AWS Route53 and then configuring the rewrite rules which varied per application type. Lets look at how it was automated.
Method
To achieve the automation flow we needed to understand each component in the process and how we could automate it. First up was the AWS Route53 hosted zone, this should be easy using the AWS cli. Second was the firewall, as the method used would create a new child domain to the existing domain the current firewall VIP rule was fine, no change there. Lastly was the RHEL Apache server which could be updated with some bash code by adding a new rewrite rule. All of this could be kicked off from a gocd pipeline with a few variables.
Considerations
As the code would be interacting with AWS resources (Route53 in this case), it would be advisable to introduce an IAM role to grant the server access to update the Route53 rules. The gocd agent server could be used by multiple teams so I decided to assign the roles to the Apache proxy server which was in my teams support alongside the AWS infrastructure. This did mean I needed to workout how to pass environment variables from the gocd server to the apache server. Once the variables needed by the code were available on the apache server the processing could all be performed there.
Process Flow
This is what the processing would look like
Setting the scene
A number of activities had to be performed to prepare the environment for this type of automation activity. The apache server was configured to use a single httpd.conf file with all the rules within. This would be a pain to append a new rule each time. As such I changed the server to use individual rewrite rule conf files located in the /etc/httpd/conf.d folder. Each rewrite rule had its own file. This made the process more granular and if and when I made a remove rule process it would be less risky.
In addition to the above, I also had to define some limitations on the rewrite rules being created. There were three distinct application rewrite configurations so the process would have to include an application type variable. If it did not fit in the agreed application types then it would not execute. These three types could then be defined as templates within the code and would also used in working out the child domain name and suffix (numeric value)
AWS Roles
A role was created to allow the apache server access to manage AWS Route 53. This was attached to the ec2 instance hosting the apache service.
I defined a new key pair on the apache server for the automation activity which could be passed to the deployment teams for their pipelines.
The new key pair was generated using ssh-keygen. The public key generated was then added to the machines authorized_keys file and the private key used in the pipeline.
Generate a public and private key pair.
Once we have our key pair we need to copy the public part (filename.pub) into the ~/.ssh/authorized_keys file
In the above screenshot you can see the default public key for the user and the newly generated example key. The private part of the key can now be passed to the deployment team for them to use in their pipeline.
You will need to ensure that the automation account has sufficient file level permissions to update apache and store any backup etc. generated by the script.
Passing environment variables
As mentioned I wanted to pass variables from the gocd pipeline server to the apache server. Reason being that any variables defined in the pipeline would create environment variables on the gocd agent. This would then pass them over to the apache server at execution. The script would then execute on the apache server using these values. After a bit of head scratching I came across SendEnv which did this very thing!
SendEnv
SendEnv is one of the many ssh options. The manual defines it as ;
That makes it so clear!
So to set this up I had to update the sending server (gocd agent) /etc/ssh/ssh_config and the receiving server (apache) /etc/ssh/sshd_config
/etc/ssh/ssh_config
1
SendEnv var1 var2 var3
/etc/ssh/sshd_config
1
AcceptEnv var1 var2 var3
So any environment variables defined in the pipeline following the names could be passed across
And then the task configuration
All environment variables prefixed with DEMO_URL_ will be passed to the ssh target
Script
So lets take a look at the shell script. Similar to my previous scripts we break it down into functions. I will also include remarks to elaborate on some of the code. Not everyone will want or need the remarks so there will be a link at the end to the public GitHub repo.
Declarations
This is where we reassign the environment variables sent from the gocd agent using SendEnv. We also perform some concatenation of values which we will use later in the script. Note that my environment was pretty static so I did hardcode a number of the AWS parameters used for Route 53. All of these could be defined as pipeline variables.
#!/bin/bash
#Local declarations based on Env variables passed from gocd agent server
sudo application=${DEMO_URL_APPLICATION,,}envcode=${DEMO_URL_ENVCODE,,}internalhostname=${DEMO_URL_INTERNALHOSTNAME,,}internalport=$DEMO_URL_INTERNALPORTtype="https"##### Calculated variables####################ProxyPass=$type":"$internalhostname":"$internalport"/"ProxyPassReverse=$type":"$internalhostname":"$internalport"/"##### start - static variables #####
supp\_apps=("first_app""second_app""third_app")# Used to validate supported rule is being defineddt=`date +%F"-"%H"-"%M`# Details for AWS Route53ip="0.0.0.0"# External IP used for Route53 to redirect traffictype="A"ttl=300hostedzoneid="XXXXXXXXXXXXXXXXX"# Your AWS Hosted Zone iddnssuffix=".example.com"#suffix to add to entrycomment="Programmatically created DNS Entry for "$record" created on "$dtcheckpath="/etc/httpd/conf.d/"initialrecord=$envcode$application
Functions
validate_app
This function ensures the inputs (variables) provided are valid so they process can successfully finish. It first checks if the application type is valid as it relies on this to define which rewrite template to use. Secondly it check to ensure the port is valid as far as it being numeric. You could validate this further by defining an allowed port range. Next it will check if there is an environment code and lastly has an internal hostname be provided. All of these inputs are required to deploy the configuration.
function validate_app
{echo"Checking if application name "$application" is valid"if(IFS=$'n';echo"${supp_apps[*]}")| grep -qFx "$application";thenecho"found application "$application" in approved list, continuing"elseecho"error : "$application" was not found in the list of approved applications"echo"Approved applications are : "for eachapp in "${supp_apps[@]}"doecho$eachappdoneecho"error: Please provide an approved application! exiting..."exit1fiecho"Validating port is a number"ncheck='^\[0-9\]+$'if ! [[$internalport=~ $ncheck]];thenecho"error : port provided is not a number! exiting..."exit1elseecho"Port is a number, continuing"fiecho"Checking that an environment has been provided"if[ -z "$envcode"];thenecho"error: No environment code has been provided! exiting..."exit1elseecho"Environment code has been provided, continuing"fiecho"Checking that an internal hostname has been provided"if[ -z "$internalhostname"];thenecho"error: No internal hostname has been provided! exiting..."exit1elseecho"Internal hostname has been provided, continuing"fi}
build_record
As each environment might have multiple deployments of an application, it was necessary to check what was already configured and then programmatically define the next value. This could have been an environment variable but this method removed possible human error.
It works by concatenating the environment name and application type and then starting at 01 it checks if this exists. If there is already a record it increments by 1 until it cannot find an existing record.
The checks are performed against an array which is populated with all of the record sets deployed in the AWS Route53 hosted zone.
As an example if could build out the name;
z1first_app it would add 01 to the end and check for a record z1first_app01. If this does not already exist the value will be used to build out the FQDN otherwise it will increment by one until it finds a free value. This occurs a max. 20 times.
function build_record
{#build the A record based on the provided variables calculating the next available addressfound=""rec_count=0existing_rec_array=()echo"Records to search Route53 : $initialrecord"all_records=(`aws route53 list-resource-record-sets --hosted-zone-id $hostedzoneid --output text --query "ResourceRecordSets[*].Name"`)echo"Route53 has ${#all_records[@]} registered A Records for $dnssuffix"if[${#all_records[@]} -gt 0];then#echo "Identifying existing records with similar prefix for processing"for record in "${all_records[@]}"do#echo $record" is being checked!"if[["$record"=="$initialrecord"]];then#echo $record" already exists"existing_rec_array=($record)found="yes"rec_count=$(($rec_count+1))fidonefi#echo "Found variable set as "$foundif["$found"=""];then#echo "No records found for $initialrecord"record=$initialrecord"01"checkurl=$record$dnssuffixecho"DNS A Record will be created for "$recordelse# There are records following this name, step through and work out available suffixavailable=""available_count=1while[["$available" !="true"&&"$available_count" -le 20]];doprintf -v pad_available_count "%02d"$available_count#build the string to check forcheckurl=$initialrecord$pad_available_count$dnssuffixcheckurlprefix=$initialrecord$pad_available_countcheckurldot=$checkurl"."echo"Checking if "$checkurl" is available"if(IFS=$'\n';echo"${existing_rec_array[*]}")| grep -qFx "$checkurldot";thenecho"found existing A record for "$checkurl" , skipping"elseecho"Did not find any existing A record for "$checkurlecho"We can proceed using "$checkurl" for the new A record"record=$checkurlprefixavailable="true"fiavailable_count=$(($available_count+1))donefi}
check_conf_d
This is a secondary check to ensure there are not any orphaned apache configurations. It is possible a free record could be calculated for Route53 and then an existing configuration file exists. Worth a manual check then.
If an existing conf file is located the pipeline exits.
1
2
3
4
5
6
7
8
9
10
11
12
function check_conf_d
{#ensure there are no existing configuration files with this name. Existing suggests an orphaned file as the names should match a route53 rulecheckfile=$checkpath$checkurlprefix".conf"echo"Checking for existing conf file "$checkfileif[ -f "$checkfile"];thenecho"There is already a config file for this rule. Please advise team to check for orphaned configuration files. Exiting!"exit1elseecho"No existing conf files for "$checkurlprefix" , continuing"fi}
update_route53
When using the AWS cli to add a Route53 A record it uses a JSON-formatted configuration file to provide the required configuration. With this requirement the first part of this function created a temporary .json file and populates it with values from either the environment variables or calculated values from previous steps.
Next the function will backup the existing Route53 records and then use the temporary .json file to create the new A-record. Finally the temporary .json file is removed
function update\_route53
{# First generate a json file with required details as Route53 cli uses this as the input streamTMPFILE=$(mktemp testXXX.json)echo"Temporary file created : "$TMPFILE
cat > ${TMPFILE}<< EOF
{
"Comment":"$comment",
"Changes":\[
{
"Action":"CREATE",
"ResourceRecordSet":{
"Name":"$record$dnssuffix",
"Type":"$type",
"TTL":$ttl,
"ResourceRecords":\[{"Value":"$ip"}\]
}
}
\]
}
EOF#backup the current record sets firstbackfile="/etc/httpd/route53/backup-"$dt".json"echo"creating backup of existing Route53 records : "$backfile
aws route53 list-resource-record-sets --hosted-zone-id $hostedzoneid > $backfile#use the temporary file to create a record on Route53 using the AWS cli
aws route53 change-resource-record-sets --hosted-zone-id $hostedzoneid --change-batch file://$TMPFILE#Remove the temporary file
rm -f $TMPFILE}
build_proxy_rule
This function will build out the .conf file used by the apache server for the rewrite rule. Different rules are defined depending on which application type is being processes. Additional rules can be added in this function if required. The output file is created in the path defined in the checkpath variable (/etc/httpd/conf.d/)
function build_proxy_rule
{echo"creating proxy rule for "$record#create a filenameconf_file=$checkpath$record".conf"if["$application"=="first_app"];then
cat > ${conf_file}<< EOF
<VirtualHost *:443>
ServerName $checkurl
SSLEngine on
SSLProxyEngine on
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLCertificateFile "conf/example-wildcard.crt"
SSLCertificateKeyFile "conf/example-wildcard.key"
SSLProtocol all -SSLv3 -SSLv2 -TLSv1
SSLCipherSuite ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256
RewriteEngine On
ProxyPass / $ProxyPass
ProxyPassReverse / $ProxyPassReverse
</VirtualHost>
EOFelif["$application"=="second_app"];then
cat > ${conf_file}<< EOF
<VirtualHost *:443>
ServerName $checkurl
SSLEngine on
SSLProxyEngine on
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLCertificateFile "conf/example-wildcard.crt"
SSLCertificateKeyFile "conf/example-wildcard.key"
SSLProtocol all -SSLv3 -SSLv2 -TLSv1
SSLCipherSuite ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256
RewriteEngine On
ProxyPass / $ProxyPass
ProxyPassReverse / $ProxyPassReverse
</VirtualHost>
EOFelif["$application"=="third_app"];then
cat > ${conf_file}<< EOF
<VirtualHost *:443>
ServerName $checkurl
SSLEngine on
SSLProxyEngine on
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLCertificateFile "conf/example-wildcard.crt"
SSLCertificateKeyFile "conf/example-wildcard.key"
SSLProtocol all -SSLv3 -SSLv2 -TLSv1
SSLCipherSuite ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256
RewriteEngine On
RewriteCond %{REQUEST\_METHOD} ^(TRACE|TRACK)
RewriteRule .\* - \[F\]
RewriteCond %{HTTP\_HOST} !^$record\\.example\\.com \[NC\]
RewriteRule ^/(.\*)https://$record$dnssuffix/\\$1 \[L,R\]
ProxyPreserveHost on
ProxyPass / $ProxyPass
ProxyPassReverse / $ProxyPassReverse
</VirtualHost>
EOFelseecho"No proxy build process defined yet for this application. We should never see this message unless the validation has failed!"fi}
restart_service
This function will restart the httpd service on the apache server. Apache servers only load their configuration when the service starts.
If you are working in a production environment you would be scheduling this to run out of hours either as a separate task in your pipeline or manually agreed in a downtime window. Mine is development so no issues running at pipeline execution.
1
2
3
4
5
6
7
8
9
10
function restart_service
{
echo "Restarting the httpd service"
sudo httpd -k restart
echo "******************************************************************"
echo ""
echo "External URL created : "$checkurl
echo ""
echo "******************************************************************"
}
Main
Not really a main function as such but just the order of function execution. Located at the bottom of the script.
So what have we achieved with this process. By defining a handful of variables in a pipelines we can calculate a subdomain name, backup AWS Route53 records, create a new A record, create an apache rewrite rule and restart the apache service.
Mission accomplished!
I enjoyed developing this process as I had to think outside the box for a number of requirements. This also meant I had to do a fair bit of infrastructure reconfiguring before it was viable as the apache server was using a single httpd.conf file.
As usual, here is a link to my GitHub repo containing the source code. Feel free to ask any questions or suggest any improvements.