Repository: keikoproj/instance-manager
Pull request #199 (2020-11-15 22:58:19)

Launch template placement support
Implementation for #198 
Also contributes to #193 

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/keikoproj/instance-manager?pullRequest=199) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/keikoproj/instance-manager?pullRequest=199) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/keikoproj/instance-manager?pullRequest=199) it.</sub>
codecov[bot]: # [Codecov](https://codecov.io/gh/keikoproj/instance-manager/pull/199?src=pr&el=h1) Report
> Merging [#199](https://codecov.io/gh/keikoproj/instance-manager/pull/199?src=pr&el=desc) (909fdf5) into [master](https://codecov.io/gh/keikoproj/instance-manager/commit/13cd2e8da311a79f6233613d94781cac8bd6291b?el=desc) (13cd2e8) will **decrease** coverage by `1.19%`.
> The diff coverage is `50.00%`.

[![Impacted file tree graph](https://codecov.io/gh/keikoproj/instance-manager/pull/199/graphs/tree.svg?width=650&height=150&src=pr&token=IJbjmSBliL)](https://codecov.io/gh/keikoproj/instance-manager/pull/199?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #199      +/-   ##
==========================================
- Coverage   88.50%   87.30%   -1.20%     
==========================================
  Files          17       17              
  Lines        2096     2151      +55     
==========================================
+ Hits         1855     1878      +23     
- Misses        147      177      +30     
- Partials       94       96       +2     
```


| [Impacted Files](https://codecov.io/gh/keikoproj/instance-manager/pull/199?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [controllers/provisioners/eks/scaling/interface.go](https://codecov.io/gh/keikoproj/instance-manager/pull/199/diff?src=pr&el=tree#diff-Y29udHJvbGxlcnMvcHJvdmlzaW9uZXJzL2Vrcy9zY2FsaW5nL2ludGVyZmFjZS5nbw==) | `0.00% <ø> (ø)` | |
| [controllers/provisioners/eks/helpers.go](https://codecov.io/gh/keikoproj/instance-manager/pull/199/diff?src=pr&el=tree#diff-Y29udHJvbGxlcnMvcHJvdmlzaW9uZXJzL2Vrcy9oZWxwZXJzLmdv) | `89.72% <37.50%> (-0.75%)` | :arrow_down: |
| [...rollers/provisioners/eks/scaling/launchtemplate.go](https://codecov.io/gh/keikoproj/instance-manager/pull/199/diff?src=pr&el=tree#diff-Y29udHJvbGxlcnMvcHJvdmlzaW9uZXJzL2Vrcy9zY2FsaW5nL2xhdW5jaHRlbXBsYXRlLmdv) | `76.14% <46.00%> (-9.65%)` | :arrow_down: |
| [controllers/provisioners/eks/create.go](https://codecov.io/gh/keikoproj/instance-manager/pull/199/diff?src=pr&el=tree#diff-Y29udHJvbGxlcnMvcHJvdmlzaW9uZXJzL2Vrcy9jcmVhdGUuZ28=) | `91.26% <100.00%> (+0.26%)` | :arrow_up: |
| [controllers/provisioners/eks/update.go](https://codecov.io/gh/keikoproj/instance-manager/pull/199/diff?src=pr&el=tree#diff-Y29udHJvbGxlcnMvcHJvdmlzaW9uZXJzL2Vrcy91cGRhdGUuZ28=) | `87.70% <100.00%> (+0.20%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/keikoproj/instance-manager/pull/199?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/keikoproj/instance-manager/pull/199?src=pr&el=footer). Last update [13cd2e8...909fdf5](https://codecov.io/gh/keikoproj/instance-manager/pull/199?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

preflightsiren: <img width="1792" alt="Screen Shot 2020-11-02 at 7 34 36 pm" src="https://user-images.githubusercontent.com/1064728/99200457-8bc1b580-27f9-11eb-9e4e-0262cd5efe77.png">
Tested locally.

Recommendations welcome where we can improve the testing structure
preflightsiren: TODO:
- [ ] Update examples
- [ ] Update changelog
preflightsiren: I've addressed all the feedback and have provided updated docs and tests. Ready for review and final feedback.
eytan-avisror: Great stuff @preflightsiren , very close to merging it, just had few more questions / comments

Patch:
diff --git a/README.md b/README.md
index 5114f65..8f86b87 100644
--- a/README.md
+++ b/README.md
@@ -11,15 +11,14 @@
 
 ![instance-manager](hack/instance-manager.png)
 
-- [Installation](#installation)
-- [Usage Example](#usage-example)
-  - [Currently supported provisioners](#currently-supported-provisioners)
-  - [EKS sample spec](#EKS-sample-spec)
-  - [Submit and Verify](#submit-and-verify)
-  - [Features](#features)
-  - [Alpha-2 Version](#alpha-2-version)
-- [Contributing](#contributing)
-- [Developer Guide](#developer-guide)
+- [instance-manager](#instance-manager)
+  - [Installation](#installation)
+  - [Usage example](#usage-example)
+    - [Currently supported provisioners](#currently-supported-provisioners)
+    - [Submit and Verify](#submit-and-verify)
+    - [Alpha-2 Version](#alpha-2-version)
+  - [Contributing](#contributing)
+  - [Developer Guide](#developer-guide)
 
 Worker nodes in Kubernetes clusters work best if provisioned and managed using a logical grouping. Kops introduced the term “InstanceGroup” for this logical grouping. In AWS, an InstanceGroup maps to an AutoScalingGroup.
 
@@ -37,58 +36,14 @@ _For installation instructions and more examples of usage, please refer to the [
 
 ### Currently supported provisioners
 
-| Provisioner | Description |
-| :---------- | :---------- |
-| eks         | provision nodes on EKS |
-| eks-managed | provision managed node groups on EKS|
-| eks-fargate | provision a cluster to run pods on EKS Fargate|
+| Provisioner | Description | Examples |
+| :---------- | :---------- | :----------|
+| eks         | provision nodes on EKS | [EKS.md](./docs/examples/EKS.md)|
+| eks-managed | provision managed node groups on EKS| [EKS-managed.md](./docs/examples/EKS-managed.md) |
+| eks-fargate | provision a cluster to run pods on EKS Fargate| [EKS-fargate.md](./docs/examples/EKS-fargate.md) |
 
 To create an instance group, submit an InstanceGroup custom resource in your cluster, and the controller will provision and bootstrap it to your cluster, and allow you to modify it from within the cluster.
 
-### EKS sample spec
-
-```yaml
-apiVersion: instancemgr.keikoproj.io/v1alpha1
-kind: InstanceGroup
-metadata:
-  name: hello-world
-  namespace: instance-manager
-spec:
-  provisioner: eks
-  strategy:
-    type: rollingUpdate
-    rollingUpdate:
-      maxUnavailable: 1
-  eks:
-    minSize: 3
-    maxSize: 6
-    configuration:
-      clusterName: my-eks-cluster
-      bootstrapArguments: <.. eks bootstrap arguments ..>
-      taints:
-      - key: node-role.kubernetes.io/some-taint
-        value: some-value
-        effect: NoSchedule
-      labels:
-        example.label.com/label: some-value
-      keyPairName: my-ec2-key-pair
-      image: ami-076c743acc3ec4159
-      instanceType: m5.large
-      volumes:
-      - name: /dev/xvda
-        type: gp2
-        size: 32
-      subnets:
-      - subnet-0bf9bc85fd80af561
-      - subnet-0130025d2673de5e4
-      - subnet-01a5c28e074c46580
-      securityGroups:
-      - sg-04adb6343b07c7914
-      tags:
-      - key: my-ec2-tag
-        value: some-value
-```
-
 ### Submit and Verify
 
 ```bash
@@ -119,248 +74,6 @@ ip-10-10-10-30.us-west-2.compute.internal   Ready    hello-world   32s    v1.14.
 ip-10-10-10-40.us-west-2.compute.internal   Ready    hello-world   32s    v1.14.6-eks-5047ed
 ```
 
-### Features
-
-#### Spot instance support
-
-You can manually specify a spot price directly in the spec of an instance group
-
-```yaml
-apiVersion: instancemgr.keikoproj.io/v1alpha1
-kind: InstanceGroup
-metadata:
-  name: hello-world
-  namespace: instance-manager
-spec:
-  strategy: <...>
-  provisioner: eks
-  eks:
-    configuration:
-      spotPrice: "0.67"
-```
-
-instance-manager will switch the instances to spot for you if that price is available.
-
-You can also use [minion-manager](https://github.com/keikoproj/minion-manager/issues/50) in `--events-only` mode to provide spot recommendations for instance-manager.
-
-#### Upgrade Strategies
-
-instance-manager supports multiple upgrade strategies, a basic one called `rollingUpdate` which rotates instances according to `maxUnavailable`.
-
-```yaml
-apiVersion: instancemgr.keikoproj.io/v1alpha1
-kind: InstanceGroup
-metadata:
-  name: hello-world
-  namespace: instance-manager
-spec:
-  strategy:
-    type: rollingUpdate
-    rollingUpdate:
-      maxUnavailable: 30%
-  provisioner: eks
-  eks:
-    configuration: <...>
-```
-
-In this case, 30% of the capacity will terminate at a time, once all desired nodes are in ready state, the next batch will terminate.
-
-You can also use custom controllers with instance manager using the `CRD strategy`.
-
-```yaml
-apiVersion: instancemgr.keikoproj.io/v1alpha1
-kind: InstanceGroup
-metadata:
-  name: hello-world
-  namespace: instance-manager
-spec:
-  strategy:
-    provisioner: eks
-    crd:
-      crdName: rollingupgrades
-      statusJSONPath: .status.currentStatus
-      statusSuccessString: completed
-      statusFailureString: error
-      spec: |
-        apiVersion: upgrademgr.keikoproj.io/v1alpha1
-        kind: RollingUpgrade
-        metadata:
-          name: rollup-nodes
-          namespace: instance-manager
-        spec:
-          postDrainDelaySeconds: 30
-          nodeIntervalSeconds: 30
-          asgName: {{ .InstanceGroup.Status.ActiveScalingGroupName }}
-          strategy:
-            mode: eager
-            type: randomUpdate
-            maxUnavailable: 30%
-            drainTimeout: 120
-```
-
-In this strategy you can create resources as a response for a pending upgrade, you can use [upgrade-manager](https://github.com/keikoproj/upgrade-manager) and `RollingUpgrade` custom resources in order to rotate your instance-groups with more fine grained strategies. You can template any required parameter right form the instance group spec, in this case we are taking the scaling group name from the instance group's `.status.activeScalingGroupName` and putting it in the `.spec.asgName` of the `RollingUpgrade` custom resource. We are also telling instance-manager how to know if this custom-resource is successful or not by looking at the resource's jsonpath for specific strings.
-
-_For more examples and usage, please refer to the [Installation Reference Walkthrough][install]._
-
-#### EKS Managed Node Group (alpha-1)
-
-You can also provision EKS managed node groups by submitting a spec with a different provisioner.
-
-```yaml
-apiVersion: instancemgr.keikoproj.io/v1alpha1
-kind: InstanceGroup
-metadata:
-  name: hello-world
-  namespace: instance-manager
-spec:
-  strategy:
-    type: managed
-  provisioner: eks-managed
-  eks-managed:
-    maxSize: 6
-    minSize: 3
-    configuration:
-      clusterName: my-eks-cluster
-      labels:
-        example.label.com/label: some-value
-      volSize: 20
-      nodeRole: arn:aws:iam::012345678910:role/basic-eks-role
-      amiType: AL2_x86_64
-      instanceType: m5.large
-      keyPairName: my-ec2-key-pair
-      securityGroups:
-      - sg-04adb6343b07c7914
-      subnets:
-      - subnet-0bf9bc85fd80af561
-      - subnet-0130025d2673de5e4
-      - subnet-01a5c28e074c46580
-      tags:
-      - key: my-ec2-tag
-        value: some-value
-```
-
-#### EKS Fargate
-
-The purpose of the fargate provisioner is to enable the management of Fargate profiles.
-
-By associating EKS clusters with a Fargate Profile, pods can be identified for execution through profile selectors. If a to-be-scheduled pod matches any of the selectors in the Fargate Profile, then that pod is scheduled on Fargate.
-
-An EKS cluster can have multiple Fargate Profiles. If a pod matches multiple Fargate Profiles, Amazon EKS picks one of the matches at random.
-
-EKS supports clusters with both local worker nodes and Fargate management.  If a pod is scheduled and matches a Fargate selector then Fargate manages the pod.  Otherwise the pod is scheduled on a worker node.  Clusters can be defined without any worker nodes (0) and completely rely upon Fargate for scheduling and running pods.
-
-More on [Fargate](https://docs.aws.amazon.com/eks/latest/userguide/fargate.html).
-
-The provisioner will manage (create and delete) Fargate Profiles on any EKS cluster (within the account) regardless of whether the cluster was created via CloudFormation, the AWS CLI or the AWS API.
-
-Below is an example specification for the **eks-fargate** provisioner
-
-```yaml
-apiVersion: instancemgr.keikoproj.io/v1alpha1
-kind: InstanceGroup
-metadata:
-  name: hello-world
-spec:
-  # provision for EKS using Fargate
-  provisioner: eks-fargate
-  strategy:
-    type: managed
-  # provisioner configuration
-  eks-fargate:
-    clusterName: "the-cluster-for-my-pods"
-    podExecutionRoleArn: "arn:aws:iam::123456789012:role/MyPodRole"
-    subnets:
-    - subnet-1a2b3c4d
-    - subnet-4d3c2b1a
-    - subnet-0w9x8y7z
-    selectors:
-    - namespace1:
-      labels:
-        key1: "value1"
-        key2: "value2"
-    - namespace2:
-      labels:
-        key1: "value1"
-        key2: "value2"
-    tags:
-      key1: "value1"
-      key2: "value2"
-```
-
-Read more about the [Fargate Profile](https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html).
-
-Note that the eks-fargate provisioner does not accept a Fargate profile name.  Instead, the provisioner creates a unique profile name based upon the cluster name, instance group name and namespace.
-
-If the above *podExecutionRoleArn* parameter is not specified, the provisioner will create a simple, limited role and policy that enables the pod to start but not access any AWS resources.  The role's name will be prefixed by the generated Fargate profile name from above.  That role and policy are shown below.
-
-```yaml
-Type: 'AWS::IAM::Role'
-Properties:
-  AssumeRolePolicyDocument:
-    Version: 2012-10-17
-    Statement:
-      - Effect: "Allow"
-        Principal:
-          Service: "eks-fargate-pods.amazonaws.com"
-        Action: "sts:AssumeRole"
-  ManagedPolicyArns:
-  - "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
-  Path: /
-```
-
-Most likely an execution role with access to addtional AWS resources will be required.  In this case, the above IAM role can be used as the basis to create a new, custom role with the IAM policies specific to your pods. Create your new role and your pod specific policies and use the new role's ARN as the *podExecutionRoleArn* parameter value in eks-fargate spec.
-
-Here is an example of a role with an additional policy for S3 access.
-
-```yaml
-Type: 'AWS::IAM::Role'
-Properties:
-  AssumeRolePolicyDocument:
-    Version: 2012-10-17
-    Statement:
-      - Effect: "Allow"
-        Principal:
-          Service: "eks-fargate-pods.amazonaws.com"
-        Action: "sts:AssumeRole"
-  ManagedPolicyArns:
-  - "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
-  Path: /
-  Policies:
-    - PolicyName: "your chosen name"
-      PolicyDocument:
-        Version: 2012-10-17
-        Statement:
-          - Effect: Allow
-            Action:
-              - 's3:*'
-            Resource: '*'
-```
-
-AWS's Fargate Profiles are immutable.  Once one is created, it cannot be directly modified.  It first has to be deleted and then re-created with the desired change.
-
-The **eks-fargate** provisioner is built on top of that immutability.  Therefore, if an attempt is made to modify an existing profile, the provisioner will return an error.  You first have to `delete` the profile and follow that with a `create`.
-
-#### Bring your own role
-
-You can choose to provide an IAM role that is managed externally to instance-manager by providing the name of the instance profile and the role name.
-
-```yaml
-apiVersion: instancemgr.keikoproj.io/v1alpha1
-kind: InstanceGroup
-metadata:
-  name: hello-world
-  namespace: instance-manager
-spec:
-  strategy: <...>
-  provisioner: eks
-  eks:
-    configuration:
-      roleName: my-eks-role-name
-      instanceProfileName: my-eks-instance-profile-name
-```
-
-if you do not provide these fields, a role will be created for your instance-group by the controller (will require IAM access).
-
 ### Alpha-2 Version
 
 Please consider that this project is in alpha stages and breaking API changes may happen, we will do our best to not break backwards compatiblity without a deprecation period going further.
diff --git a/api/v1alpha1/instancegroup_types.go b/api/v1alpha1/instancegroup_types.go
index d81a7ab..92fed5f 100644
--- a/api/v1alpha1/instancegroup_types.go
+++ b/api/v1alpha1/instancegroup_types.go
@@ -72,6 +72,10 @@ const (
 
 	FileSystemTypeXFS  = "xfs"
 	FileSystemTypeEXT4 = "ext4"
+
+	HostPlacementTenancyType      = "host"
+	DefaultPlacementTenancyType   = "default"
+	DedicatedPlacementTenancyType = "dedicated"
 )
 
 type ScalingConfigurationType string
@@ -100,12 +104,13 @@ var (
 		},
 	}
 
-	AllowedFileSystemTypes            = []string{FileSystemTypeXFS, FileSystemTypeEXT4}
-	AllowedMixedPolicyStrategies      = []string{LaunchTemplateStrategyCapacityOptimized, LaunchTemplateStrategyLowestPrice}
-	AllowedInstancePools              = []string{SubFamilyFlexibleInstancePool}
-	LifecycleHookAllowedTransitions   = []string{LifecycleHookTransitionLaunch, LifecycleHookTransitionTerminate}
-	LifecycleHookAllowedDefaultResult = []string{LifecycleHookResultAbandon, LifecycleHookResultContinue}
-	log                               = ctrl.Log.WithName("v1alpha1")
+	AllowedFileSystemTypes              = []string{FileSystemTypeXFS, FileSystemTypeEXT4}
+	AllowedMixedPolicyStrategies        = []string{LaunchTemplateStrategyCapacityOptimized, LaunchTemplateStrategyLowestPrice}
+	AllowedInstancePools                = []string{SubFamilyFlexibleInstancePool}
+	LifecycleHookAllowedTransitions     = []string{LifecycleHookTransitionLaunch, LifecycleHookTransitionTerminate}
+	LifecycleHookAllowedDefaultResult   = []string{LifecycleHookResultAbandon, LifecycleHookResultContinue}
+	LaunchTemplatePlacementTenancyTypes = []string{HostPlacementTenancyType, DefaultPlacementTenancyType, DedicatedPlacementTenancyType}
+	log                                 = ctrl.Log.WithName("v1alpha1")
 )
 
 // InstanceGroup is the Schema for the instancegroups API
@@ -207,6 +212,8 @@ type EKSConfiguration struct {
 	MetricsCollection           []string                  `json:"metricsCollection,omitempty"`
 	LifecycleHooks              []LifecycleHookSpec       `json:"lifecycleHooks,omitempty"`
 	MixedInstancesPolicy        *MixedInstancesPolicySpec `json:"mixedInstancesPolicy,omitempty"`
+	LicenseSpecifications       []string                  `json:"licenseSpecifications,omitempty"`
+	Placement                   *PlacementSpec            `json:"placement,omitempty"`
 }
 
 const (
@@ -224,6 +231,12 @@ type MixedInstancesPolicySpec struct {
 	InstanceTypes []*InstanceTypeSpec `json:"instanceTypes,omitempty"`
 }
 
+type PlacementSpec struct {
+	AvailabilityZone     string `json:"availabilityZone,omitempty"`
+	HostResourceGroupArn string `json:"hostResourceGroupArn,omitempty"`
+	Tenancy              string `json:"tenancy,omitempty"`
+}
+
 type InstanceTypeSpec struct {
 	Type   string `json:"type"`
 	Weight int64  `json:"weight,omitempty"`
@@ -361,6 +374,20 @@ func (s *EKSSpec) Validate() error {
 		s.Type = LaunchConfiguration
 	}
 
+	if s.IsLaunchConfiguration() {
+		if !common.SliceEmpty(s.EKSConfiguration.LicenseSpecifications) {
+			return errors.Errorf("validation failed, field 'licenseSpecifications' is only valid for LaunchTemplates")
+		}
+		if s.EKSConfiguration.GetPlacement() != nil {
+			if s.EKSConfiguration.GetPlacement().HostResourceGroupArn != "" {
+				return errors.Errorf("validation failed, field 'hostResourceGroupArn' is only valid for LaunchTemplates")
+			}
+			if s.EKSConfiguration.GetPlacement().AvailabilityZone != "" {
+				return errors.Errorf("validation failed, field 'availabilityZone' is only valid for LaunchTemplates")
+			}
+		}
+	}
+
 	return nil
 }
 
@@ -488,8 +515,43 @@ func (c *EKSConfiguration) Validate() error {
 			return err
 		}
 	}
+
+	for i, v := range c.LicenseSpecifications {
+		if !strings.HasPrefix(v, awsprovider.ARNPrefix) {
+			return errors.Errorf("validation failed, 'LicenseSpecifications[%d]' must be a valid IAM role ARN", i)
+		}
+	}
+
+	if c.Placement != nil {
+		if err := c.Placement.Validate(); err != nil {
+			return err
+		}
+	}
+
 	return nil
 }
+
+func (p *PlacementSpec) Validate() error {
+
+	if p == nil {
+		return nil
+	}
+
+	if !common.ContainsEqualFold(LaunchTemplatePlacementTenancyTypes, p.Tenancy) {
+		return errors.Errorf("validation failed, Tenancy must be one of default, dedicated, host")
+	}
+
+	if !common.StringEmpty(p.HostResourceGroupArn) && !strings.HasPrefix(p.HostResourceGroupArn, awsprovider.ARNPrefix) {
+		return errors.Errorf("validation failed, HostResourceGroupArn must be a valid dedicated HostResourceGroup ARN")
+	}
+
+	if !common.StringEmpty(p.HostResourceGroupArn) && p.Tenancy != HostPlacementTenancyType {
+		return errors.Errorf("validation failed, Tenancy must be \"host\" when HostResourceGroupArn is set")
+	}
+
+	return nil
+}
+
 func (m *MixedInstancesPolicySpec) Validate() error {
 	if m.Strategy == nil {
 		m.Strategy = common.StringPtr(LaunchTemplateStrategyCapacityOptimized)
@@ -594,6 +656,9 @@ func (c *EKSConfiguration) GetRoleName() string {
 func (c *EKSConfiguration) GetMixedInstancesPolicy() *MixedInstancesPolicySpec {
 	return c.MixedInstancesPolicy
 }
+func (c *EKSConfiguration) GetPlacement() *PlacementSpec {
+	return c.Placement
+}
 func (c *EKSConfiguration) GetLifecycleHooks() []LifecycleHookSpec {
 	return c.LifecycleHooks
 }
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index b20c77a..dfd036a 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -142,6 +142,16 @@ func (in *EKSConfiguration) DeepCopyInto(out *EKSConfiguration) {
 		*out = new(MixedInstancesPolicySpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.LicenseSpecifications != nil {
+		in, out := &in.LicenseSpecifications, &out.LicenseSpecifications
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Placement != nil {
+		in, out := &in.Placement, &out.Placement
+		*out = new(PlacementSpec)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EKSConfiguration.
@@ -557,6 +567,21 @@ func (in *NodeVolumeMountOptions) DeepCopy() *NodeVolumeMountOptions {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PlacementSpec) DeepCopyInto(out *PlacementSpec) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementSpec.
+func (in *PlacementSpec) DeepCopy() *PlacementSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(PlacementSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RollingUpdateStrategy) DeepCopyInto(out *RollingUpdateStrategy) {
 	*out = *in
diff --git a/config/crd/bases/instancemgr.keikoproj.io_instancegroups.yaml b/config/crd/bases/instancemgr.keikoproj.io_instancegroups.yaml
index e3f26df..1aa2b53 100644
--- a/config/crd/bases/instancemgr.keikoproj.io_instancegroups.yaml
+++ b/config/crd/bases/instancemgr.keikoproj.io_instancegroups.yaml
@@ -91,6 +91,10 @@ spec:
                       additionalProperties:
                         type: string
                       type: object
+                    licenseSpecifications:
+                      items:
+                        type: string
+                      type: array
                     lifecycleHooks:
                       items:
                         properties:
@@ -152,6 +156,15 @@ spec:
                         strategy:
                           type: string
                       type: object
+                    placement:
+                      properties:
+                        availabilityZone:
+                          type: string
+                        hostResourceGroupArn:
+                          type: string
+                        tenancy:
+                          type: string
+                      type: object
                     roleName:
                       type: string
                     securityGroups:
diff --git a/controllers/providers/aws/aws.go b/controllers/providers/aws/aws.go
index 7ad21c1..2b1d1ad 100644
--- a/controllers/providers/aws/aws.go
+++ b/controllers/providers/aws/aws.go
@@ -354,6 +354,62 @@ func (w *AwsWorker) GetLaunchTemplateBlockDevice(name, volType, snapshot string,
 	return device
 }
 
+func (w *AwsWorker) LaunchTemplatePlacementRequest(availabilityZone, hostResourceGroupArn, tenancy string) *ec2.LaunchTemplatePlacementRequest {
+	placement := &ec2.LaunchTemplatePlacementRequest{}
+
+	if !common.StringEmpty(availabilityZone) {
+		placement.AvailabilityZone = aws.String(availabilityZone)
+	}
+
+	if !common.StringEmpty(hostResourceGroupArn) {
+		placement.HostResourceGroupArn = aws.String(hostResourceGroupArn)
+	}
+
+	if !common.StringEmpty(tenancy) {
+		placement.Tenancy = aws.String(tenancy)
+	}
+
+	return placement
+}
+
+func (w *AwsWorker) LaunchTemplatePlacement(availabilityZone, hostResourceGroupArn, tenancy string) *ec2.LaunchTemplatePlacement {
+	placement := &ec2.LaunchTemplatePlacement{}
+
+	if !common.StringEmpty(availabilityZone) {
+		placement.AvailabilityZone = aws.String(availabilityZone)
+	}
+
+	if !common.StringEmpty(hostResourceGroupArn) {
+		placement.HostResourceGroupArn = aws.String(hostResourceGroupArn)
+	}
+
+	if !common.StringEmpty(tenancy) {
+		placement.Tenancy = aws.String(tenancy)
+	}
+
+	return placement
+}
+
+func (w *AwsWorker) LaunchTemplateLicenseConfigurationRequest(input []string) []*ec2.LaunchTemplateLicenseConfigurationRequest {
+	var licenses []*ec2.LaunchTemplateLicenseConfigurationRequest
+	for _, v := range input {
+		licenses = append(licenses, &ec2.LaunchTemplateLicenseConfigurationRequest{
+			LicenseConfigurationArn: aws.String(v),
+		})
+	}
+	return licenses
+}
+
+func (w *AwsWorker) LaunchTemplateLicenseConfiguration(input []string) []*ec2.LaunchTemplateLicenseConfiguration {
+	var licenses []*ec2.LaunchTemplateLicenseConfiguration
+	for _, v := range input {
+		licenses = append(licenses, &ec2.LaunchTemplateLicenseConfiguration{
+			LicenseConfigurationArn: aws.String(v),
+		})
+	}
+	return licenses
+}
+
 func (w *AwsWorker) CreateLaunchConfig(input *autoscaling.CreateLaunchConfigurationInput) error {
 	_, err := w.AsgClient.CreateLaunchConfiguration(input)
 	if err != nil {
diff --git a/controllers/provisioners/eks/create.go b/controllers/provisioners/eks/create.go
index 4d9f48d..12c4cce 100644
--- a/controllers/provisioners/eks/create.go
+++ b/controllers/provisioners/eks/create.go
@@ -45,6 +45,7 @@ func (ctx *EksInstanceGroupContext) Create() error {
 		userData        = ctx.GetBasicUserData(clusterName, args, kubeletArgs, userDataPayload, mounts)
 		sgs             = ctx.ResolveSecurityGroups()
 		spotPrice       = configuration.GetSpotPrice()
+		placement       = configuration.GetPlacement()
 	)
 
 	instanceGroup.SetState(v1alpha1.ReconcileModifying)
@@ -68,6 +69,8 @@ func (ctx *EksInstanceGroupContext) Create() error {
 			Volumes:               configuration.Volumes,
 			UserData:              userData,
 			SpotPrice:             spotPrice,
+			LicenseSpecifications: configuration.LicenseSpecifications,
+			Placement:             placement,
 		}); err != nil {
 			return errors.Wrap(err, "failed to create scaling configuration")
 		}
diff --git a/controllers/provisioners/eks/helpers.go b/controllers/provisioners/eks/helpers.go
index 82d78de..952f53a 100644
--- a/controllers/provisioners/eks/helpers.go
+++ b/controllers/provisioners/eks/helpers.go
@@ -890,7 +890,7 @@ func (ctx *EksInstanceGroupContext) GetOverrides() []*autoscaling.LaunchTemplate
 	)
 	overrides := []*autoscaling.LaunchTemplateOverrides{}
 
-	if mixedPolicy.InstanceTypes != nil {
+	if mixedPolicy != nil && mixedPolicy.InstanceTypes != nil {
 		overrides = append(overrides, &autoscaling.LaunchTemplateOverrides{
 			InstanceType:     aws.String(primaryType),
 			WeightedCapacity: aws.String("1"),
diff --git a/controllers/provisioners/eks/scaling/interface.go b/controllers/provisioners/eks/scaling/interface.go
index 17a2455..60925b6 100644
--- a/controllers/provisioners/eks/scaling/interface.go
+++ b/controllers/provisioners/eks/scaling/interface.go
@@ -58,6 +58,8 @@ type CreateConfigurationInput struct {
 	Volumes               []v1alpha1.NodeVolume
 	UserData              string
 	SpotPrice             string
+	LicenseSpecifications []string
+	Placement             *v1alpha1.PlacementSpec
 }
 
 func ConvertToLaunchTemplate(resource interface{}) *ec2.LaunchTemplate {
diff --git a/controllers/provisioners/eks/scaling/launchconfig.go b/controllers/provisioners/eks/scaling/launchconfig.go
index fa9273f..ed423e6 100644
--- a/controllers/provisioners/eks/scaling/launchconfig.go
+++ b/controllers/provisioners/eks/scaling/launchconfig.go
@@ -90,6 +90,10 @@ func (lc *LaunchConfiguration) Create(input *CreateConfigurationInput) error {
 		opts.SpotPrice = aws.String(input.SpotPrice)
 	}
 
+	if input.Placement != nil && !common.StringEmpty(input.Placement.Tenancy) {
+		opts.PlacementTenancy = aws.String(input.Placement.Tenancy)
+	}
+
 	if err := lc.CreateLaunchConfig(opts); err != nil {
 		return err
 	}
@@ -204,6 +208,14 @@ func (lc *LaunchConfiguration) Drifted(input *CreateConfigurationInput) bool {
 		drift = true
 	}
 
+	if aws.StringValue(existingConfig.PlacementTenancy) != lc.placementTenancy(input.Placement) {
+		log.Info("detected drift", "reason", "placement tenancy has changed", "instancegroup", lc.OwnerName,
+			"previousValue", aws.StringValue(existingConfig.PlacementTenancy),
+			"newValue", lc.placementTenancy(input.Placement),
+		)
+		drift = true
+	}
+
 	devices := lc.blockDeviceList(input.Volumes)
 	existingDevices := sortConfigDevices(existingConfig.BlockDeviceMappings)
 	if !reflect.DeepEqual(existingDevices, devices) {
@@ -250,6 +262,13 @@ func (lc *LaunchConfiguration) RotationNeeded(input *DiscoverConfigurationInput)
 	return false
 }
 
+func (lc *LaunchConfiguration) placementTenancy(placement *v1alpha1.PlacementSpec) string {
+	if placement != nil {
+		return placement.Tenancy
+	}
+	return ""
+}
+
 func (lc *LaunchConfiguration) blockDeviceList(volumes []v1alpha1.NodeVolume) []*autoscaling.BlockDeviceMapping {
 	var devices []*autoscaling.BlockDeviceMapping
 	for _, v := range volumes {
diff --git a/controllers/provisioners/eks/scaling/launchtemplate.go b/controllers/provisioners/eks/scaling/launchtemplate.go
index 6dadd94..e3b8a52 100644
--- a/controllers/provisioners/eks/scaling/launchtemplate.go
+++ b/controllers/provisioners/eks/scaling/launchtemplate.go
@@ -96,12 +96,14 @@ func (lt *LaunchTemplate) Create(input *CreateConfigurationInput) error {
 		IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
 			Arn: aws.String(input.IamInstanceProfileArn),
 		},
-		ImageId:             aws.String(input.ImageId),
-		InstanceType:        aws.String(input.InstanceType),
-		KeyName:             aws.String(input.KeyName),
-		SecurityGroupIds:    aws.StringSlice(input.SecurityGroups),
-		UserData:            aws.String(input.UserData),
-		BlockDeviceMappings: lt.blockDeviceListRequest(input.Volumes),
+		ImageId:               aws.String(input.ImageId),
+		InstanceType:          aws.String(input.InstanceType),
+		KeyName:               aws.String(input.KeyName),
+		SecurityGroupIds:      aws.StringSlice(input.SecurityGroups),
+		UserData:              aws.String(input.UserData),
+		BlockDeviceMappings:   lt.blockDeviceListRequest(input.Volumes),
+		LicenseSpecifications: lt.LaunchTemplateLicenseConfigurationRequest(input.LicenseSpecifications),
+		Placement:             lt.launchTemplatePlacementRequest(input.Placement),
 	}
 
 	if !lt.Provisioned() {
@@ -245,6 +247,29 @@ func (lt *LaunchTemplate) Drifted(input *CreateConfigurationInput) bool {
 		drift = true
 	}
 
+	existingSpec := sortLicenseSpecifications(latestVersion.LaunchTemplateData.LicenseSpecifications)
+	newSpec := sortLicenseSpecifications(lt.LaunchTemplateLicenseConfiguration(input.LicenseSpecifications))
+	if !reflect.DeepEqual(existingSpec, newSpec) {
+		log.Info("detected drift", "reason", "LicenseSpecifications has changed", "instancegroup", lt.OwnerName,
+			"previousValue", existingSpec,
+			"newValue", newSpec,
+		)
+		drift = true
+	}
+
+	placementConfig := lt.launchTemplatePlacement(input.Placement)
+	currentPlacement := latestVersion.LaunchTemplateData.Placement
+	if currentPlacement == nil {
+		currentPlacement = &ec2.LaunchTemplatePlacement{}
+	}
+	if !reflect.DeepEqual(currentPlacement, placementConfig) {
+		log.Info("detected drift", "reason", "placement configuration has changed", "instancegroup", lt.OwnerName,
+			"previousValue", currentPlacement,
+			"newValue", placementConfig,
+		)
+		drift = true
+	}
+
 	if !drift {
 		log.Info("drift not detected", "instancegroup", lt.OwnerName)
 	}
@@ -313,6 +338,20 @@ func (lt *LaunchTemplate) blockDeviceList(volumes []v1alpha1.NodeVolume) []*ec2.
 	return sortTemplateDevices(devices)
 }
 
+func (lt *LaunchTemplate) launchTemplatePlacementRequest(input *v1alpha1.PlacementSpec) *ec2.LaunchTemplatePlacementRequest {
+	if input == nil {
+		return &ec2.LaunchTemplatePlacementRequest{}
+	}
+	return lt.LaunchTemplatePlacementRequest(input.AvailabilityZone, input.HostResourceGroupArn, input.Tenancy)
+}
+
+func (lt *LaunchTemplate) launchTemplatePlacement(input *v1alpha1.PlacementSpec) *ec2.LaunchTemplatePlacement {
+	if input == nil {
+		return &ec2.LaunchTemplatePlacement{}
+	}
+	return lt.LaunchTemplatePlacement(input.AvailabilityZone, input.HostResourceGroupArn, input.Tenancy)
+}
+
 func (lt *LaunchTemplate) getVersion(id int64) *ec2.LaunchTemplateVersion {
 	for _, v := range lt.TargetVersions {
 		n := aws.Int64Value(v.VersionNumber)
@@ -349,3 +388,13 @@ func sortVersions(versions []*ec2.LaunchTemplateVersion) []*ec2.LaunchTemplateVe
 
 	return versions
 }
+
+func sortLicenseSpecifications(licenses []*ec2.LaunchTemplateLicenseConfiguration) []*ec2.LaunchTemplateLicenseConfiguration {
+	if len(licenses) == 0 {
+		return []*ec2.LaunchTemplateLicenseConfiguration{}
+	}
+	sort.Slice(licenses[:], func(i, j int) bool {
+		return aws.StringValue(licenses[i].LicenseConfigurationArn) < aws.StringValue(licenses[j].LicenseConfigurationArn)
+	})
+	return licenses
+}
diff --git a/controllers/provisioners/eks/update.go b/controllers/provisioners/eks/update.go
index 2603654..6af8854 100644
--- a/controllers/provisioners/eks/update.go
+++ b/controllers/provisioners/eks/update.go
@@ -47,6 +47,7 @@ func (ctx *EksInstanceGroupContext) Update() error {
 		userData        = ctx.GetBasicUserData(clusterName, args, kubeletArgs, userDataPayload, mounts)
 		sgs             = ctx.ResolveSecurityGroups()
 		spotPrice       = configuration.GetSpotPrice()
+		placement       = configuration.GetPlacement()
 	)
 
 	instanceGroup.SetState(v1alpha1.ReconcileModifying)
@@ -68,6 +69,8 @@ func (ctx *EksInstanceGroupContext) Update() error {
 		Volumes:               configuration.Volumes,
 		UserData:              userData,
 		SpotPrice:             spotPrice,
+		LicenseSpecifications: configuration.LicenseSpecifications,
+		Placement:             placement,
 	}
 
 	// create new launchconfig if it has drifted
diff --git a/docs/EKS.md b/docs/EKS.md
index d0e8831..c0f5aac 100644
--- a/docs/EKS.md
+++ b/docs/EKS.md
@@ -92,6 +92,10 @@ spec:
 
       # add LifecycleHooks to be created as part of the scaling group
       lifecycleHooks: <[]LifecycleHookSpec> : must be a list of LifecycleHookSpec
+
+      # add Placement information
+      licenseSpecifications: <[]string> : must be a list of strings containing ARNs to Dedicated host license specifications
+      placement: <PlacementSpec> : placement information for EC2 instances.
 ```
 
 ### LifecycleHookSpec
@@ -215,6 +219,33 @@ spec:
         effect: <string> : the effect of the taint
 ```
 
+### PlacementSpec
+
+Represents the EC2 Placement information for your EC2 instances.
+All fields are supported for Launch Templates; Launch Configuration's only support setting `tenancy`.
+
+```yaml
+spec:
+  provisioner: eks
+  eks:
+    type: LaunchTemplate
+    configuration:
+      placement:
+        availabiltyZone: "us-west-2a"
+        hostResourceGroupArn: "arn:aws:resource-groups:us-west-2:1234456789:group/host-group-name"
+        tenancy: "host"
+```
+
+```yaml
+spec:
+  provisioner: eks
+  eks:
+    type: LaunchConfiguration # can be omitted as the default
+    configuration:
+      placement:
+        tenancy: "host"
+```
+
 ## Upgrade Strategies
 
 An 'upgrade' is needed when a change is made to an instance-group which requires node rotation in order to take effect, for example the AMI has changed.
diff --git a/docs/examples/EKS-fargate.md b/docs/examples/EKS-fargate.md
new file mode 100644
index 0000000..3944209
--- /dev/null
+++ b/docs/examples/EKS-fargate.md
@@ -0,0 +1,100 @@
+### EKS Fargate
+
+The purpose of the fargate provisioner is to enable the management of Fargate profiles.
+
+By associating EKS clusters with a Fargate Profile, pods can be identified for execution through profile selectors. If a to-be-scheduled pod matches any of the selectors in the Fargate Profile, then that pod is scheduled on Fargate.
+
+An EKS cluster can have multiple Fargate Profiles. If a pod matches multiple Fargate Profiles, Amazon EKS picks one of the matches at random.
+
+EKS supports clusters with both local worker nodes and Fargate management.  If a pod is scheduled and matches a Fargate selector then Fargate manages the pod.  Otherwise the pod is scheduled on a worker node.  Clusters can be defined without any worker nodes (0) and completely rely upon Fargate for scheduling and running pods.
+
+More on [Fargate](https://docs.aws.amazon.com/eks/latest/userguide/fargate.html).
+
+The provisioner will manage (create and delete) Fargate Profiles on any EKS cluster (within the account) regardless of whether the cluster was created via CloudFormation, the AWS CLI or the AWS API.
+
+Below is an example specification for the **eks-fargate** provisioner
+
+```yaml
+apiVersion: instancemgr.keikoproj.io/v1alpha1
+kind: InstanceGroup
+metadata:
+  name: hello-world
+spec:
+  # provision for EKS using Fargate
+  provisioner: eks-fargate
+  strategy:
+    type: managed
+  # provisioner configuration
+  eks-fargate:
+    clusterName: "the-cluster-for-my-pods"
+    podExecutionRoleArn: "arn:aws:iam::123456789012:role/MyPodRole"
+    subnets:
+    - subnet-1a2b3c4d
+    - subnet-4d3c2b1a
+    - subnet-0w9x8y7z
+    selectors:
+    - namespace1:
+      labels:
+        key1: "value1"
+        key2: "value2"
+    - namespace2:
+      labels:
+        key1: "value1"
+        key2: "value2"
+    tags:
+      key1: "value1"
+      key2: "value2"
+```
+
+Read more about the [Fargate Profile](https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html).
+
+Note that the eks-fargate provisioner does not accept a Fargate profile name.  Instead, the provisioner creates a unique profile name based upon the cluster name, instance group name and namespace.
+
+If the above *podExecutionRoleArn* parameter is not specified, the provisioner will create a simple, limited role and policy that enables the pod to start but not access any AWS resources.  The role's name will be prefixed by the generated Fargate profile name from above.  That role and policy are shown below.
+
+```yaml
+Type: 'AWS::IAM::Role'
+Properties:
+  AssumeRolePolicyDocument:
+    Version: 2012-10-17
+    Statement:
+      - Effect: "Allow"
+        Principal:
+          Service: "eks-fargate-pods.amazonaws.com"
+        Action: "sts:AssumeRole"
+  ManagedPolicyArns:
+  - "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
+  Path: /
+```
+
+Most likely an execution role with access to addtional AWS resources will be required.  In this case, the above IAM role can be used as the basis to create a new, custom role with the IAM policies specific to your pods. Create your new role and your pod specific policies and use the new role's ARN as the *podExecutionRoleArn* parameter value in eks-fargate spec.
+
+Here is an example of a role with an additional policy for S3 access.
+
+```yaml
+Type: 'AWS::IAM::Role'
+Properties:
+  AssumeRolePolicyDocument:
+    Version: 2012-10-17
+    Statement:
+      - Effect: "Allow"
+        Principal:
+          Service: "eks-fargate-pods.amazonaws.com"
+        Action: "sts:AssumeRole"
+  ManagedPolicyArns:
+  - "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
+  Path: /
+  Policies:
+    - PolicyName: "your chosen name"
+      PolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Action:
+              - 's3:*'
+            Resource: '*'
+```
+
+AWS's Fargate Profiles are immutable.  Once one is created, it cannot be directly modified.  It first has to be deleted and then re-created with the desired change.
+
+The **eks-fargate** provisioner is built on top of that immutability.  Therefore, if an attempt is made to modify an existing profile, the provisioner will return an error.  You first have to `delete` the profile and follow that with a `create`.
diff --git a/docs/examples/EKS-managed.md b/docs/examples/EKS-managed.md
new file mode 100644
index 0000000..560482a
--- /dev/null
+++ b/docs/examples/EKS-managed.md
@@ -0,0 +1,36 @@
+### EKS Managed Node Group (alpha-1)
+
+You can also provision EKS managed node groups by submitting a spec with a different provisioner.
+
+```yaml
+apiVersion: instancemgr.keikoproj.io/v1alpha1
+kind: InstanceGroup
+metadata:
+  name: hello-world
+  namespace: instance-manager
+spec:
+  strategy:
+    type: managed
+  provisioner: eks-managed
+  eks-managed:
+    maxSize: 6
+    minSize: 3
+    configuration:
+      clusterName: my-eks-cluster
+      labels:
+        example.label.com/label: some-value
+      volSize: 20
+      nodeRole: arn:aws:iam::012345678910:role/basic-eks-role
+      amiType: AL2_x86_64
+      instanceType: m5.large
+      keyPairName: my-ec2-key-pair
+      securityGroups:
+      - sg-04adb6343b07c7914
+      subnets:
+      - subnet-0bf9bc85fd80af561
+      - subnet-0130025d2673de5e4
+      - subnet-01a5c28e074c46580
+      tags:
+      - key: my-ec2-tag
+        value: some-value
+```
\ No newline at end of file
diff --git a/docs/examples/EKS.md b/docs/examples/EKS.md
new file mode 100644
index 0000000..90baf86
--- /dev/null
+++ b/docs/examples/EKS.md
@@ -0,0 +1,179 @@
+# EKS Usage examples
+
+- [EKS Usage examples](#eks-usage-examples)
+  - [EKS sample spec](#eks-sample-spec)
+  - [Features](#features)
+    - [Spot instance support](#spot-instance-support)
+    - [Upgrade Strategies](#upgrade-strategies)
+    - [EC2 Instance placement](#ec2-instance-placement)
+    - [Bring your own role](#bring-your-own-role)
+
+## EKS sample spec
+
+```yaml
+apiVersion: instancemgr.keikoproj.io/v1alpha1
+kind: InstanceGroup
+metadata:
+  name: hello-world
+  namespace: instance-manager
+spec:
+  provisioner: eks
+  strategy:
+    type: rollingUpdate
+    rollingUpdate:
+      maxUnavailable: 1
+  eks:
+    minSize: 3
+    maxSize: 6
+    configuration:
+      clusterName: my-eks-cluster
+      bootstrapArguments: <.. eks bootstrap arguments ..>
+      taints:
+      - key: node-role.kubernetes.io/some-taint
+        value: some-value
+        effect: NoSchedule
+      labels:
+        example.label.com/label: some-value
+      keyPairName: my-ec2-key-pair
+      image: ami-076c743acc3ec4159
+      instanceType: m5.large
+      volumes:
+      - name: /dev/xvda
+        type: gp2
+        size: 32
+      subnets:
+      - subnet-0bf9bc85fd80af561
+      - subnet-0130025d2673de5e4
+      - subnet-01a5c28e074c46580
+      securityGroups:
+      - sg-04adb6343b07c7914
+      tags:
+      - key: my-ec2-tag
+        value: some-value
+```
+
+## Features
+
+### Spot instance support
+
+You can manually specify a spot price directly in the spec of an instance group
+
+```yaml
+apiVersion: instancemgr.keikoproj.io/v1alpha1
+kind: InstanceGroup
+metadata:
+  name: hello-world
+  namespace: instance-manager
+spec:
+  strategy: <...>
+  provisioner: eks
+  eks:
+    configuration:
+      spotPrice: "0.67"
+```
+
+instance-manager will switch the instances to spot for you if that price is available.
+
+You can also use [minion-manager](https://github.com/keikoproj/minion-manager/issues/50) in `--events-only` mode to provide spot recommendations for instance-manager.
+
+### Upgrade Strategies
+
+instance-manager supports multiple upgrade strategies, a basic one called `rollingUpdate` which rotates instances according to `maxUnavailable`.
+
+```yaml
+apiVersion: instancemgr.keikoproj.io/v1alpha1
+kind: InstanceGroup
+metadata:
+  name: hello-world
+  namespace: instance-manager
+spec:
+  strategy:
+    type: rollingUpdate
+    rollingUpdate:
+      maxUnavailable: 30%
+  provisioner: eks
+  eks:
+    configuration: <...>
+```
+
+In this case, 30% of the capacity will terminate at a time, once all desired nodes are in ready state, the next batch will terminate.
+
+You can also use custom controllers with instance manager using the `CRD strategy`.
+
+```yaml
+apiVersion: instancemgr.keikoproj.io/v1alpha1
+kind: InstanceGroup
+metadata:
+  name: hello-world
+  namespace: instance-manager
+spec:
+  strategy:
+    provisioner: eks
+    crd:
+      crdName: rollingupgrades
+      statusJSONPath: .status.currentStatus
+      statusSuccessString: completed
+      statusFailureString: error
+      spec: |
+        apiVersion: upgrademgr.keikoproj.io/v1alpha1
+        kind: RollingUpgrade
+        metadata:
+          name: rollup-nodes
+          namespace: instance-manager
+        spec:
+          postDrainDelaySeconds: 30
+          nodeIntervalSeconds: 30
+          asgName: {{ .InstanceGroup.Status.ActiveScalingGroupName }}
+          strategy:
+            mode: eager
+            type: randomUpdate
+            maxUnavailable: 30%
+            drainTimeout: 120
+```
+
+In this strategy you can create resources as a response for a pending upgrade, you can use [upgrade-manager](https://github.com/keikoproj/upgrade-manager) and `RollingUpgrade` custom resources in order to rotate your instance-groups with more fine grained strategies. You can template any required parameter right form the instance group spec, in this case we are taking the scaling group name from the instance group's `.status.activeScalingGroupName` and putting it in the `.spec.asgName` of the `RollingUpgrade` custom resource. We are also telling instance-manager how to know if this custom-resource is successful or not by looking at the resource's jsonpath for specific strings.
+
+_For more examples and usage, please refer to the [Installation Reference Walkthrough][install]._
+
+### EC2 Instance placement
+
+When using Launch Templates, you can provide EC2 placement information to take advantage on EC2 dedicated hosts.
+
+```yaml
+apiVersion: instancemgr.keikoproj.io/v1alpha1
+kind: InstanceGroup
+metadata:
+  name: hello-world
+  namespace: instance-manager
+spec:
+  strategy: <...>
+  provisioner: eks
+  eks:
+    type: LaunchTemplate
+    configuration:
+      placement:
+        availabiltyZone: "us-west-2a"
+        hostResourceGroupArn: "arn:aws:resource-groups:us-west-2:1234456789:group/host-group-name"
+        tenancy: "host"
+```
+
+### Bring your own role
+
+You can choose to provide an IAM role that is managed externally to instance-manager by providing the name of the instance profile and the role name.
+
+```yaml
+apiVersion: instancemgr.keikoproj.io/v1alpha1
+kind: InstanceGroup
+metadata:
+  name: hello-world
+  namespace: instance-manager
+spec:
+  strategy: <...>
+  provisioner: eks
+  eks:
+    configuration:
+      roleName: my-eks-role-name
+      instanceProfileName: my-eks-instance-profile-name
+```
+
+if you do not provide these fields, a role will be created for your instance-group by the controller (will require IAM access).


Test patch:
diff --git a/api/v1alpha1/instancegroup_types_test.go b/api/v1alpha1/instancegroup_types_test.go
index f2527b7..e86bac5 100644
--- a/api/v1alpha1/instancegroup_types_test.go
+++ b/api/v1alpha1/instancegroup_types_test.go
@@ -21,7 +21,7 @@ import (
 )
 
 type EksUnitTest struct {
-	InstanceGroup InstanceGroup
+	InstanceGroup *InstanceGroup
 }
 
 func (u *EksUnitTest) Run(t *testing.T) string {
@@ -35,7 +35,7 @@ func (u *EksUnitTest) Run(t *testing.T) string {
 
 func TestInstanceGroupSpecValidate(t *testing.T) {
 	type args struct {
-		instancegroup InstanceGroup
+		instancegroup *InstanceGroup
 	}
 	testFunction := func(t *testing.T, args args) string {
 		testCase := EksUnitTest{
@@ -51,24 +51,156 @@ func TestInstanceGroupSpecValidate(t *testing.T) {
 		{
 			name: "eks-fargate with managed strategy",
 			args: args{
-				instancegroup: MockInstanceGroup("eks-fargate", "managed"),
+				instancegroup: MockInstanceGroup("eks-fargate", "managed", nil, nil, basicFargateSpec()),
 			},
 			want: "",
 		},
 		{
 			name: "eks-bogus provisioner",
 			args: args{
-				instancegroup: MockInstanceGroup("eks-bogus", "managed"),
+				instancegroup: MockInstanceGroup("eks-bogus", "managed", nil, nil, nil),
 			},
 			want: "validation failed, provisioner 'eks-bogus' is invalid",
 		},
 		{
 			name: "eks-fargate with bad strategy",
 			args: args{
-				instancegroup: MockInstanceGroup("eks-fargate", "rollingUpdate"),
+				instancegroup: MockInstanceGroup("eks-fargate", "rollingUpdate", nil, nil, basicFargateSpec()),
 			},
 			want: "validation failed, strategy 'rollingUpdate' is invalid for the eks-fargate provisioner",
 		},
+		{
+			name: "eks with empty strings in licenseSpecifications",
+			args: args{
+				instancegroup: MockInstanceGroup("eks", "rollingUpdate", &EKSSpec{
+					MaxSize: 1,
+					MinSize: 1,
+					Type:    "LaunchTemplate",
+					EKSConfiguration: &EKSConfiguration{
+						EksClusterName:        "my-eks-cluster",
+						NodeSecurityGroups:    []string{"sg-123456789"},
+						Image:                 "ami-12345",
+						InstanceType:          "m5.large",
+						KeyPairName:           "thisShouldBeOptional",
+						Subnets:               []string{"subnet-1111111", "subnet-222222"},
+						LicenseSpecifications: []string{""},
+					},
+				}, nil, nil),
+			},
+			want: "validation failed, 'LicenseSpecifications[0]' must be a valid IAM role ARN",
+		},
+		{
+			name: "eks with invalid licenseSpecification",
+			args: args{
+				instancegroup: MockInstanceGroup("eks", "rollingUpdate", &EKSSpec{
+					MaxSize: 1,
+					MinSize: 1,
+					Type:    "LaunchTemplate",
+					EKSConfiguration: &EKSConfiguration{
+						EksClusterName:        "my-eks-cluster",
+						NodeSecurityGroups:    []string{"sg-123456789"},
+						Image:                 "ami-12345",
+						InstanceType:          "m5.large",
+						KeyPairName:           "thisShouldBeOptional",
+						Subnets:               []string{"subnet-1111111", "subnet-222222"},
+						LicenseSpecifications: []string{"thisShouldBeAnARN"},
+					},
+				}, nil, nil),
+			},
+			want: "validation failed, 'LicenseSpecifications[0]' must be a valid IAM role ARN",
+		},
+		{
+			name: "eks with valid Placement",
+			args: args{
+				instancegroup: MockInstanceGroup("eks", "rollingUpdate", &EKSSpec{
+					MaxSize: 1,
+					MinSize: 1,
+					Type:    "LaunchTemplate",
+					EKSConfiguration: &EKSConfiguration{
+						EksClusterName:     "my-eks-cluster",
+						NodeSecurityGroups: []string{"sg-123456789"},
+						Image:              "ami-12345",
+						InstanceType:       "m5.large",
+						KeyPairName:        "thisShouldBeOptional",
+						Subnets:            []string{"subnet-1111111", "subnet-222222"},
+						Placement: &PlacementSpec{
+							AvailabilityZone:     "us-west-2a",
+							HostResourceGroupArn: "arn:aws:resource-groups:us-west-2:1122334455:group/resourceName",
+							Tenancy:              "host",
+						},
+					},
+				}, nil, nil),
+			},
+			want: "",
+		},
+		{
+			name: "eks with invalid combination of HostResourceGroupArn and Tenancy in Placement",
+			args: args{
+				instancegroup: MockInstanceGroup("eks", "rollingUpdate", &EKSSpec{
+					MaxSize: 1,
+					MinSize: 1,
+					Type:    "LaunchTemplate",
+					EKSConfiguration: &EKSConfiguration{
+						EksClusterName:     "my-eks-cluster",
+						NodeSecurityGroups: []string{"sg-123456789"},
+						Image:              "ami-12345",
+						InstanceType:       "m5.large",
+						KeyPairName:        "thisShouldBeOptional",
+						Subnets:            []string{"subnet-1111111", "subnet-222222"},
+						Placement: &PlacementSpec{
+							HostResourceGroupArn: "arn:aws:resource-groups:us-west-2:1122334455:group/resourceName",
+							Tenancy:              "default",
+						},
+					},
+				}, nil, nil),
+			},
+			want: "validation failed, Tenancy must be \"host\" when HostResourceGroupArn is set",
+		},
+		{
+			name: "eks with invalid HostResourceGroupArn in Placement",
+			args: args{
+				instancegroup: MockInstanceGroup("eks", "rollingUpdate", &EKSSpec{
+					MaxSize: 1,
+					MinSize: 1,
+					Type:    "LaunchTemplate",
+					EKSConfiguration: &EKSConfiguration{
+						EksClusterName:     "my-eks-cluster",
+						NodeSecurityGroups: []string{"sg-123456789"},
+						Image:              "ami-12345",
+						InstanceType:       "m5.large",
+						KeyPairName:        "thisShouldBeOptional",
+						Subnets:            []string{"subnet-1111111", "subnet-222222"},
+						Placement: &PlacementSpec{
+							HostResourceGroupArn: "notAnARN",
+							Tenancy:              "host",
+						},
+					},
+				}, nil, nil),
+			},
+			want: "validation failed, HostResourceGroupArn must be a valid dedicated HostResourceGroup ARN",
+		},
+		{
+			name: "eks with invalid Tenancy in Placement",
+			args: args{
+				instancegroup: MockInstanceGroup("eks", "rollingUpdate", &EKSSpec{
+					MaxSize: 1,
+					MinSize: 1,
+					Type:    "LaunchTemplate",
+					EKSConfiguration: &EKSConfiguration{
+						EksClusterName:     "my-eks-cluster",
+						NodeSecurityGroups: []string{"sg-123456789"},
+						Image:              "ami-12345",
+						InstanceType:       "m5.large",
+						KeyPairName:        "thisShouldBeOptional",
+						Subnets:            []string{"subnet-1111111", "subnet-222222"},
+						Placement: &PlacementSpec{
+							Tenancy: "invalid",
+						},
+					},
+				}, nil, nil),
+			},
+			want: "validation failed, Tenancy must be one of default, dedicated, host",
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -81,26 +213,31 @@ func TestInstanceGroupSpecValidate(t *testing.T) {
 	}
 }
 
-func MockInstanceGroup(provisioner, strategy string) InstanceGroup {
-	ig := InstanceGroup{
+func basicFargateSpec() *EKSFargateSpec {
+	return &EKSFargateSpec{
+		ClusterName:         "",
+		PodExecutionRoleArn: "",
+		Subnets:             []string{"subnet-1111111", "subnet-222222"},
+		Tags: []map[string]string{
+			{
+				"key":   "a-key",
+				"value": "a-value",
+			},
+		},
+	}
+}
+
+func MockInstanceGroup(provisioner, strategy string, eksSpec *EKSSpec, eksManagedSpec *EKSManagedSpec, eksFargateSpec *EKSFargateSpec) *InstanceGroup {
+	return &InstanceGroup{
 		Spec: InstanceGroupSpec{
 			Provisioner: provisioner,
 			AwsUpgradeStrategy: AwsUpgradeStrategy{
 				Type: strategy,
 			},
-			EKSFargateSpec: &EKSFargateSpec{
-				ClusterName:         "",
-				PodExecutionRoleArn: "",
-				Subnets:             []string{"subnet-1111111", "subnet-222222"},
-				Tags: []map[string]string{
-					{
-						"key":   "a-key",
-						"value": "a-value",
-					},
-				},
-			},
+			EKSSpec:        eksSpec,
+			EKSManagedSpec: eksManagedSpec,
+			EKSFargateSpec: eksFargateSpec,
 		},
 	}
 
-	return ig
 }
diff --git a/controllers/provisioners/eks/scaling/launchconfig_test.go b/controllers/provisioners/eks/scaling/launchconfig_test.go
index 37be326..6ba2e81 100644
--- a/controllers/provisioners/eks/scaling/launchconfig_test.go
+++ b/controllers/provisioners/eks/scaling/launchconfig_test.go
@@ -185,6 +185,9 @@ func TestLaunchConfigurationCreate(t *testing.T) {
 				Size: 30,
 			},
 		},
+		Placement: &v1alpha1.PlacementSpec{
+			Tenancy: "host",
+		},
 	})
 	g.Expect(err).NotTo(gomega.HaveOccurred())
 
@@ -459,6 +462,19 @@ func TestLaunchConfigurationDrifted(t *testing.T) {
 			},
 			shouldDrift: true,
 		},
+		{
+			launchConfig: &autoscaling.LaunchConfiguration{
+				LaunchConfigurationName: aws.String("my-launch-config"),
+				PlacementTenancy:        aws.String(""),
+			},
+			input: &CreateConfigurationInput{
+				SecurityGroups: []string{},
+				Placement: &v1alpha1.PlacementSpec{
+					Tenancy: "host",
+				},
+			},
+			shouldDrift: true,
+		},
 	}
 
 	for i, tc := range tests {
diff --git a/controllers/provisioners/eks/scaling/launchtemplate_test.go b/controllers/provisioners/eks/scaling/launchtemplate_test.go
index 5c67358..f8dd597 100644
--- a/controllers/provisioners/eks/scaling/launchtemplate_test.go
+++ b/controllers/provisioners/eks/scaling/launchtemplate_test.go
@@ -591,6 +591,26 @@ func TestLaunchTemplateDrifted(t *testing.T) {
 			},
 			shouldDrift: true,
 		},
+		{
+			launchTemplate: MockLaunchTemplate("my-launch-template"),
+			latestVersion:  MockLaunchTemplateVersion(),
+			input: &CreateConfigurationInput{
+				LicenseSpecifications: []string{"arn:aws:license-manager:us-west-2:1234456789:license-configuration:lic-1111122223333344444111112222333"},
+			},
+			shouldDrift: true,
+		},
+		{
+			launchTemplate: MockLaunchTemplate("my-launch-template"),
+			latestVersion:  MockLaunchTemplateVersion(),
+			input: &CreateConfigurationInput{
+				Placement: &v1alpha1.PlacementSpec{
+					AvailabilityZone:     "us-west-2a",
+					HostResourceGroupArn: "arn:aws:resource-groups:us-west-2:1234456789:group/host-group-name",
+					Tenancy:              "host",
+				},
+			},
+			shouldDrift: true,
+		},
 	}
 
 	for i, tc := range tests {
@@ -606,3 +626,159 @@ func TestLaunchTemplateDrifted(t *testing.T) {
 		g.Expect(result).To(gomega.Equal(tc.shouldDrift))
 	}
 }
+
+func TestLaunchTemplatePlacementRequest(t *testing.T) {
+	var (
+		g       = gomega.NewGomegaWithT(t)
+		asgMock = &MockAutoScalingClient{}
+		ec2Mock = &MockEc2Client{}
+	)
+
+	w := awsprovider.AwsWorker{
+		AsgClient: asgMock,
+		Ec2Client: ec2Mock,
+	}
+
+	discoveryInput := &DiscoverConfigurationInput{
+		ScalingGroup: &autoscaling.Group{
+			AutoScalingGroupName: aws.String("my-asg"),
+			LaunchTemplate: &autoscaling.LaunchTemplateSpecification{
+				LaunchTemplateName: aws.String("prefix-my-launch-template"),
+				Version:            aws.String("6"),
+			},
+		},
+	}
+
+	tests := []struct {
+		name     string
+		input    *v1alpha1.PlacementSpec
+		expected *ec2.LaunchTemplatePlacementRequest
+	}{
+		{
+			name:     "nil input",
+			input:    nil,
+			expected: &ec2.LaunchTemplatePlacementRequest{},
+		},
+		{
+			name: "valid input",
+			input: &v1alpha1.PlacementSpec{
+				AvailabilityZone:     "us-west-2a",
+				HostResourceGroupArn: "arn:aws:resource-groups:us-west-2:1234456789:group/host-group-name",
+				Tenancy:              "host",
+			},
+			expected: &ec2.LaunchTemplatePlacementRequest{
+				AvailabilityZone:     aws.String("us-west-2a"),
+				HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:1234456789:group/host-group-name"),
+				Tenancy:              aws.String("host"),
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			lt, err := NewLaunchTemplate("", w, discoveryInput)
+			g.Expect(err).NotTo(gomega.HaveOccurred())
+			output := lt.launchTemplatePlacementRequest(tc.input)
+
+			g.Expect(output).To(gomega.Equal(tc.expected))
+		})
+	}
+}
+
+func TestLaunchTemplatePlacement(t *testing.T) {
+	var (
+		g       = gomega.NewGomegaWithT(t)
+		asgMock = &MockAutoScalingClient{}
+		ec2Mock = &MockEc2Client{}
+	)
+
+	w := awsprovider.AwsWorker{
+		AsgClient: asgMock,
+		Ec2Client: ec2Mock,
+	}
+
+	discoveryInput := &DiscoverConfigurationInput{
+		ScalingGroup: &autoscaling.Group{
+			AutoScalingGroupName: aws.String("my-asg"),
+			LaunchTemplate: &autoscaling.LaunchTemplateSpecification{
+				LaunchTemplateName: aws.String("prefix-my-launch-template"),
+				Version:            aws.String("6"),
+			},
+		},
+	}
+
+	tests := []struct {
+		name     string
+		input    *v1alpha1.PlacementSpec
+		expected *ec2.LaunchTemplatePlacement
+	}{
+		{
+			name:     "nil input",
+			input:    nil,
+			expected: &ec2.LaunchTemplatePlacement{},
+		},
+		{
+			name: "valid input",
+			input: &v1alpha1.PlacementSpec{
+				AvailabilityZone:     "us-west-2a",
+				HostResourceGroupArn: "arn:aws:resource-groups:us-west-2:1234456789:group/host-group-name",
+				Tenancy:              "host",
+			},
+			expected: &ec2.LaunchTemplatePlacement{
+				AvailabilityZone:     aws.String("us-west-2a"),
+				HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:1234456789:group/host-group-name"),
+				Tenancy:              aws.String("host"),
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			lt, err := NewLaunchTemplate("", w, discoveryInput)
+			g.Expect(err).NotTo(gomega.HaveOccurred())
+			output := lt.launchTemplatePlacement(tc.input)
+
+			g.Expect(output).To(gomega.Equal(tc.expected))
+		})
+	}
+}
+
+func TestSortLicenseSpecifications(t *testing.T) {
+	g := gomega.NewGomegaWithT(t)
+
+	tests := []struct {
+		input    []*ec2.LaunchTemplateLicenseConfiguration
+		expected []*ec2.LaunchTemplateLicenseConfiguration
+	}{
+		{
+			input: []*ec2.LaunchTemplateLicenseConfiguration{
+				&ec2.LaunchTemplateLicenseConfiguration{
+					LicenseConfigurationArn: aws.String("arn:aws:license-manager:us-west-2:1234456789:license-configuration:lic-2"),
+				},
+				&ec2.LaunchTemplateLicenseConfiguration{
+					LicenseConfigurationArn: aws.String("arn:aws:license-manager:us-west-2:1234456789:license-configuration:lic-3"),
+				},
+				&ec2.LaunchTemplateLicenseConfiguration{
+					LicenseConfigurationArn: aws.String("arn:aws:license-manager:us-west-2:1234456789:license-configuration:lic-1"),
+				},
+			},
+			expected: []*ec2.LaunchTemplateLicenseConfiguration{
+				&ec2.LaunchTemplateLicenseConfiguration{
+					LicenseConfigurationArn: aws.String("arn:aws:license-manager:us-west-2:1234456789:license-configuration:lic-1"),
+				},
+				&ec2.LaunchTemplateLicenseConfiguration{
+					LicenseConfigurationArn: aws.String("arn:aws:license-manager:us-west-2:1234456789:license-configuration:lic-2"),
+				},
+				&ec2.LaunchTemplateLicenseConfiguration{
+					LicenseConfigurationArn: aws.String("arn:aws:license-manager:us-west-2:1234456789:license-configuration:lic-3"),
+				},
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		output := sortLicenseSpecifications(tc.input)
+		g.Expect(output).To(gomega.Equal(tc.expected))
+	}
+
+}