Naveen Vijay

IMHO one of the most important missing pieces in CloudFormation is not having the ability to explicitly specify a SLEEP or WAIT function. It is not that it is totally impossible to the implement the same, except that, it would involve having a hard work around one of which is to inject the wait script via the user data with WaitHandle and WaitConditions on EC2 instances.

"DelayWaitHandle" : {
  "Type" : "AWS::CloudFormation::WaitConditionHandle",
  "Properties" : {
  }
},
"DelayWaitCondition" : {
  "Type" : "AWS::CloudFormation::WaitCondition",
  "DependsOn" : "RemoteDesktopServer",
  "Properties" : {
      "Handle" : { "Ref" : "DelayWaitHandle" },
      "Timeout" : "3600"
  }
},
"RemoteDesktopServer": {
  "Type": "AWS::EC2::Instance",
  "Properties": {
    "ImageId":   "ami-123456",
    "InstanceType":  "t2.micro",
    "SubnetId":  "subnet-12345",
    "UserData" : {
    "Fn::Base64" : {
      "Fn::Join" : ["",
      [
          "<powershell>\n",
          "Start-Sleep -s ", {"Ref" : "DelayTimeSecs" },                                
          "\n",
          "cfn-signal.exe  ",  {"Fn::Base64" : {"Ref" : "DelayWaitHandle"}},
          "\n",
          "</powershell>\n"
      ]
    ]
  }
  }
},
"DependsOn" : "HFMServer"
}

There is absolutely no downside to the approach and also it is easier to bake the wait script inside the CloudFormation template; the problem would arise if you wanted to have the same WAIT functionality to be implemented for the stacks which do not have EC2 instances as part of the stack.

I tried and succeeded using Amazon Lambda for the purpose instead of EC2. To summarize the implementation; a CloudFormation custom resource would call a lambda function and all that the function would do is to respond after 5 minutes [current maximum execution time for lambda function execution time]. Again, if you need to wait longer than 5 minutes, you would use the DependsOn in CloudFormation and cascade the Custom Resource Calling.

WAIT FUNCTION – AWS LAMBDA

exports.handler = function(event, context) { //Respond with a SUCCESS Signal regardless of create/update/delete
     if (event.RequestType == "Delete") {
        sendResponse(event, context, "SUCCESS");
        return;
    }
    setTimeout(function() {
        console.log('Slept');
        Data = {"val1":"val1"}; // Some Dummy Value to be returned back to the CloudFormation Stack
        return sendResponse(event, context, "SUCCESS",Data);
    }, 294000); //slightly less than 5 mins - currently hardcoded but be possible accept dynamically
    
};

//Sends response to the pre-signed S3 URL
function sendResponse(event, context, responseStatus, responseData) {
   var responseBody = JSON.stringify({
        Status: responseStatus,
        Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName,
        PhysicalResourceId: context.logStreamName,
        StackId: event.StackId,
        RequestId: event.RequestId,
        LogicalResourceId: event.LogicalResourceId,
        Data: responseData
    });
    
    console.log("RESPONSE BODY:\n", responseBody);

    var https = require("https");
    var url = require("url");

    var parsedUrl = url.parse(event.ResponseURL);
    var options = {
        hostname: parsedUrl.hostname,
        port: 443,
        path: parsedUrl.path,
        method: "PUT",
        headers: {
            "content-type": "",
            "content-length": responseBody.length
        }
    };

    var request = https.request(options, function(response) {
        console.log("STATUS: " + response.statusCode);
        console.log("HEADERS: " + JSON.stringify(response.headers));
        // Tell AWS Lambda that the function execution is done  
        context.succeed('Stopped');
    });

    request.on("error", function(error) {
        console.log("sendResponse Error:\n", error);
        // Tell AWS Lambda that the function execution is done  
        context.done();
    });

    // write data to request body
    request.write(responseBody);
    request.end();
}

Below is the sample Code where you create a VPC and a Security Group after waiting a 5 minute delay time and 10 minute delay.

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "Lambda Delay",
  "Parameters": {
    "SubnetAZ": {
            "Description": "CIDR for AZ",
            "Type": "AWS::EC2::AvailabilityZone::Name",
            "Default": "us-east-1d"
    }
  },
  "Mappings" : {
    "CIDRMappings" : {
        "VPC" : { "cidr" : "10.5.0.0/16" }
    }
  },
  "Resources": {
  "VPC": {
        "Type": "AWS::EC2::VPC",
        "Properties": {
          "CidrBlock": "10.5.0.0/16",
          "Tags": [
             {"Key": "Name", "Value": "Delay Test VPC" }
          ]
        }
      },      
      "DelayLambdaFunction1" : {
      "Type" : "Custom::DelayLambdaFunction1",
      "DependsOn" : "VPC",
      "Properties" : {
         "ServiceToken" : "arn:aws:lambda:us-east-1:123456789:function:DelayFunction"
        }
      },
      "Delay5MinsSG": {
      "Type": "AWS::EC2::SecurityGroup",
      "DependsOn" : "VPC",
      "Properties": {
        "GroupDescription": "After 5 SG 1",
        "VpcId": {
          "Ref": "VPC"
        },
        "SecurityGroupIngress": [
          {
            "IpProtocol": "-1",
            "CidrIp": 
            {
              "Fn::FindInMap": [ "CIDRMappings", "VPC", "cidr" ]
            }
          }
        ],
        "SecurityGroupEgress": [
          {
            "IpProtocol": "-1",
            "CidrIp": "0.0.0.0/0"
          }
        ],
        "Tags": [
          {
            "Key": "Name",
            "Value": "Delay5MinsSG"
          },
          {"Key":"Lambda", "Value":{ "Fn::GetAtt" : ["DelayLambdaFunction1", "val1"]}}
        ]
      }
    },
    "DelayLambdaFunction2a" : {
      "Type" : "Custom::DelayLambdaFunction2a",
      "DependsOn" : "DelayLambdaFunction1",
      "Properties" : {
         "ServiceToken" : "arn:aws:lambda:us-east-1:123456789:function:DelayFunction"
        }
      },
      "DelayLambdaFunction2b" : {
      "Type" : "Custom::DelayLambdaFunction2b",
      "DependsOn" : "DelayLambdaFunction2a",
      "Properties" : {
         "ServiceToken" : "arn:aws:lambda:us-east-1:123456789:function:DelayFunction"
        }
      },
      "Delay10MinsSG": {
      "Type": "AWS::EC2::SecurityGroup",
      "DependsOn" : "Delay5MinsSG",
      "Properties": {
        "GroupDescription": "Delay10MinsSG",
        "VpcId": {
          "Ref": "VPC"
        },
        "SecurityGroupIngress": [
          {
            "IpProtocol": "-1",
            "CidrIp": 
            {
              "Fn::FindInMap": [ "CIDRMappings", "VPC", "cidr" ]
            }
          }
        ],
        "SecurityGroupEgress": [
          {
            "IpProtocol": "-1",
            "CidrIp": "0.0.0.0/0"
          }
        ],
        "Tags": [
          {
            "Key": "Name",
            "Value": "Delay10MinsSG"
          },
          {"Key":"Lambda", "Value":{ "Fn::Join" : [ " ", [{ "Fn::GetAtt" : ["DelayLambdaFunction2a", "val1"]}, { "Fn::GetAtt" : ["DelayLambdaFunction2b", "val1"]} ] ]  }}
        ]
      }
    }
  }
}

cloudformation sleep function screenshot

comments powered by Disqus