{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Web Application using Spot Fleet\n", "\n", "In this workshop we will extend the basics of AWS EC2 we learned from the `intro_to_aws` workshop by utilizing EC2 Spot Fleet instances for hosting the web application. Python is used extensively so you will need experience in or be comfortable reading python code. \n", "\n", "\n", "### Initialize notebook\n", "\n", "We will be using the [boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) library for creation of all resources." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import boto3\n", "import sys\n", "import os\n", "import json\n", "import base64\n", "import project_path\n", "import time\n", "\n", "from lib import workshop\n", "from botocore.exceptions import ClientError\n", "\n", "ec2_client = boto3.client('ec2')\n", "alb_client = boto3.client(\"elbv2\")\n", "ssm = boto3.client('ssm')\n", "iam = boto3.client('iam')\n", "cwe = boto3.client('events')\n", "aa = boto3.client('application-autoscaling')\n", "lambda_client = boto3.client('lambda')\n", "\n", "session = boto3.session.Session()\n", "region = session.region_name\n", "account_id = boto3.client('sts').get_caller_identity().get('Account')\n", "\n", "workshop_user = 'spot' # no capitals all lower case\n", "alb_name = 'alb-web-{0}'.format(workshop_user)\n", "alb_sec_group_name = 'alb-sg-{0}'.format(workshop_user)\n", "alb_target_group_name = 'alb-target-group-{0}'.format(workshop_user)\n", "auto_scaling_group_name = 'web-asg-{0}'.format(workshop_user)\n", "scale_up_name = 'scale_up_{0}'.format(workshop_user)\n", "scale_down_name = 'scale_down_{0}'.format(workshop_user)\n", "launch_template = 'webserver-lt-{0}'.format(workshop_user)\n", "root_volume_size = 2000\n", "\n", "use_existing = True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create VPC](https://aws.amazon.com/vpc/)\n", "\n", "Amazon Virtual Private Cloud (Amazon VPC) lets you provision a logically isolated section of the AWS Cloud where you can launch AWS resources in a virtual network that you define. You have complete control over your virtual networking environment, including selection of your own IP address range, creation of subnets, and configuration of route tables and network gateways. You can use both IPv4 and IPv6 in your VPC for secure and easy access to resources and applications." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "if use_existing:\n", " vpc_filter = [{'Name':'isDefault', 'Values':['true']}]\n", " default_vpc = ec2_client.describe_vpcs(Filters=vpc_filter)\n", " vpc_id = default_vpc['Vpcs'][0]['VpcId']\n", "\n", " subnet_filter = [{'Name':'vpc-id', 'Values':[vpc_id]}]\n", " subnets = ec2_client.describe_subnets(Filters=subnet_filter)\n", " subnet1_id = subnets['Subnets'][0]['SubnetId']\n", " subnet2_id = subnets['Subnets'][1]['SubnetId']\n", "else: \n", " vpc, subnet1, subnet2 = workshop.create_and_configure_vpc()\n", " vpc_id = vpc.id\n", " subnet1_id = subnet1.id\n", " subnet2_id = subnet2.id" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(vpc_id)\n", "print(subnet1_id)\n", "print(subnet2_id)\n", "print(region)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create Security Groups](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html)\n", "\n", "\n", "A security group acts as a virtual firewall for your instance to control inbound and outbound traffic. When you launch an instance in a VPC, you can assign up to five security groups to the instance. Security groups act at the instance level, not the subnet level. Therefore, each instance in a subnet in your VPC could be assigned to a different set of security groups. If you don't specify a particular group at launch time, the instance is automatically assigned to the default security group for the VPC.\n", "\n", "[ec2_client.create_security_group](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.create_security_group) boto3 documentation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sg = ec2_client.create_security_group(\n", " Description='security group for ALB',\n", " GroupName=alb_sec_group_name,\n", " VpcId=vpc_id\n", ")\n", "alb_sec_group_id=sg[\"GroupId\"]\n", "print('ALB security group id - ' + alb_sec_group_id)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Configure available ports\n", "\n", "In order for the ALB to communicate with the outside world, we will open port 80. As you can see in the call below we can define the `ToPort` and `FromPort` and a `CidrIp` range we want to allow.\n", "\n", "[ec2_client.authorize_security_group_ingress](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.authorize_security_group_ingress) boto3 documentation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data = ec2_client.authorize_security_group_ingress(\n", " GroupId=alb_sec_group_id,\n", " IpPermissions=[\n", " {'IpProtocol': 'tcp',\n", " 'FromPort': 80,\n", " 'ToPort': 80,\n", " 'IpRanges': [\n", " {\n", " 'CidrIp': '0.0.0.0/0',\n", " 'Description': 'HTTP access'\n", " },\n", " ]\n", " }\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create Application Load Balancer (ALB)](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html)\n", "\n", "\n", "Elastic Load Balancing supports three types of load balancers: Application Load Balancers, Network Load Balancers, and Classic Load Balancers. In this example we will be using an [Application Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html). For more information about Network Load Balancers, see the [User Guide for Network Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/). For more information about Classic Load Balancers, see the [User Guide for Classic Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/).\n", "\n", "[elbv2.create_load_balancer](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elbv2.html#ElasticLoadBalancingv2.Client.create_load_balancer)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sn_all = ec2_client.describe_subnets()\n", "alb_subnets = []\n", "\n", "for sn in sn_all['Subnets'] :\n", " if sn['MapPublicIpOnLaunch'] and vpc_id == sn['VpcId']:\n", " alb_subnets.append(sn['SubnetId'])\n", " \n", "print(alb_subnets)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "alb = alb_client.create_load_balancer(\n", " Name=alb_name,\n", " Subnets=alb_subnets,\n", " SecurityGroups=[\n", " alb_sec_group_id,\n", " ],\n", " Scheme='internet-facing',\n", " Type='application',\n", " IpAddressType='ipv4'\n", ")\n", "\n", "alb_arn = alb[\"LoadBalancers\"][0][\"LoadBalancerArn\"]\n", "alb_name = alb[\"LoadBalancers\"][0][\"LoadBalancerName\"]\n", "print(alb_arn)\n", "print(alb_name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create Target Group](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html)\n", "\n", "Each target group is used to route requests to one or more registered targets. When you create each listener rule, you specify a target group and conditions. When a rule condition is met, traffic is forwarded to the corresponding target group. You can create different target groups for different types of requests. For example, create one target group for general requests and other target groups for requests to the microservices for your application.\n", "\n", "[elbv2.create_target_group](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elbv2.html#ElasticLoadBalancingv2.Client.create_target_group) boto3 documentation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "target_group = alb_client.create_target_group(\n", " Name=alb_target_group_name,\n", " Protocol='HTTP',\n", " Port=80,\n", " VpcId=vpc_id,\n", " HealthCheckProtocol='HTTP',\n", " HealthCheckPort='80',\n", " HealthCheckPath='/'\n", ")\n", "\n", "target_group_arn = target_group[\"TargetGroups\"][0][\"TargetGroupArn\"]\n", "print(target_group_arn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create Listener](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html)\n", "\n", "Before you start using your Application Load Balancer, you must add one or more listeners. A listener is a process that checks for connection requests, using the protocol and port that you configure. The rules that you define for a listener determine how the load balancer routes requests to the targets in one or more target groups.\n", "\n", "[elbv2.create_listener](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elbv2.html#ElasticLoadBalancingv2.Client.create_listener) boto3 documentation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "listener = alb_client.create_listener(\n", " DefaultActions=[\n", " {'TargetGroupArn': target_group_arn,\n", " 'Type': 'forward'\n", " }],\n", " LoadBalancerArn=alb_arn,\n", " Port=80,\n", " Protocol='HTTP'\n", ")\n", "\n", "listener_arn = listener[\"Listeners\"][0][\"ListenerArn\"]\n", "print(listener_arn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Get Latest [Amazon Linux AMI](https://aws.amazon.com/amazon-linux-ami/)\n", "\n", "The Amazon Linux 2 AMI is a supported and maintained Linux image provided by Amazon Web Services for use on Amazon Elastic Compute Cloud (Amazon EC2). It is designed to provide a stable, secure, and high performance execution environment for applications running on Amazon EC2. It supports the latest EC2 instance type features and includes packages that enable easy integration with AWS. Amazon Web Services provides ongoing security and maintenance updates to all instances running the Amazon Linux AMI. The Amazon Linux AMI is provided at no additional charge to Amazon EC2 users. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = ssm.get_parameters(Names=['/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'])\n", "ami = response['Parameters'][0]['Value']\n", "print(ami)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create Spot Fleet IAM Role" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "role_doc = {\n", " \"Version\": \"2012-10-17\", \n", " \"Statement\": [\n", " {\"Sid\": \"\", \n", " \"Effect\": \"Allow\", \n", " \"Principal\": {\n", " \"Service\": \"spotfleet.amazonaws.com\"\n", " }, \n", " \"Action\": \"sts:AssumeRole\"\n", " }]\n", " }\n", "\n", "role_name = 'WorkshopSpotFleetRole-{0}'.format(workshop_user)\n", "spot_fleet_role_arn = workshop.create_role(iam=iam, policy_name=role_name, \\\n", " assume_role_policy_document=json.dumps(role_doc))\n", "\n", "iam.attach_role_policy(RoleName=role_name, PolicyArn='arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole')\n", "print(spot_fleet_role_arn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create UserData to install Apache web server and download index\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile userdata.sh\n", "#!/bin/bash\n", "yum -y update\n", "yum -y install httpd\n", "chkconfig httpd on\n", "instanceid=$(curl http://169.254.169.254/latest/meta-data/instance-id)\n", "echo \"hello spot workshop from $instanceid\" > /var/www/html/index.html\n", "service httpd start" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Load userdata.sh\n", "\n", "We will read the UserData into a local variable and base64 encode the contents of the file to be used on the EC2 instance launch configuraton." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fh=open(\"userdata.sh\")\n", "userdata=fh.read()\n", "fh.close()\n", "\n", "userdataencode = base64.b64encode(userdata.encode()).decode(\"ascii\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create Instance Profile for EC2 instances](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html)\n", "\n", "An instance profile is a container for an IAM role that you can use to pass role information to an EC2 instance when the instance starts.\n", "\n", "[iam.create_instance_profile](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html#IAM.Client.create_instance_profile)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "instance_profile_name = 'SpotSessionManagerAccessRole-{0}'.format(workshop_user)\n", "\n", "response = iam.create_instance_profile(\n", " InstanceProfileName=instance_profile_name\n", ")\n", "\n", "instance_profile_arn = response['InstanceProfile']['Arn']\n", "print(instance_profile_arn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Add the managed Role `SessionManagerAccessRole` to allow Session Manager to gain shell access to the EC2 instances for troubleshooting." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "iam.add_role_to_instance_profile(\n", " InstanceProfileName=instance_profile_name,\n", " RoleName='SessionManagerAccessRole'\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create Launch Template](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-templates.html)\n", "\n", "You can create a launch template that contains the configuration information to launch an instance. Launch templates enable you to store launch parameters so that you do not have to specify them every time you launch an instance. \n", "\n", "[ec2_client.create_launch_template](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.create_launch_template)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = ec2_client.create_launch_template(\n", " LaunchTemplateName=launch_template,\n", " LaunchTemplateData={\n", " 'IamInstanceProfile': {\n", " 'Arn': instance_profile_arn,\n", " },\n", " 'BlockDeviceMappings': [\n", " {\n", " 'DeviceName': '/dev/xvda',\n", " 'Ebs': {\n", " 'DeleteOnTermination': True,\n", " 'VolumeSize': root_volume_size,\n", " 'VolumeType': 'gp2'\n", " }\n", " }\n", " ],\n", " 'UserData': userdataencode,\n", " 'ImageId': ami,\n", " 'SecurityGroupIds': [\n", " alb_sec_group_id,\n", " ],\n", " }\n", ")\n", "\n", "launch_template_id = response['LaunchTemplate']['LaunchTemplateId']\n", "print(launch_template_id)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create EC2 Spot Fleet](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet.html)\n", "\n", "A Spot Fleet is a collection, or fleet, of Spot Instances, and optionally On-Demand Instances. The request for Spot Instances is fulfilled if the maximum price you specified in the request exceeds the current Spot price and there is available capacity.\n", " \n", "You can set the `SpotPrice` on the `SpotFleetRequestConfig` to lower how much you are willing to pay, but by default it will use the `On-Demand` price of the instance type selected and you will only pay for the current going market rate of the instance.\n", "\n", "[ec2_client.request_spot_fleet](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.request_spot_fleet)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = ec2_client.request_spot_fleet(\n", " SpotFleetRequestConfig={\n", " 'AllocationStrategy': 'lowestPrice',\n", " 'OnDemandAllocationStrategy': 'lowestPrice',\n", " 'IamFleetRole': spot_fleet_role_arn, \n", " 'LaunchTemplateConfigs': [\n", " {\n", " 'LaunchTemplateSpecification': {\n", " 'LaunchTemplateId': launch_template_id,\n", " 'Version': '1'\n", " },\n", " 'Overrides': [\n", " {\n", " 'InstanceType': 't3.micro'\n", " },\n", " ]\n", " },\n", " ],\n", " 'TargetCapacity': 1,\n", " 'TerminateInstancesWithExpiration': True,\n", " 'Type': 'maintain',\n", " 'ReplaceUnhealthyInstances': True,\n", " 'InstanceInterruptionBehavior': 'terminate',\n", " 'LoadBalancersConfig': {\n", " 'TargetGroupsConfig': {\n", " 'TargetGroups': [\n", " {\n", " 'Arn': target_group_arn\n", " }\n", " ]\n", " }\n", " }\n", " }\n", ")\n", "\n", "spot_request_id = response['SpotFleetRequestId']\n", "print(spot_request_id)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Wait for Web servers to start\n", "\n", "Once the spot request has been fulfilled you will be able to select the spot fleet request and view the savings tab like below:\n", "\n", "![Spot Savings](../../docs/assets/images/spot-savings.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Register target for application autoscaling](https://docs.aws.amazon.com/autoscaling/application/APIReference/API_RegisterScalableTarget.html)\n", "\n", "Registers or updates a scalable target. A scalable target is a resource that Application Auto Scaling can scale out and scale in. Each scalable target has a resource ID, scalable dimension, and namespace, as well as values for minimum and maximum capacity.\n", "\n", "[aa.register_scalable_target](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/application-autoscaling.html#ApplicationAutoScaling.Client.register_scalable_target)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "resource_id = 'spot-fleet-request/{0}'.format(spot_request_id)\n", "\n", "response = aa.register_scalable_target(\n", " ServiceNamespace='ec2',\n", " ResourceId=resource_id,\n", " ScalableDimension='ec2:spot-fleet-request:TargetCapacity',\n", " MinCapacity=1,\n", " MaxCapacity=3\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Application Autoscaling with Target Tracking](https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-target-tracking.html)\n", "\n", "With target tracking scaling policies, you choose a scaling metric and set a target value. Application Auto Scaling creates and manages the CloudWatch alarms that trigger the scaling policy and calculates the scaling adjustment based on the metric and the target value. The scaling policy adds or removes capacity as required to keep the metric at, or close to, the specified target value. In addition to keeping the metric close to the target value, a target tracking scaling policy also adjusts to changes in the metric due to a changing load pattern.\n", "\n", "[aa.put_scaling_policy](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/application-autoscaling.html#ApplicationAutoScaling.Client.put_scaling_policy)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = aa.put_scaling_policy(\n", " PolicyName='Scale fleet to target',\n", " ServiceNamespace='ec2',\n", " ResourceId= resource_id,\n", " ScalableDimension='ec2:spot-fleet-request:TargetCapacity',\n", " PolicyType='TargetTrackingScaling',\n", " TargetTrackingScalingPolicyConfiguration={\n", " 'TargetValue': 50,\n", " 'PredefinedMetricSpecification': {\n", " 'PredefinedMetricType': 'EC2SpotFleetRequestAverageCPUUtilization'\n", " },\n", " 'ScaleOutCooldown': 10,\n", " 'DisableScaleIn': False\n", " }\n", ")\n", "\n", "print(response['PolicyARN'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Spot Pricing History](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances-history.html)\n", "\n", "Let's take a look at the spot price history available to you to beter understand the cost savings and availability of the instances. Once the page is loaded click on the `Pricing History` button to review the spot pricing history for the `t3.micro` instances." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('https://{0}.console.aws.amazon.com/ec2sp/v1/spot/home?region={0}#'.format(region))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### View Target Group\n", "\n", "The `Target Group` for the spot fleet request will be used with the ALB. We will be waiting for the instances to get into a `healthy` state and readily available to serve up the index page." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('https://{0}.console.aws.amazon.com/ec2/v2/home?region={0}#TargetGroups:sort=targetGroupName'.format(region))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### View Web Application\n", "\n", "If all has gone well to this point you should be able to view the ALB and see the request being routed to different spot instances behind the load balancer." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('http://{0}'.format(alb['LoadBalancers'][0]['DNSName']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Spot Instance Interruption Handler](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html)\n", "\n", "Demand for Spot Instances can vary significantly from moment to moment, and the availability of Spot Instances can also vary significantly depending on how many unused EC2 instances are available. It is always possible that your Spot Instance might be interrupted. Therefore, you must ensure that your application is prepared for a Spot Instance interruption.\n", "\n", "We will create a Lambda function that will be triggered from an CloudWatch Event rule on EC2 state-change notifications. The `event` form CloudWatch will contain the `instance-id` and the `instance-action` and you can decide what intervention is needed if any. The example code below logs typically calls to get the details of the EC2 instance, an example of deregistering with the alb if necessary, and logs the details trhoughout. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile spothandler.py\n", "import boto3\n", "\n", "def handler(event, context):\n", " print(event['detail'])\n", " instanceId = event['detail']['instance-id']\n", " instanceAction = event['detail']['instance-action']\n", " \n", " # Here you could get the instance details, send an SSM command, etc. to shutdown or hibernate the instance\n", " print(\"Interrupting instance:\") \n", " print(\"{0}, {1}\".format(instanceId, instanceAction))\n", " return" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Zip Lambda function for Spot notifications" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%bash\n", "zip spot-notify.zip -r6 spothandler.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create the Lamba function to trigger the orchestration\n", "\n", "[lambda_client.create_function](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Client.create_function)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "spot_interrupt_fn_name = \"Spot-Notify-{0}\".format(workshop_user)\n", "lambda_role_arn = 'arn:aws:iam::{0}:role/lambda_basic_execution'.format(account_id)\n", "\n", "response = lambda_client.create_function(\n", " FunctionName=spot_interrupt_fn_name,\n", " Runtime='python2.7',\n", " Role=lambda_role_arn,\n", " Handler=\"spothandler.handler\",\n", " Code={'ZipFile': open(\"spot-notify.zip\", 'rb').read(), },\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "lambda_arn = response['FunctionArn']\n", "print(lambda_arn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Create CloudWatch Rule](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html)\n", "\n", "Amazon CloudWatch Events delivers a near real-time stream of system events that describe changes in Amazon Web Services (AWS) resources. In this instance we will trigger an event based on EC2 state changes.\n", "\n", "[cwe.put_rule](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/events.html#CloudWatchEvents.Client.put_rule)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "spot_rule = 'Spot-Notification-{0}'.format(workshop_user)\n", "response= cwe.put_rule(\n", " Name=spot_rule,\n", " EventPattern=json.dumps(\n", " {\n", " \"source\": [\"aws.ec2\"],\n", " \"detail-type\": [\"EC2 Spot Instance Interruption Warning\"]\n", " }),\n", " State='ENABLED',\n", " Description='EC2 Spot Event Change Rule'\n", ")\n", " \n", "rule_arn = response['RuleArn']\n", "print(rule_arn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Add Permission to Lambda function](https://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html)\n", "\n", "Grants an AWS service or another account permission to use a function. You can apply the policy at the function level, or specify a qualifier to restrict access to a single version or alias. In this example, we will be granting permission to CloudWatch events to trigger the lambda function." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "lambda_client.add_permission(\n", " FunctionName=spot_interrupt_fn_name,\n", " StatementId=\"{0}-Event\".format(spot_interrupt_fn_name),\n", " Action='lambda:InvokeFunction',\n", " Principal='events.amazonaws.com',\n", " SourceArn=rule_arn,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create Target for Rule\n", "\n", "Targets need to be configured when a CloudWatch rule is raised. Here we will add the Lambda function that handles the Spot interruptions as the target for the rule to allow you to act on the instance before termination.\n", "\n", "[cwe.put_targets](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/events.html#CloudWatchEvents.Client.put_targets)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = cwe.put_targets(\n", " Rule=spot_rule,\n", " Targets=[\n", " {\n", " 'Id': spot_rule,\n", " 'Arn': lambda_arn,\n", " }\n", " ]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### View Lambda function for Spot interruptions\n", "\n", "![Lambda Events](../../docs/assets/images/lambda-designer.png)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('https://{0}.console.aws.amazon.com/lambda/home?region={0}#/functions/{1}?tab=graph'.format(region, spot_interrupt_fn_name))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Remove instance from Spot Fleet request\n", "\n", "In the link below, find the splot fleet request created above and check the checkbox for the row.\n", "\n", "![Spot Req](../../docs/assets/images/spot-fleet-req.png)\n", "\n", "---\n", "\n", "Find the `Auto Scaling` tab and click the `Edit` button an change the `Scale capacity between` min from `3` to `2` and click `Save`. Once the spot fleet request is updated it will trigger the CloudWatch rule and trigger the Lambda function.\n", "\n", "---\n", "\n", "![Spot AS](../../docs/assets/images/spot-fleet-as.png)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('https://{0}.console.aws.amazon.com/ec2sp/v1/spot/home?region={0}#'.format(region))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### View CloudWatch logs on Spot Interruption Lambda Function\n", "\n", "On the Lambda function select the `Monitoring` tab and click the `View logs in CloudWatch` button to see the execution of the Lambda function. When the notification is sent to CloudWatch it will look similar to below:\n", "\n", "```json\n", "{\n", " \"version\": \"0\",\n", " \"id\": \"12345678-1234-1234-1234-123456789012\",\n", " \"detail-type\": \"EC2 Spot Instance Interruption Warning\",\n", " \"source\": \"aws.ec2\",\n", " \"account\": \"123456789012\",\n", " \"time\": \"yyyy-mm-ddThh:mm:ssZ\",\n", " \"region\": \"us-east-2\",\n", " \"resources\": [\"arn:aws:ec2:us-east-2:123456789012:instance/i-1234567890abcdef0\"],\n", " \"detail\": {\n", " \"instance-id\": \"i-1234567890abcdef0\",\n", " \"instance-action\": \"action\"\n", " }\n", "}\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('https://{0}.console.aws.amazon.com/lambda/home?region={0}#/functions/{1}?tab=graph'.format(region, spot_interrupt_fn_name))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You have now walked through creating a web application backed by spot instances and what it takes to monitor for spot interruptions. If you are interested in learning more about EC2 spot follow this [link](https://aws.amazon.com/ec2/spot/)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Clean Up\n", "\n", "In order to remove everything created in this workshop you can run the cells below and finally remove the VPC created for this workshop." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = ec2_client.cancel_spot_fleet_requests(\n", " SpotFleetRequestIds=[\n", " spot_request_id,\n", " ],\n", " TerminateInstances=True\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = ec2_client.delete_launch_template(LaunchTemplateId=launch_template_id)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = iam.remove_role_from_instance_profile(\n", " InstanceProfileName=instance_profile_name,\n", " RoleName='SessionManagerAccessRole'\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = iam.delete_instance_profile(\n", " InstanceProfileName=instance_profile_name\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = alb_client.delete_listener(ListenerArn=listener_arn)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = alb_client.delete_target_group(TargetGroupArn=target_group_arn)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = alb_client.delete_load_balancer(LoadBalancerArn=alb_arn)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = iam.detach_role_policy(\n", " RoleName=role_name,\n", " PolicyArn='arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole'\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = iam.delete_role(RoleName=role_name)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = lambda_client.delete_function(FunctionName=spot_interrupt_fn_name)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = cwe.remove_targets(\n", " Rule=spot_rule,\n", " Ids=[\n", " spot_rule,\n", " ],\n", " Force=True\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = cwe.delete_rule(\n", " Name=spot_rule,\n", " Force=True\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "response = aa.delete_scaling_policy(\n", " PolicyName='Scale fleet to target',\n", " ServiceNamespace='ec2',\n", " ResourceId=resource_id,\n", " ScalableDimension='ec2:spot-fleet-request:TargetCapacity'\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Wait for EC2 instances to terminate" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "time.sleep(60)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "try:\n", " response = ec2_client.delete_security_group(GroupId=alb_sec_group_id)\n", "except:\n", " print('Spot instances not terminated yet.')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "conda_python3", "language": "python", "name": "conda_python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.5" } }, "nbformat": 4, "nbformat_minor": 2 }