mirror of
https://github.com/docker/compose.git
synced 2025-07-22 13:14:29 +02:00
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"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/compose-cli/api/compose"
|
|
||||||
|
|
||||||
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
|
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
|
||||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||||
cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
|
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/logs"
|
||||||
"github.com/awslabs/goformation/v4/cloudformation/secretsmanager"
|
"github.com/awslabs/goformation/v4/cloudformation/secretsmanager"
|
||||||
cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
|
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/compatibility"
|
||||||
"github.com/compose-spec/compose-go/errdefs"
|
"github.com/compose-spec/compose-go/errdefs"
|
||||||
"github.com/compose-spec/compose-go/types"
|
"github.com/compose-spec/compose-go/types"
|
||||||
@ -52,7 +49,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
return marshall(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert a compose project into a CloudFormation 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{
|
var checker compatibility.Checker = &fargateCompatibilityChecker{
|
||||||
compatibility.AllowList{
|
compatibility.AllowList{
|
||||||
Supported: compatibleComposeAttributes,
|
Supported: compatibleComposeAttributes,
|
||||||
@ -116,7 +118,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !compatibility.IsCompatible(checker) {
|
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()
|
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)",
|
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))
|
template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(parameterClusterName))
|
||||||
|
|
||||||
cluster := createCluster(project, template)
|
cluster := createCluster(project, template)
|
||||||
@ -168,19 +169,14 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||||||
}
|
}
|
||||||
secret, err := ioutil.ReadFile(s.File)
|
secret, err := ioutil.ReadFile(s.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name))
|
name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name))
|
||||||
template.Resources[name] = &secretsmanager.Secret{
|
template.Resources[name] = &secretsmanager.Secret{
|
||||||
Description: "",
|
Description: "",
|
||||||
SecretString: string(secret),
|
SecretString: string(secret),
|
||||||
Tags: []tags.Tag{
|
Tags: projectTags(project),
|
||||||
{
|
|
||||||
Key: compose.ProjectTag,
|
|
||||||
Value: project.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
s.Name = cloudformation.Ref(name)
|
s.Name = cloudformation.Ref(name)
|
||||||
project.Secrets[i] = s
|
project.Secrets[i] = s
|
||||||
@ -197,7 +193,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||||||
|
|
||||||
definition, err := convert(project, service)
|
definition, err := convert(project, service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
taskExecutionRole := createTaskExecutionRole(service, definition, template)
|
taskExecutionRole := createTaskExecutionRole(service, definition, template)
|
||||||
@ -255,7 +251,14 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||||||
|
|
||||||
minPercent, maxPercent, err := computeRollingUpdateLimits(service)
|
minPercent, maxPercent, err := computeRollingUpdateLimits(service)
|
||||||
if err != nil {
|
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{
|
template.Resources[serviceResourceName(service.Name)] = &ecs.Service{
|
||||||
@ -269,11 +272,12 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
|
|||||||
MaximumPercent: maxPercent,
|
MaximumPercent: maxPercent,
|
||||||
MinimumHealthyPercent: minPercent,
|
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,
|
LoadBalancers: serviceLB,
|
||||||
NetworkConfiguration: &ecs.Service_NetworkConfiguration{
|
NetworkConfiguration: &ecs.Service_NetworkConfiguration{
|
||||||
AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
|
AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
|
||||||
AssignPublicIp: ecsapi.AssignPublicIpEnabled,
|
AssignPublicIp: ecsapi.AssignPublicIpDisabled,
|
||||||
SecurityGroups: serviceSecurityGroups,
|
SecurityGroups: serviceSecurityGroups,
|
||||||
Subnets: []string{
|
Subnets: []string{
|
||||||
cloudformation.Ref(parameterSubnet1Id),
|
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,
|
PropagateTags: ecsapi.PropagateTagsService,
|
||||||
SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
|
SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
|
||||||
ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry},
|
ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry},
|
||||||
Tags: []tags.Tag{
|
Tags: serviceTags(project, service),
|
||||||
{
|
TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)),
|
||||||
Key: compose.ProjectTag,
|
|
||||||
Value: project.Name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: compose.ServiceTag,
|
|
||||||
Value: service.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return template, nil
|
return template, networks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createLogGroup(project *types.Project, template *cloudformation.Template) {
|
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(parameterSubnet1Id),
|
||||||
cloudformation.Ref(parameterSubnet2Id),
|
cloudformation.Ref(parameterSubnet2Id),
|
||||||
},
|
},
|
||||||
Tags: []tags.Tag{
|
Tags: projectTags(project),
|
||||||
{
|
|
||||||
Key: compose.ProjectTag,
|
|
||||||
Value: project.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Type: loadBalancerType,
|
Type: loadBalancerType,
|
||||||
AWSCloudFormationCondition: "CreateLoadBalancer",
|
AWSCloudFormationCondition: "CreateLoadBalancer",
|
||||||
}
|
}
|
||||||
@ -462,14 +452,9 @@ func createTargetGroup(project *types.Project, service types.ServiceConfig, port
|
|||||||
port.Published,
|
port.Published,
|
||||||
)
|
)
|
||||||
template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{
|
template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{
|
||||||
Port: int(port.Target),
|
Port: int(port.Target),
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
Tags: []tags.Tag{
|
Tags: projectTags(project),
|
||||||
{
|
|
||||||
Key: compose.ProjectTag,
|
|
||||||
Value: project.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
VpcId: cloudformation.Ref(parameterVPCId),
|
VpcId: cloudformation.Ref(parameterVPCId),
|
||||||
TargetType: elbv2.TargetTypeEnumIp,
|
TargetType: elbv2.TargetTypeEnumIp,
|
||||||
}
|
}
|
||||||
@ -507,7 +492,7 @@ func createTaskExecutionRole(service types.ServiceConfig, definition *ecs.TaskDe
|
|||||||
taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name))
|
taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name))
|
||||||
policies := createPolicies(service, definition)
|
policies := createPolicies(service, definition)
|
||||||
template.Resources[taskExecutionRole] = &iam.Role{
|
template.Resources[taskExecutionRole] = &iam.Role{
|
||||||
AssumeRolePolicyDocument: assumeRolePolicyDocument,
|
AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument,
|
||||||
Policies: policies,
|
Policies: policies,
|
||||||
ManagedPolicyArns: []string{
|
ManagedPolicyArns: []string{
|
||||||
ecsTaskExecutionPolicy,
|
ecsTaskExecutionPolicy,
|
||||||
@ -535,7 +520,7 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
template.Resources[taskRole] = &iam.Role{
|
template.Resources[taskRole] = &iam.Role{
|
||||||
AssumeRolePolicyDocument: assumeRolePolicyDocument,
|
AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument,
|
||||||
Policies: rolePolicies,
|
Policies: rolePolicies,
|
||||||
ManagedPolicyArns: managedPolicies,
|
ManagedPolicyArns: managedPolicies,
|
||||||
}
|
}
|
||||||
@ -544,13 +529,8 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa
|
|||||||
|
|
||||||
func createCluster(project *types.Project, template *cloudformation.Template) string {
|
func createCluster(project *types.Project, template *cloudformation.Template) string {
|
||||||
template.Resources["Cluster"] = &ecs.Cluster{
|
template.Resources["Cluster"] = &ecs.Cluster{
|
||||||
ClusterName: project.Name,
|
ClusterName: project.Name,
|
||||||
Tags: []tags.Tag{
|
Tags: projectTags(project),
|
||||||
{
|
|
||||||
Key: compose.ProjectTag,
|
|
||||||
Value: project.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
AWSCloudFormationCondition: "CreateCluster",
|
AWSCloudFormationCondition: "CreateCluster",
|
||||||
}
|
}
|
||||||
cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(parameterClusterName))
|
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,
|
GroupName: securityGroup,
|
||||||
SecurityGroupIngress: ingresses,
|
SecurityGroupIngress: ingresses,
|
||||||
VpcId: vpc,
|
VpcId: vpc,
|
||||||
Tags: []tags.Tag{
|
Tags: networkTags(project, net),
|
||||||
{
|
|
||||||
Key: compose.ProjectTag,
|
|
||||||
Value: project.Name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: compose.NetworkTag,
|
|
||||||
Value: net.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ingress := securityGroup + "Ingress"
|
ingress := securityGroup + "Ingress"
|
||||||
|
@ -290,7 +290,7 @@ services:
|
|||||||
memory: 2043248M
|
memory: 2043248M
|
||||||
`)
|
`)
|
||||||
backend := &ecsAPIService{}
|
backend := &ecsAPIService{}
|
||||||
_, err := backend.convert(model)
|
_, _, err := backend.convert(model)
|
||||||
assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate")
|
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 {
|
func convertResultAsString(t *testing.T, project *types.Project) string {
|
||||||
backend := &ecsAPIService{}
|
backend := &ecsAPIService{}
|
||||||
template, err := backend.convert(project)
|
template, _, err := backend.convert(project)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
resultAsJSON, err := marshall(template)
|
resultAsJSON, err := marshall(template)
|
||||||
assert.NilError(t, err)
|
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 {
|
func convertYaml(t *testing.T, yaml string) *cloudformation.Template {
|
||||||
project := loadConfig(t, yaml)
|
project := loadConfig(t, yaml)
|
||||||
backend := &ecsAPIService{}
|
backend := &ecsAPIService{}
|
||||||
template, err := backend.convert(project)
|
template, _, err := backend.convert(project)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,11 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
|
|||||||
return nil, err
|
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{
|
containers := append(initContainers, ecs.TaskDefinition_ContainerDefinition{
|
||||||
Command: service.Command,
|
Command: service.Command,
|
||||||
DisableNetworking: service.NetworkMode == "none",
|
DisableNetworking: service.NetworkMode == "none",
|
||||||
@ -129,7 +134,7 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
|
|||||||
PseudoTerminal: service.Tty,
|
PseudoTerminal: service.Tty,
|
||||||
ReadonlyRootFilesystem: service.ReadOnly,
|
ReadonlyRootFilesystem: service.ReadOnly,
|
||||||
RepositoryCredentials: credential,
|
RepositoryCredentials: credential,
|
||||||
ResourceRequirements: nil,
|
ResourceRequirements: toTaskResourceRequirements(reservations),
|
||||||
StartTimeout: 0,
|
StartTimeout: 0,
|
||||||
StopTimeout: durationToInt(service.StopGracePeriod),
|
StopTimeout: durationToInt(service.StopGracePeriod),
|
||||||
SystemControls: toSystemControls(service.Sysctls),
|
SystemControls: toSystemControls(service.Sysctls),
|
||||||
@ -139,21 +144,44 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
|
|||||||
WorkingDirectory: service.WorkingDir,
|
WorkingDirectory: service.WorkingDir,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
launchType := ecsapi.LaunchTypeFargate
|
||||||
|
if requireEC2(service) {
|
||||||
|
launchType = ecsapi.LaunchTypeEc2
|
||||||
|
}
|
||||||
|
|
||||||
return &ecs.TaskDefinition{
|
return &ecs.TaskDefinition{
|
||||||
ContainerDefinitions: containers,
|
ContainerDefinitions: containers,
|
||||||
Cpu: cpu,
|
Cpu: cpu,
|
||||||
Family: fmt.Sprintf("%s-%s", project.Name, service.Name),
|
Family: fmt.Sprintf("%s-%s", project.Name, service.Name),
|
||||||
IpcMode: service.Ipc,
|
IpcMode: service.Ipc,
|
||||||
Memory: mem,
|
Memory: mem,
|
||||||
NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’.
|
NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’.
|
||||||
PidMode: service.Pid,
|
PidMode: service.Pid,
|
||||||
PlacementConstraints: toPlacementConstraints(service.Deploy),
|
PlacementConstraints: toPlacementConstraints(service.Deploy),
|
||||||
ProxyConfiguration: nil,
|
ProxyConfiguration: nil,
|
||||||
RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate},
|
RequiresCompatibilities: []string{
|
||||||
Volumes: volumes,
|
launchType,
|
||||||
|
},
|
||||||
|
Volumes: volumes,
|
||||||
}, nil
|
}, 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) (
|
func createSecretsSideCar(project *types.Project, service types.ServiceConfig, logConfiguration *ecs.TaskDefinition_LogConfiguration) (
|
||||||
ecs.TaskDefinition_Volume,
|
ecs.TaskDefinition_Volume,
|
||||||
ecs.TaskDefinition_MountPoint,
|
ecs.TaskDefinition_MountPoint,
|
||||||
@ -295,7 +323,7 @@ func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl
|
|||||||
const miB = 1024 * 1024
|
const miB = 1024 * 1024
|
||||||
|
|
||||||
func toLimits(service types.ServiceConfig) (string, string, error) {
|
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{
|
cpuToMem := map[int64][]types.UnitBytes{
|
||||||
256: {512, 1024, 2048},
|
256: {512, 1024, 2048},
|
||||||
512: {1024, 2048, 3072, 4096},
|
512: {1024, 2048, 3072, 4096},
|
||||||
@ -490,3 +518,20 @@ func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_Reposit
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
110
ecs/ec2.go
Normal file
110
ecs/ec2.go
Normal file
@ -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
|
type family []machine
|
||||||
|
|
||||||
var p3family = family{
|
var gpufamily = family{
|
||||||
{
|
{
|
||||||
id: "p3.2xlarge",
|
id: "g4dn.xlarge",
|
||||||
cpus: 8,
|
cpus: 4,
|
||||||
memory: 64 * units.GiB,
|
memory: 16 * units.GiB,
|
||||||
gpus: 2,
|
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,
|
cpus: 32,
|
||||||
memory: 244 * units.GiB,
|
memory: 128 * units.GiB,
|
||||||
|
gpus: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "g4dn.12xlarge",
|
||||||
|
cpus: 48,
|
||||||
|
memory: 192 * units.GiB,
|
||||||
gpus: 4,
|
gpus: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "p3.16xlarge",
|
id: "g4dn.16xlarge",
|
||||||
cpus: 64,
|
cpus: 64,
|
||||||
memory: 488 * units.GiB,
|
memory: 256 * units.GiB,
|
||||||
|
gpus: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "g4dn.metal",
|
||||||
|
cpus: 96,
|
||||||
|
memory: 384 * units.GiB,
|
||||||
gpus: 8,
|
gpus: 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -82,7 +106,7 @@ func guessMachineType(project *types.Project) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceType, err := p3family.
|
instanceType, err := gpufamily.
|
||||||
filter(func(m machine) bool {
|
filter(func(m machine) bool {
|
||||||
return m.memory >= requirements.memory
|
return m.memory >= requirements.memory
|
||||||
}).
|
}).
|
||||||
@ -92,7 +116,7 @@ func guessMachineType(project *types.Project) (string, error) {
|
|||||||
filter(func(m machine) bool {
|
filter(func(m machine) bool {
|
||||||
return m.gpus >= requirements.gpus
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ services:
|
|||||||
kind: gpus
|
kind: gpus
|
||||||
value: 1
|
value: 1
|
||||||
`,
|
`,
|
||||||
want: "p3.2xlarge",
|
want: "g4dn.xlarge",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -58,7 +58,7 @@ services:
|
|||||||
kind: gpus
|
kind: gpus
|
||||||
value: 4
|
value: 4
|
||||||
`,
|
`,
|
||||||
want: "p3.8xlarge",
|
want: "g4dn.12xlarge",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -76,7 +76,7 @@ services:
|
|||||||
kind: gpus
|
kind: gpus
|
||||||
value: 2
|
value: 2
|
||||||
`,
|
`,
|
||||||
want: "p3.16xlarge",
|
want: "g4dn.metal",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -95,7 +95,7 @@ services:
|
|||||||
kind: gpus
|
kind: gpus
|
||||||
value: 2
|
value: 2
|
||||||
`,
|
`,
|
||||||
want: "p3.8xlarge",
|
want: "g4dn.12xlarge",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
16
ecs/iam.go
16
ecs/iam.go
@ -19,13 +19,14 @@ package ecs
|
|||||||
const (
|
const (
|
||||||
ecsTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
ecsTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
||||||
ecrReadOnlyPolicy = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
|
ecrReadOnlyPolicy = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
|
||||||
|
ecsEC2InstanceRole = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
|
||||||
|
|
||||||
actionGetSecretValue = "secretsmanager:GetSecretValue"
|
actionGetSecretValue = "secretsmanager:GetSecretValue"
|
||||||
actionGetParameters = "ssm:GetParameters"
|
actionGetParameters = "ssm:GetParameters"
|
||||||
actionDecrypt = "kms:Decrypt"
|
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
|
Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html
|
||||||
Statement: []PolicyStatement{
|
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
|
// 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
|
// 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 {
|
type PolicyDocument struct {
|
||||||
|
35
ecs/sdk.go
35
ecs/sdk.go
@ -18,10 +18,14 @@ package ecs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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/compose"
|
||||||
"github.com/docker/compose-cli/api/secrets"
|
"github.com/docker/compose-cli/api/secrets"
|
||||||
|
|
||||||
@ -56,6 +60,7 @@ type sdk struct {
|
|||||||
IAM iamiface.IAMAPI
|
IAM iamiface.IAMAPI
|
||||||
CF cloudformationiface.CloudFormationAPI
|
CF cloudformationiface.CloudFormationAPI
|
||||||
SM secretsmanageriface.SecretsManagerAPI
|
SM secretsmanageriface.SecretsManagerAPI
|
||||||
|
SSM ssmiface.SSMAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSDK(sess *session.Session) sdk {
|
func newSDK(sess *session.Session) sdk {
|
||||||
@ -71,6 +76,7 @@ func newSDK(sess *session.Session) sdk {
|
|||||||
IAM: iam.New(sess),
|
IAM: iam.New(sess),
|
||||||
CF: cloudformation.New(sess),
|
CF: cloudformation.New(sess),
|
||||||
SM: secretsmanager.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" {
|
if *serviceLongArnFormat != "enabled" {
|
||||||
return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+
|
return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+
|
||||||
"Check https://%s.console.aws.amazon.com/ecs/home#/settings\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
|
return nil
|
||||||
}
|
}
|
||||||
@ -182,7 +188,7 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
|
|||||||
StackName: aws.String(name),
|
StackName: aws.String(name),
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -688,3 +694,28 @@ func (s sdk) WithVolumeSecurityGroups(ctx context.Context, id string, fn func(se
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
58
ecs/tags.go
Normal file
58
ecs/tags.go
Normal file
@ -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": {
|
"NetworkConfiguration": {
|
||||||
"AwsvpcConfiguration": {
|
"AwsvpcConfiguration": {
|
||||||
"AssignPublicIp": "ENABLED",
|
"AssignPublicIp": "DISABLED",
|
||||||
"SecurityGroups": [
|
"SecurityGroups": [
|
||||||
{
|
{
|
||||||
"Ref": "TestSimpleConvertDefaultNetwork"
|
"Ref": "TestSimpleConvertDefaultNetwork"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user