Implement Hostname-only service discovery using LOCALDOMAIN

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2020-05-12 09:41:29 +02:00
parent 0864513bfe
commit 4ab37f8229
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
4 changed files with 46 additions and 10 deletions

View File

@ -39,3 +39,27 @@ file do not include unsupported features.
* _Convert_ produces a CloudFormation template to define all resources required to implement the application model on AWS.
* _Apply_ phase do apply the CloudFormation template, either by exporting to a stack file or to deploy on AWS.
## Application model
### Services
Compose services are mapped to ECS services. Compose specification has no support for multi-container services, nor
does it support sidecars. When an ECS feature requires a sidecar, we introduce custom Compose extension (`x-aws-*`)
to actually expose ECS feature as a service-level feature, not plumbing details.
### Networking
We map the "network" abstraction from Compose model to AWS SecurityGroups. The whole application is created within a
single VPC, SecurityGroups are created per networks, including the implicit `default` one. Services are attached
according to the networks declared in Compose model. Doing so, services attached to a common security group can
communicate together, while services from distinct SecurityGroups can't. We just can't set service aliasses per network.
A CloudMap private namespace is created for application as `{project}.local`. Services get registered so that we
get service discovery and DNS round-robin (equivalent for Compose's `endpoint_mode: dnsrr`). Hostname-only service
discovery is enabled by running application containers with `LOCALDOMAIN={project}.local`
(see [resolv.conf(5)](http://man7.org/linux/man-pages/man5/resolv.conf.5.html)). This works out-of-the-box for
debian-based Docker images. Alpine images have to include a tiny entrypoint script to replicate this feature:
```shell script
if [ $LOCALDOMAIN ]; then echo "search ${LOCALDOMAIN}" >> /etc/resolv.conf; fi
```

View File

@ -5,7 +5,7 @@ require (
github.com/Microsoft/hcsshim v0.8.7 // indirect
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/aws/aws-sdk-go v1.28.9
github.com/aws/aws-sdk-go v1.30.22
github.com/awslabs/goformation/v4 v4.8.0
github.com/bitly/go-hostpool v0.1.0 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
@ -24,7 +24,6 @@ require (
github.com/docker/go v1.5.1-1 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/gofrs/uuid v3.2.0+incompatible // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/mock v1.4.3

View File

@ -21,6 +21,9 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.28.9 h1:grIuBQc+p3dTRXerh5+2OxSuWFi0iXuxbFdTSg0jaW0=
github.com/aws/aws-sdk-go v1.28.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.30.2/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.30.22 h1:wImJ8jQrplgmxaTeUY7FrJFn4te/VtWq+mmmJ1TnWAg=
github.com/aws/aws-sdk-go v1.30.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc=
github.com/awslabs/goformation/v4 v4.8.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -161,6 +164,8 @@ github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo=
github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@ -300,6 +305,7 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
@ -357,6 +363,7 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYR
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=

View File

@ -1,11 +1,12 @@
package amazon
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
"github.com/awslabs/goformation/v4/cloudformation"
"github.com/awslabs/goformation/v4/cloudformation/ecs"
@ -21,9 +22,17 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe
}
credential := getRepoCredentials(service)
// override resolve.conf search directive to also search <project>.local
// TODO remove once ECS support hostname-only service discovery
service.Environment["LOCALDOMAIN"] = aws.String(
cloudformation.Join("", []string{
cloudformation.Ref("AWS::Region"),
".compute.internal",
fmt.Sprintf(" %s.local", project.Name),
}))
return &ecs.TaskDefinition{
ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{
// Here we can declare sidecars and init-containers using https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_dependson
{
Command: service.Command,
DisableNetworking: service.NetworkMode == "none",
@ -50,7 +59,6 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe
"awslogs-stream-prefix": service.Name,
},
},
MountPoints: nil,
Name: service.Name,
PortMappings: toPortMappings(service.Ports),
Privileged: service.Privileged,
@ -68,7 +76,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe
},
},
Cpu: cpu,
Family: project.Name,
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.
@ -77,7 +85,6 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe
ProxyConfiguration: nil,
RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate},
Tags: nil,
Volumes: []ecs.TaskDefinition_Volume{},
}, nil
}
@ -122,7 +129,7 @@ func toLimits(service types.ServiceConfig) (string, string, error) {
}
}
}
return "", "", errors.New("unable to find cpu/mem for the required resources")
return "", "", fmt.Errorf("unable to find cpu/mem for the required resources")
}
func toRequiresCompatibilities(isolation string) []*string {
@ -198,8 +205,7 @@ func toTmpfs(tmpfs types.StringList) []ecs.TaskDefinition_Tmpfs {
for _, path := range tmpfs {
o = append(o, ecs.TaskDefinition_Tmpfs{
ContainerPath: path,
MountOptions: nil,
Size: 0,
Size: 100, // size is required on ECS, unlimited by the compose spec
})
}
return o