mirror of https://github.com/docker/compose.git
Run on EC2 when a service require GPU
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
parent
d42a931d67
commit
109ba96743
|
@ -23,8 +23,6 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose-cli/api/compose"
|
||||
|
||||
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
|
||||
|
@ -36,7 +34,6 @@ import (
|
|||
"github.com/awslabs/goformation/v4/cloudformation/logs"
|
||||
"github.com/awslabs/goformation/v4/cloudformation/secretsmanager"
|
||||
cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
|
||||
"github.com/awslabs/goformation/v4/cloudformation/tags"
|
||||
"github.com/compose-spec/compose-go/compatibility"
|
||||
"github.com/compose-spec/compose-go/errdefs"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
|
@ -52,7 +49,7 @@ const (
|
|||
)
|
||||
|
||||
func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
|
||||
template, err := b.convert(project)
|
||||
template, networks, err := b.convert(project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -97,11 +94,16 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]
|
|||
}
|
||||
}
|
||||
|
||||
err = b.createCapacityProvider(ctx, project, networks, template)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return marshall(template)
|
||||
}
|
||||
|
||||
// Convert a compose project into a CloudFormation template
|
||||
func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, error) { //nolint:gocyclo
|
||||
func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, map[string]string, error) { //nolint:gocyclo
|
||||
var checker compatibility.Checker = &fargateCompatibilityChecker{
|
||||
compatibility.AllowList{
|
||||
Supported: compatibleComposeAttributes,
|
||||
|
@ -116,7 +118,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||
}
|
||||
}
|
||||
if !compatibility.IsCompatible(checker) {
|
||||
return nil, fmt.Errorf("compose file is incompatible with Amazon ECS")
|
||||
return nil, nil, fmt.Errorf("compose file is incompatible with Amazon ECS")
|
||||
}
|
||||
|
||||
template := cloudformation.NewTemplate()
|
||||
|
@ -152,7 +154,6 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||
Description: "Name of the LoadBalancer to connect to (optional)",
|
||||
}
|
||||
|
||||
// Createmount.nfs4: Connection timed out : unsuccessful EFS utils command execution; code: 32 Cluster is `ParameterClusterName` parameter is not set
|
||||
template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(parameterClusterName))
|
||||
|
||||
cluster := createCluster(project, template)
|
||||
|
@ -168,19 +169,14 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||
}
|
||||
secret, err := ioutil.ReadFile(s.File)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name))
|
||||
template.Resources[name] = &secretsmanager.Secret{
|
||||
Description: "",
|
||||
SecretString: string(secret),
|
||||
Tags: []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
},
|
||||
Tags: projectTags(project),
|
||||
}
|
||||
s.Name = cloudformation.Ref(name)
|
||||
project.Secrets[i] = s
|
||||
|
@ -197,7 +193,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||
|
||||
definition, err := convert(project, service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
taskExecutionRole := createTaskExecutionRole(service, definition, template)
|
||||
|
@ -255,7 +251,14 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||
|
||||
minPercent, maxPercent, err := computeRollingUpdateLimits(service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
launchType := ecsapi.LaunchTypeFargate
|
||||
platformVersion := "1.4.0" // LATEST which is set to 1.3.0 (?) which doesn’t allow efs volumes.
|
||||
if requireEC2(service) {
|
||||
launchType = ecsapi.LaunchTypeEc2
|
||||
platformVersion = "" // The platform version must be null when specifying an EC2 launch type
|
||||
}
|
||||
|
||||
template.Resources[serviceResourceName(service.Name)] = &ecs.Service{
|
||||
|
@ -269,11 +272,12 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||
MaximumPercent: maxPercent,
|
||||
MinimumHealthyPercent: minPercent,
|
||||
},
|
||||
LaunchType: ecsapi.LaunchTypeFargate,
|
||||
LaunchType: launchType,
|
||||
// TODO we miss support for https://github.com/aws/containers-roadmap/issues/631 to select a capacity provider
|
||||
LoadBalancers: serviceLB,
|
||||
NetworkConfiguration: &ecs.Service_NetworkConfiguration{
|
||||
AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
|
||||
AssignPublicIp: ecsapi.AssignPublicIpEnabled,
|
||||
AssignPublicIp: ecsapi.AssignPublicIpDisabled,
|
||||
SecurityGroups: serviceSecurityGroups,
|
||||
Subnets: []string{
|
||||
cloudformation.Ref(parameterSubnet1Id),
|
||||
|
@ -281,24 +285,15 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||
},
|
||||
},
|
||||
},
|
||||
PlatformVersion: "1.4.0", // LATEST which is set to 1.3.0 (?) which doesn’t allow efs volumes.
|
||||
PlatformVersion: platformVersion,
|
||||
PropagateTags: ecsapi.PropagateTagsService,
|
||||
SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
|
||||
ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry},
|
||||
Tags: []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
{
|
||||
Key: compose.ServiceTag,
|
||||
Value: service.Name,
|
||||
},
|
||||
},
|
||||
TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)),
|
||||
Tags: serviceTags(project, service),
|
||||
TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)),
|
||||
}
|
||||
}
|
||||
return template, nil
|
||||
return template, networks, nil
|
||||
}
|
||||
|
||||
func createLogGroup(project *types.Project, template *cloudformation.Template) {
|
||||
|
@ -413,12 +408,7 @@ func createLoadBalancer(project *types.Project, template *cloudformation.Templat
|
|||
cloudformation.Ref(parameterSubnet1Id),
|
||||
cloudformation.Ref(parameterSubnet2Id),
|
||||
},
|
||||
Tags: []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
},
|
||||
Tags: projectTags(project),
|
||||
Type: loadBalancerType,
|
||||
AWSCloudFormationCondition: "CreateLoadBalancer",
|
||||
}
|
||||
|
@ -462,14 +452,9 @@ func createTargetGroup(project *types.Project, service types.ServiceConfig, port
|
|||
port.Published,
|
||||
)
|
||||
template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{
|
||||
Port: int(port.Target),
|
||||
Protocol: protocol,
|
||||
Tags: []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
},
|
||||
Port: int(port.Target),
|
||||
Protocol: protocol,
|
||||
Tags: projectTags(project),
|
||||
VpcId: cloudformation.Ref(parameterVPCId),
|
||||
TargetType: elbv2.TargetTypeEnumIp,
|
||||
}
|
||||
|
@ -507,7 +492,7 @@ func createTaskExecutionRole(service types.ServiceConfig, definition *ecs.TaskDe
|
|||
taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name))
|
||||
policies := createPolicies(service, definition)
|
||||
template.Resources[taskExecutionRole] = &iam.Role{
|
||||
AssumeRolePolicyDocument: assumeRolePolicyDocument,
|
||||
AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument,
|
||||
Policies: policies,
|
||||
ManagedPolicyArns: []string{
|
||||
ecsTaskExecutionPolicy,
|
||||
|
@ -535,7 +520,7 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa
|
|||
return ""
|
||||
}
|
||||
template.Resources[taskRole] = &iam.Role{
|
||||
AssumeRolePolicyDocument: assumeRolePolicyDocument,
|
||||
AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument,
|
||||
Policies: rolePolicies,
|
||||
ManagedPolicyArns: managedPolicies,
|
||||
}
|
||||
|
@ -544,13 +529,8 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa
|
|||
|
||||
func createCluster(project *types.Project, template *cloudformation.Template) string {
|
||||
template.Resources["Cluster"] = &ecs.Cluster{
|
||||
ClusterName: project.Name,
|
||||
Tags: []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
},
|
||||
ClusterName: project.Name,
|
||||
Tags: projectTags(project),
|
||||
AWSCloudFormationCondition: "CreateCluster",
|
||||
}
|
||||
cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(parameterClusterName))
|
||||
|
@ -598,16 +578,7 @@ func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string,
|
|||
GroupName: securityGroup,
|
||||
SecurityGroupIngress: ingresses,
|
||||
VpcId: vpc,
|
||||
Tags: []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
{
|
||||
Key: compose.NetworkTag,
|
||||
Value: net.Name,
|
||||
},
|
||||
},
|
||||
Tags: networkTags(project, net),
|
||||
}
|
||||
|
||||
ingress := securityGroup + "Ingress"
|
||||
|
|
|
@ -290,7 +290,7 @@ services:
|
|||
memory: 2043248M
|
||||
`)
|
||||
backend := &ecsAPIService{}
|
||||
_, err := backend.convert(model)
|
||||
_, _, err := backend.convert(model)
|
||||
assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate")
|
||||
}
|
||||
|
||||
|
@ -374,7 +374,7 @@ services:
|
|||
|
||||
func convertResultAsString(t *testing.T, project *types.Project) string {
|
||||
backend := &ecsAPIService{}
|
||||
template, err := backend.convert(project)
|
||||
template, _, err := backend.convert(project)
|
||||
assert.NilError(t, err)
|
||||
resultAsJSON, err := marshall(template)
|
||||
assert.NilError(t, err)
|
||||
|
@ -394,7 +394,7 @@ func load(t *testing.T, paths ...string) *types.Project {
|
|||
func convertYaml(t *testing.T, yaml string) *cloudformation.Template {
|
||||
project := loadConfig(t, yaml)
|
||||
backend := &ecsAPIService{}
|
||||
template, err := backend.convert(project)
|
||||
template, _, err := backend.convert(project)
|
||||
assert.NilError(t, err)
|
||||
return template
|
||||
}
|
||||
|
|
|
@ -102,6 +102,11 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var reservations *types.Resource
|
||||
if service.Deploy != nil && service.Deploy.Resources.Reservations != nil {
|
||||
reservations = service.Deploy.Resources.Reservations
|
||||
}
|
||||
|
||||
containers := append(initContainers, ecs.TaskDefinition_ContainerDefinition{
|
||||
Command: service.Command,
|
||||
DisableNetworking: service.NetworkMode == "none",
|
||||
|
@ -129,7 +134,7 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
|
|||
PseudoTerminal: service.Tty,
|
||||
ReadonlyRootFilesystem: service.ReadOnly,
|
||||
RepositoryCredentials: credential,
|
||||
ResourceRequirements: nil,
|
||||
ResourceRequirements: toTaskResourceRequirements(reservations),
|
||||
StartTimeout: 0,
|
||||
StopTimeout: durationToInt(service.StopGracePeriod),
|
||||
SystemControls: toSystemControls(service.Sysctls),
|
||||
|
@ -139,21 +144,44 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
|
|||
WorkingDirectory: service.WorkingDir,
|
||||
})
|
||||
|
||||
launchType := ecsapi.LaunchTypeFargate
|
||||
if requireEC2(service) {
|
||||
launchType = ecsapi.LaunchTypeEc2
|
||||
}
|
||||
|
||||
return &ecs.TaskDefinition{
|
||||
ContainerDefinitions: containers,
|
||||
Cpu: cpu,
|
||||
Family: fmt.Sprintf("%s-%s", project.Name, service.Name),
|
||||
IpcMode: service.Ipc,
|
||||
Memory: mem,
|
||||
NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’.
|
||||
PidMode: service.Pid,
|
||||
PlacementConstraints: toPlacementConstraints(service.Deploy),
|
||||
ProxyConfiguration: nil,
|
||||
RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate},
|
||||
Volumes: volumes,
|
||||
ContainerDefinitions: containers,
|
||||
Cpu: cpu,
|
||||
Family: fmt.Sprintf("%s-%s", project.Name, service.Name),
|
||||
IpcMode: service.Ipc,
|
||||
Memory: mem,
|
||||
NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’.
|
||||
PidMode: service.Pid,
|
||||
PlacementConstraints: toPlacementConstraints(service.Deploy),
|
||||
ProxyConfiguration: nil,
|
||||
RequiresCompatibilities: []string{
|
||||
launchType,
|
||||
},
|
||||
Volumes: volumes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toTaskResourceRequirements(reservations *types.Resource) []ecs.TaskDefinition_ResourceRequirement {
|
||||
if reservations == nil {
|
||||
return nil
|
||||
}
|
||||
var requirements []ecs.TaskDefinition_ResourceRequirement
|
||||
for _, r := range reservations.GenericResources {
|
||||
if r.DiscreteResourceSpec.Kind == "gpus" {
|
||||
requirements = append(requirements, ecs.TaskDefinition_ResourceRequirement{
|
||||
Type: ecsapi.ResourceTypeGpu,
|
||||
Value: fmt.Sprint(r.DiscreteResourceSpec.Value),
|
||||
})
|
||||
}
|
||||
}
|
||||
return requirements
|
||||
}
|
||||
|
||||
func createSecretsSideCar(project *types.Project, service types.ServiceConfig, logConfiguration *ecs.TaskDefinition_LogConfiguration) (
|
||||
ecs.TaskDefinition_Volume,
|
||||
ecs.TaskDefinition_MountPoint,
|
||||
|
@ -295,7 +323,7 @@ func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl
|
|||
const miB = 1024 * 1024
|
||||
|
||||
func toLimits(service types.ServiceConfig) (string, string, error) {
|
||||
// All possible cpu/mem values for Fargate
|
||||
// All possible CPU/mem values for Fargate
|
||||
cpuToMem := map[int64][]types.UnitBytes{
|
||||
256: {512, 1024, 2048},
|
||||
512: {1024, 2048, 3072, 4096},
|
||||
|
@ -490,3 +518,20 @@ func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_Reposit
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func requireEC2(s types.ServiceConfig) bool {
|
||||
return gpuRequirements(s) > 0
|
||||
}
|
||||
|
||||
func gpuRequirements(s types.ServiceConfig) int64 {
|
||||
if deploy := s.Deploy; deploy != nil {
|
||||
if reservations := deploy.Resources.Reservations; reservations != nil {
|
||||
for _, resource := range reservations.GenericResources {
|
||||
if resource.DiscreteResourceSpec.Kind == "gpus" {
|
||||
return resource.DiscreteResourceSpec.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2020 Docker, 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 ecs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/awslabs/goformation/v4/cloudformation"
|
||||
"github.com/awslabs/goformation/v4/cloudformation/autoscaling"
|
||||
"github.com/awslabs/goformation/v4/cloudformation/ecs"
|
||||
"github.com/awslabs/goformation/v4/cloudformation/iam"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
)
|
||||
|
||||
func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *types.Project, networks map[string]string, template *cloudformation.Template) error {
|
||||
var ec2 bool
|
||||
for _, s := range project.Services {
|
||||
if requireEC2(s) {
|
||||
ec2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ec2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ami, err := b.SDK.GetParameter(ctx, "/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
machineType := "g4dn.xlarge" // FIXME https://github.com/docker/compose-cli/pull/628
|
||||
|
||||
var securityGroups []string
|
||||
for _, r := range networks {
|
||||
securityGroups = append(securityGroups, r)
|
||||
}
|
||||
|
||||
template.Resources["CapacityProvider"] = &ecs.CapacityProvider{
|
||||
AutoScalingGroupProvider: &ecs.CapacityProvider_AutoScalingGroupProvider{
|
||||
AutoScalingGroupArn: cloudformation.Ref("AutoscalingGroup"),
|
||||
ManagedScaling: &ecs.CapacityProvider_ManagedScaling{
|
||||
TargetCapacity: 100,
|
||||
},
|
||||
},
|
||||
Tags: projectTags(project),
|
||||
AWSCloudFormationCondition: "CreateCluster",
|
||||
}
|
||||
|
||||
template.Resources["AutoscalingGroup"] = &autoscaling.AutoScalingGroup{
|
||||
LaunchConfigurationName: cloudformation.Ref("LaunchConfiguration"),
|
||||
MaxSize: "10", //TODO
|
||||
MinSize: "1",
|
||||
VPCZoneIdentifier: []string{
|
||||
cloudformation.Ref(parameterSubnet1Id),
|
||||
cloudformation.Ref(parameterSubnet2Id),
|
||||
},
|
||||
AWSCloudFormationCondition: "CreateCluster",
|
||||
}
|
||||
|
||||
userData := base64.StdEncoding.EncodeToString([]byte(
|
||||
fmt.Sprintf("#!/bin/bash\necho ECS_CLUSTER=%s >> /etc/ecs/ecs.config", project.Name)))
|
||||
|
||||
template.Resources["LaunchConfiguration"] = &autoscaling.LaunchConfiguration{
|
||||
ImageId: ami,
|
||||
InstanceType: machineType,
|
||||
SecurityGroups: securityGroups,
|
||||
IamInstanceProfile: cloudformation.Ref("EC2InstanceProfile"),
|
||||
UserData: userData,
|
||||
AWSCloudFormationCondition: "CreateCluster",
|
||||
}
|
||||
|
||||
template.Resources["EC2InstanceProfile"] = &iam.InstanceProfile{
|
||||
Roles: []string{cloudformation.Ref("EC2InstanceRole")},
|
||||
AWSCloudFormationCondition: "CreateCluster",
|
||||
}
|
||||
|
||||
template.Resources["EC2InstanceRole"] = &iam.Role{
|
||||
AssumeRolePolicyDocument: ec2InstanceAssumeRolePolicyDocument,
|
||||
ManagedPolicyArns: []string{
|
||||
ecsEC2InstanceRole,
|
||||
},
|
||||
Tags: projectTags(project),
|
||||
AWSCloudFormationCondition: "CreateCluster",
|
||||
}
|
||||
|
||||
cluster := template.Resources["Cluster"].(*ecs.Cluster)
|
||||
cluster.CapacityProviders = []string{
|
||||
cloudformation.Ref("CapacityProvider"),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
46
ecs/gpu.go
46
ecs/gpu.go
|
@ -34,23 +34,47 @@ type machine struct {
|
|||
|
||||
type family []machine
|
||||
|
||||
var p3family = family{
|
||||
var gpufamily = family{
|
||||
{
|
||||
id: "p3.2xlarge",
|
||||
cpus: 8,
|
||||
memory: 64 * units.GiB,
|
||||
gpus: 2,
|
||||
id: "g4dn.xlarge",
|
||||
cpus: 4,
|
||||
memory: 16 * units.GiB,
|
||||
gpus: 1,
|
||||
},
|
||||
{
|
||||
id: "p3.8xlarge",
|
||||
id: "g4dn.2xlarge",
|
||||
cpus: 8,
|
||||
memory: 32 * units.GiB,
|
||||
gpus: 1,
|
||||
},
|
||||
{
|
||||
id: "g4dn.4xlarge",
|
||||
cpus: 16,
|
||||
memory: 64 * units.GiB,
|
||||
gpus: 1,
|
||||
},
|
||||
{
|
||||
id: "g4dn.8xlarge",
|
||||
cpus: 32,
|
||||
memory: 244 * units.GiB,
|
||||
memory: 128 * units.GiB,
|
||||
gpus: 1,
|
||||
},
|
||||
{
|
||||
id: "g4dn.12xlarge",
|
||||
cpus: 48,
|
||||
memory: 192 * units.GiB,
|
||||
gpus: 4,
|
||||
},
|
||||
{
|
||||
id: "p3.16xlarge",
|
||||
id: "g4dn.16xlarge",
|
||||
cpus: 64,
|
||||
memory: 488 * units.GiB,
|
||||
memory: 256 * units.GiB,
|
||||
gpus: 1,
|
||||
},
|
||||
{
|
||||
id: "g4dn.metal",
|
||||
cpus: 96,
|
||||
memory: 384 * units.GiB,
|
||||
gpus: 8,
|
||||
},
|
||||
}
|
||||
|
@ -82,7 +106,7 @@ func guessMachineType(project *types.Project) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
instanceType, err := p3family.
|
||||
instanceType, err := gpufamily.
|
||||
filter(func(m machine) bool {
|
||||
return m.memory >= requirements.memory
|
||||
}).
|
||||
|
@ -92,7 +116,7 @@ func guessMachineType(project *types.Project) (string, error) {
|
|||
filter(func(m machine) bool {
|
||||
return m.gpus >= requirements.gpus
|
||||
}).
|
||||
firstOrError("none of the Amazon EC2 P3 instance types meet the requirements for memory:%d cpu:%f gpus:%d", requirements.memory, requirements.cpus, requirements.gpus)
|
||||
firstOrError("none of the Amazon EC2 G4 instance types meet the requirements for memory:%d cpu:%f gpus:%d", requirements.memory, requirements.cpus, requirements.gpus)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ services:
|
|||
kind: gpus
|
||||
value: 1
|
||||
`,
|
||||
want: "p3.2xlarge",
|
||||
want: "g4dn.xlarge",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
|
@ -58,7 +58,7 @@ services:
|
|||
kind: gpus
|
||||
value: 4
|
||||
`,
|
||||
want: "p3.8xlarge",
|
||||
want: "g4dn.12xlarge",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
|
@ -76,7 +76,7 @@ services:
|
|||
kind: gpus
|
||||
value: 2
|
||||
`,
|
||||
want: "p3.16xlarge",
|
||||
want: "g4dn.metal",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
|
@ -95,7 +95,7 @@ services:
|
|||
kind: gpus
|
||||
value: 2
|
||||
`,
|
||||
want: "p3.8xlarge",
|
||||
want: "g4dn.12xlarge",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
|
16
ecs/iam.go
16
ecs/iam.go
|
@ -19,13 +19,14 @@ package ecs
|
|||
const (
|
||||
ecsTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
||||
ecrReadOnlyPolicy = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
|
||||
ecsEC2InstanceRole = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
|
||||
|
||||
actionGetSecretValue = "secretsmanager:GetSecretValue"
|
||||
actionGetParameters = "ssm:GetParameters"
|
||||
actionDecrypt = "kms:Decrypt"
|
||||
)
|
||||
|
||||
var assumeRolePolicyDocument = PolicyDocument{
|
||||
var ecsTaskAssumeRolePolicyDocument = PolicyDocument{
|
||||
Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html
|
||||
Statement: []PolicyStatement{
|
||||
{
|
||||
|
@ -38,6 +39,19 @@ var assumeRolePolicyDocument = PolicyDocument{
|
|||
},
|
||||
}
|
||||
|
||||
var ec2InstanceAssumeRolePolicyDocument = PolicyDocument{
|
||||
Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html
|
||||
Statement: []PolicyStatement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: PolicyPrincipal{
|
||||
Service: "ec2.amazonaws.com",
|
||||
},
|
||||
Action: []string{"sts:AssumeRole"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// PolicyDocument describes an IAM policy document
|
||||
// could alternatively depend on https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/master/cmd/clusterawsadm/api/iam/v1alpha1/types.go
|
||||
type PolicyDocument struct {
|
||||
|
|
35
ecs/sdk.go
35
ecs/sdk.go
|
@ -18,10 +18,14 @@ package ecs
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ssm"
|
||||
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
|
||||
|
||||
"github.com/docker/compose-cli/api/compose"
|
||||
"github.com/docker/compose-cli/api/secrets"
|
||||
|
||||
|
@ -56,6 +60,7 @@ type sdk struct {
|
|||
IAM iamiface.IAMAPI
|
||||
CF cloudformationiface.CloudFormationAPI
|
||||
SM secretsmanageriface.SecretsManagerAPI
|
||||
SSM ssmiface.SSMAPI
|
||||
}
|
||||
|
||||
func newSDK(sess *session.Session) sdk {
|
||||
|
@ -71,6 +76,7 @@ func newSDK(sess *session.Session) sdk {
|
|||
IAM: iam.New(sess),
|
||||
CF: cloudformation.New(sess),
|
||||
SM: secretsmanager.New(sess),
|
||||
SSM: ssm.New(sess),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +92,7 @@ func (s sdk) CheckRequirements(ctx context.Context, region string) error {
|
|||
if *serviceLongArnFormat != "enabled" {
|
||||
return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+
|
||||
"Check https://%s.console.aws.amazon.com/ecs/home#/settings\n"+
|
||||
"Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2", region)
|
||||
"Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-ID-format-2", region)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -182,7 +188,7 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
|
|||
StackName: aws.String(name),
|
||||
})
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with id %s does not exist", name)) {
|
||||
if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with ID %s does not exist", name)) {
|
||||
return false, nil
|
||||
}
|
||||
return false, nil
|
||||
|
@ -688,3 +694,28 @@ func (s sdk) WithVolumeSecurityGroups(ctx context.Context, id string, fn func(se
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
|
||||
parameter, err := s.SSM.GetParameterWithContext(ctx, &ssm.GetParameterInput{
|
||||
Name: aws.String(name),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
value := *parameter.Parameter.Value
|
||||
var ami struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
ImageName string `json:"image_name"`
|
||||
ImageID string `json:"image_id"`
|
||||
OS string `json:"os"`
|
||||
ECSRuntimeVersion string `json:"ecs_runtime_verion"`
|
||||
ECSAgentVersion string `json:"ecs_agent_version"`
|
||||
}
|
||||
err = json.Unmarshal([]byte(value), &ami)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ami.ImageID, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
Copyright 2020 Docker, 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 ecs
|
||||
|
||||
import (
|
||||
"github.com/awslabs/goformation/v4/cloudformation/tags"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose-cli/api/compose"
|
||||
)
|
||||
|
||||
func projectTags(project *types.Project) []tags.Tag {
|
||||
return []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func serviceTags(project *types.Project, service types.ServiceConfig) []tags.Tag {
|
||||
return []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
{
|
||||
Key: compose.ServiceTag,
|
||||
Value: service.Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func networkTags(project *types.Project, net types.NetworkConfig) []tags.Tag {
|
||||
return []tags.Tag{
|
||||
{
|
||||
Key: compose.ProjectTag,
|
||||
Value: project.Name,
|
||||
},
|
||||
{
|
||||
Key: compose.NetworkTag,
|
||||
Value: net.Name,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -107,7 +107,7 @@
|
|||
],
|
||||
"NetworkConfiguration": {
|
||||
"AwsvpcConfiguration": {
|
||||
"AssignPublicIp": "ENABLED",
|
||||
"AssignPublicIp": "DISABLED",
|
||||
"SecurityGroups": [
|
||||
{
|
||||
"Ref": "TestSimpleConvertDefaultNetwork"
|
||||
|
|
Loading…
Reference in New Issue