Skip to content

Commit

Permalink
feat(aws): Adding support for caching ec2 launch templates (#4523)
Browse files Browse the repository at this point in the history
* feat(aws): Adding support for ec2 launch templates

- Added new Launch template caching agent (feature flagged)
- Updated cluster provider & agent to handle server groups with templates

* - null check

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
jeyrschabu and mergify[bot] committed Apr 24, 2020
1 parent 283107c commit a76ff34
Show file tree
Hide file tree
Showing 10 changed files with 641 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ class Keys implements KeyParser {
break
case Namespace.STACKS.ns:
result << [stackId: parts[2], account: parts[3], region: parts[4]]
break
case Namespace.LAUNCH_TEMPLATES.ns:
def names = Names.parseName(parts[4])
result << [
account: parts[2],
region: parts[ 3],
launchTemplateName: parts[4],
application: names.app?.toLowerCase(),
stack: names.stack
]

break
default:
return null
Expand Down Expand Up @@ -203,4 +214,9 @@ class Keys implements KeyParser {
static String getCloudFormationKey(String stackId, String accountName, String region) {
"${ID}:${Namespace.STACKS}:${accountName}:${region}:${stackId}"
}

static String getLaunchTemplateKey(
String launchTemplateName, String account, String region) {
"${ID}:${Namespace.LAUNCH_TEMPLATES}:${account}:${region}:${launchTemplateName}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class AmazonServerGroup implements ServerGroup, Serializable {
Set health
Map<String, Object> image
Map<String, Object> launchConfig
Map<String, Object> launchTemplate
Map<String, Object> asg
List<Map> scalingPolicies
List<Map> scheduledActions
Expand Down Expand Up @@ -111,7 +112,12 @@ class AmazonServerGroup implements ServerGroup, Serializable {
if (launchConfig && launchConfig.containsKey("securityGroups")) {
securityGroups = (Set<String>) launchConfig.securityGroups
}
securityGroups

if (launchTemplate && launchTemplate.containsKey("securityGroups")) {
securityGroups = (Set<String>) launchConfig.securityGroups
}

return securityGroups
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright 2020 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.clouddriver.aws.provider.agent;

import static com.netflix.spinnaker.cats.agent.AgentDataType.Authority.AUTHORITATIVE;
import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.IMAGES;
import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.LAUNCH_TEMPLATES;
import static java.util.stream.Collectors.toSet;

import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.DescribeLaunchTemplateVersionsRequest;
import com.amazonaws.services.ec2.model.DescribeLaunchTemplateVersionsResult;
import com.amazonaws.services.ec2.model.DescribeLaunchTemplatesRequest;
import com.amazonaws.services.ec2.model.DescribeLaunchTemplatesResult;
import com.amazonaws.services.ec2.model.LaunchTemplate;
import com.amazonaws.services.ec2.model.LaunchTemplateVersion;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spectator.api.Registry;
import com.netflix.spinnaker.cats.agent.AccountAware;
import com.netflix.spinnaker.cats.agent.AgentDataType;
import com.netflix.spinnaker.cats.agent.CacheResult;
import com.netflix.spinnaker.cats.agent.CachingAgent;
import com.netflix.spinnaker.cats.agent.DefaultCacheResult;
import com.netflix.spinnaker.cats.cache.CacheData;
import com.netflix.spinnaker.cats.cache.DefaultCacheData;
import com.netflix.spinnaker.cats.provider.ProviderCache;
import com.netflix.spinnaker.clouddriver.aws.data.Keys;
import com.netflix.spinnaker.clouddriver.aws.provider.AwsProvider;
import com.netflix.spinnaker.clouddriver.aws.security.AmazonClientProvider;
import com.netflix.spinnaker.clouddriver.aws.security.NetflixAmazonCredentials;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class AmazonLaunchTemplateCachingAgent implements CachingAgent, AccountAware {
private final AmazonClientProvider amazonClientProvider;
private final NetflixAmazonCredentials account;
private final ObjectMapper objectMapper;
private final String region;

private static final TypeReference<Map<String, Object>> ATTRIBUTES =
new TypeReference<Map<String, Object>>() {};
private static final Set<AgentDataType> types =
new HashSet<>(Collections.singletonList(AUTHORITATIVE.forType(LAUNCH_TEMPLATES.getNs())));

public AmazonLaunchTemplateCachingAgent(
AmazonClientProvider amazonClientProvider,
NetflixAmazonCredentials account,
String region,
ObjectMapper objectMapper,
Registry registry) {
this.amazonClientProvider = amazonClientProvider;
this.account = account;
this.region = region;
this.objectMapper = objectMapper;
}

@Override
public Collection<AgentDataType> getProvidedDataTypes() {
return types;
}

@Override
public CacheResult loadData(ProviderCache providerCache) {
final AmazonEC2 ec2 = amazonClientProvider.getAmazonEC2(account, region);
final List<LaunchTemplate> launchTemplates = getLaunchTemplates(ec2);
final List<LaunchTemplateVersion> launchTemplateVersions =
getLaunchTemplateVersions(ec2, launchTemplates);
final List<CacheData> cachedData = new ArrayList<>();
for (LaunchTemplate launchTemplate : launchTemplates) {
Set<LaunchTemplateVersion> versions =
launchTemplateVersions.stream()
.filter(t -> t.getLaunchTemplateId().equals(launchTemplate.getLaunchTemplateId()))
.collect(toSet());

// store the latest template version info
LaunchTemplateVersion latest =
versions.stream()
.filter(v -> v.getVersionNumber().equals(launchTemplate.getLatestVersionNumber()))
.findFirst()
.orElseThrow(
() ->
new IllegalStateException(
String.format(
"No latest version found for template %s", launchTemplate)));

String key =
Keys.getLaunchTemplateKey(
launchTemplate.getLaunchTemplateName(), account.getName(), region);
Map<String, Object> attributes = objectMapper.convertValue(launchTemplate, ATTRIBUTES);

attributes.put("application", Keys.parse(key).get("application"));
attributes.put("latestVersion", latest);

// include version info
attributes.put("versions", versions);

Set<String> images =
versions.stream()
.map(
i ->
Keys.getImageKey(
i.getLaunchTemplateData().getImageId(), account.getName(), region))
.collect(Collectors.toSet());

Map<String, Collection<String>> relationships = Collections.singletonMap(IMAGES.ns, images);
cachedData.add(new DefaultCacheData(key, attributes, relationships));
}

return new DefaultCacheResult(Collections.singletonMap(LAUNCH_TEMPLATES.ns, cachedData));
}

/** Gets a list of ec2 Launch templates */
private List<LaunchTemplate> getLaunchTemplates(AmazonEC2 ec2) {
final List<LaunchTemplate> launchTemplates = new ArrayList<>();
final DescribeLaunchTemplatesRequest request = new DescribeLaunchTemplatesRequest();
while (true) {
final DescribeLaunchTemplatesResult result = ec2.describeLaunchTemplates(request);
launchTemplates.addAll(result.getLaunchTemplates());
if (result.getNextToken() != null) {
request.withNextToken(result.getNextToken());
} else {
break;
}
}

return launchTemplates;
}

/** Gets Launch template versions */
private List<LaunchTemplateVersion> getLaunchTemplateVersions(
AmazonEC2 ec2, List<LaunchTemplate> launchTemplates) {
final List<LaunchTemplateVersion> launchTemplateVersions = new ArrayList<>();
for (LaunchTemplate launchTemplate : launchTemplates) {
launchTemplateVersions.addAll(getLaunchTemplateVersions(ec2, launchTemplate));
}

return launchTemplateVersions;
}

/** Gets a list of ec2 Launch template versions for a Launch template */
private List<LaunchTemplateVersion> getLaunchTemplateVersions(
AmazonEC2 ec2, LaunchTemplate template) {
final List<LaunchTemplateVersion> launchTemplateVersions = new ArrayList<>();
final DescribeLaunchTemplateVersionsRequest request =
new DescribeLaunchTemplateVersionsRequest()
.withLaunchTemplateId(template.getLaunchTemplateId());
while (true) {
final DescribeLaunchTemplateVersionsResult result =
ec2.describeLaunchTemplateVersions(request);
launchTemplateVersions.addAll(result.getLaunchTemplateVersions());
if (result.getNextToken() != null) {
request.withNextToken(result.getNextToken());
} else {
break;
}
}

return launchTemplateVersions;
}

@Override
public String getAgentType() {
return String.format("%s/%s/%s", account.getName(), region, getClass());
}

@Override
public String getProviderName() {
return AwsProvider.PROVIDER_NAME;
}

@Override
public String getAccountName() {
return account.getName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
INFORMATIVE.forType(LOAD_BALANCERS.ns),
INFORMATIVE.forType(TARGET_GROUPS.ns),
INFORMATIVE.forType(LAUNCH_CONFIGS.ns),
INFORMATIVE.forType(INSTANCES.ns)
INFORMATIVE.forType(INSTANCES.ns),
INFORMATIVE.forType(LAUNCH_TEMPLATES.ns)
] as Set)

final AmazonCloudProvider amazonCloudProvider
Expand Down Expand Up @@ -443,6 +444,7 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
log.debug("Caching ${cacheResults[TARGET_GROUPS.ns]?.size()} target groups in ${agentType}")
log.debug("Caching ${cacheResults[LAUNCH_CONFIGS.ns]?.size()} launch configs in ${agentType}")
log.debug("Caching ${cacheResults[INSTANCES.ns]?.size()} instances in ${agentType}")
log.debug("Caching ${cacheResults[LAUNCH_TEMPLATES.ns]?.size()} launch templates in ${agentType}")
if (evictableOnDemandCacheDatas) {
log.info("Evicting onDemand cache keys (${evictableOnDemandCacheDatas.collect { "${it.id}/${start - it.attributes.cacheTime}ms" }.join(", ")})")
}
Expand Down Expand Up @@ -496,6 +498,7 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
Map<String, CacheData> targetGroups = cache()
Map<String, CacheData> launchConfigs = cache()
Map<String, CacheData> instances = cache()
Map<String, CacheData> launchTemplates = cache()

for (AutoScalingGroup asg : asgs) {
def onDemandCacheData = onDemandCacheDataByAsg ? onDemandCacheDataByAsg[Keys.getServerGroupKey(asg.autoScalingGroupName, account.name, region)] : null
Expand All @@ -511,6 +514,7 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
cache(cacheResults["targetGroups"], targetGroups)
cache(cacheResults["launchConfigs"], launchConfigs)
cache(cacheResults["instances"], instances)
cache(cacheResults["launchTemplates"], launchTemplates)
} else {
try {
AsgData data = new AsgData(asg, scalingPolicies[asg.autoScalingGroupName], scheduledActions[asg.autoScalingGroupName], account.name, region, subnetMap)
Expand All @@ -521,6 +525,7 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
cacheInstances(data, instances)
cacheLoadBalancers(data, loadBalancers)
cacheTargetGroups(data, targetGroups)
cacheLaunchTemplate(data, launchTemplates)
} catch (Exception ex) {
log.warn("Failed to cache ${asg.autoScalingGroupName} in ${account.name}/${region}", ex)
}
Expand All @@ -535,6 +540,7 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
(TARGET_GROUPS.ns): targetGroups.values(),
(LAUNCH_CONFIGS.ns): launchConfigs.values(),
(INSTANCES.ns) : instances.values(),
(LAUNCH_TEMPLATES.ns): launchTemplates.values(),
(ON_DEMAND.ns) : onDemandCacheDataByAsg.values()
], [
(ON_DEMAND.ns) : evictableOnDemandCacheDataIdentifiers
Expand Down Expand Up @@ -613,13 +619,16 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
relationships[LOAD_BALANCERS.ns].addAll(data.loadBalancerNames)
relationships[TARGET_GROUPS.ns].addAll(data.targetGroupKeys)
relationships[LAUNCH_CONFIGS.ns].add(data.launchConfig)
relationships[LAUNCH_TEMPLATES.ns].add(data.launchTemplate)
relationships[INSTANCES.ns].addAll(data.instanceIds)
}
}

private void cacheLaunchConfig(AsgData data, Map<String, CacheData> launchConfigs) {
launchConfigs[data.launchConfig].with {
relationships[SERVER_GROUPS.ns].add(data.serverGroup)
if (data.launchConfig) {
launchConfigs[data.launchConfig].with {
relationships[SERVER_GROUPS.ns].add(data.serverGroup)
}
}
}

Expand Down Expand Up @@ -649,6 +658,14 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
}
}

private void cacheLaunchTemplate(AsgData data, Map<String, CacheData> launchTemplates) {
if (data.launchTemplate) {
launchTemplates[data.launchTemplate].with {
relationships[SERVER_GROUPS.ns].add(data.serverGroup)
}
}
}

private AutoScalingGroup loadAutoScalingGroup(String autoScalingGroupName, boolean skipEdda) {
def autoScaling = amazonClientProvider.getAutoScaling(account, region, skipEdda)
def result = autoScaling.describeAutoScalingGroups(
Expand Down Expand Up @@ -703,6 +720,7 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
final String serverGroup
final String vpcId
final String launchConfig
final String launchTemplate
final Set<String> loadBalancerNames
final Set<String> targetGroupKeys
final Set<String> targetGroupNames
Expand Down Expand Up @@ -734,7 +752,12 @@ class ClusterCachingAgent implements CachingAgent, OnDemandAgent, AccountAware,
vpcId = vpcIds.first()
}
this.vpcId = vpcId
launchConfig = Keys.getLaunchConfigKey(asg.launchConfigurationName, account, region)
if (asg.launchTemplate) {
launchTemplate = Keys.getLaunchTemplateKey(asg.launchTemplate.launchTemplateName, account, region)
} else {
launchConfig = Keys.getLaunchConfigKey(asg.launchConfigurationName, account, region)
}

loadBalancerNames = (asg.loadBalancerNames.collect {
Keys.getLoadBalancerKey(it, account, region, vpcId, null)
} as Set).asImmutable()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.netflix.spinnaker.clouddriver.aws.AmazonCloudProvider
import com.netflix.spinnaker.clouddriver.aws.provider.agent.AmazonApplicationLoadBalancerCachingAgent
import com.netflix.spinnaker.clouddriver.aws.provider.agent.AmazonCertificateCachingAgent
import com.netflix.spinnaker.clouddriver.aws.provider.agent.AmazonCloudFormationCachingAgent
import com.netflix.spinnaker.clouddriver.aws.provider.agent.AmazonLaunchTemplateCachingAgent
import com.netflix.spinnaker.clouddriver.aws.provider.agent.AmazonLoadBalancerCachingAgent

import com.netflix.spinnaker.clouddriver.aws.provider.agent.ReservedInstancesCachingAgent
Expand Down Expand Up @@ -154,6 +155,10 @@ class AwsProviderConfig {
amazonClientProvider, credentials, region.name, objectMapper, ctx
)
}

if (dynamicConfigService.isEnabled("aws.features.launch-templates", false)) {
newlyAddedAgents << new AmazonLaunchTemplateCachingAgent(amazonClientProvider, credentials, region.name, objectMapper, registry)
}
}
}
}
Expand Down
Loading

0 comments on commit a76ff34

Please sign in to comment.