awsResources.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package ecs
  14. import (
  15. "context"
  16. "fmt"
  17. "strconv"
  18. "strings"
  19. "github.com/docker/compose-cli/api/compose"
  20. "github.com/docker/compose-cli/errdefs"
  21. "github.com/aws/aws-sdk-go/service/elbv2"
  22. "github.com/awslabs/goformation/v4/cloudformation"
  23. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  24. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  25. "github.com/awslabs/goformation/v4/cloudformation/efs"
  26. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  27. "github.com/compose-spec/compose-go/types"
  28. "github.com/pkg/errors"
  29. "github.com/sirupsen/logrus"
  30. )
  31. // awsResources hold the AWS component being used or created to support services definition
  32. type awsResources struct {
  33. vpc string // shouldn't this also be an awsResource ?
  34. subnets []awsResource
  35. cluster awsResource
  36. loadBalancer awsResource
  37. loadBalancerType string
  38. securityGroups map[string]string
  39. filesystems map[string]awsResource
  40. }
  41. func (r *awsResources) serviceSecurityGroups(service types.ServiceConfig) []string {
  42. var groups []string
  43. for net := range service.Networks {
  44. groups = append(groups, r.securityGroups[net])
  45. }
  46. return groups
  47. }
  48. func (r *awsResources) allSecurityGroups() []string {
  49. var securityGroups []string
  50. for _, r := range r.securityGroups {
  51. securityGroups = append(securityGroups, r)
  52. }
  53. return securityGroups
  54. }
  55. func (r *awsResources) subnetsIDs() []string {
  56. var ids []string
  57. for _, r := range r.subnets {
  58. ids = append(ids, r.ID())
  59. }
  60. return ids
  61. }
  62. // awsResource is abstract representation for any (existing or future) AWS resource that we can refer both by ID or full ARN
  63. type awsResource interface {
  64. ARN() string
  65. ID() string
  66. }
  67. // existingAWSResource hold references to an existing AWS component
  68. type existingAWSResource struct {
  69. arn string
  70. id string
  71. }
  72. func (r existingAWSResource) ARN() string {
  73. return r.arn
  74. }
  75. func (r existingAWSResource) ID() string {
  76. return r.id
  77. }
  78. // cloudformationResource hold references to a future AWS resource managed by CloudFormation
  79. // to be used by CloudFormation resources where Ref returns the Amazon Resource ID
  80. type cloudformationResource struct {
  81. logicalName string
  82. }
  83. func (r cloudformationResource) ARN() string {
  84. return cloudformation.GetAtt(r.logicalName, "Arn")
  85. }
  86. func (r cloudformationResource) ID() string {
  87. return cloudformation.Ref(r.logicalName)
  88. }
  89. // cloudformationARNResource hold references to a future AWS resource managed by CloudFormation
  90. // to be used by CloudFormation resources where Ref returns the Amazon Resource Name (ARN)
  91. type cloudformationARNResource struct {
  92. logicalName string
  93. nameProperty string
  94. }
  95. func (r cloudformationARNResource) ARN() string {
  96. return cloudformation.Ref(r.logicalName)
  97. }
  98. func (r cloudformationARNResource) ID() string {
  99. return cloudformation.GetAtt(r.logicalName, r.nameProperty)
  100. }
  101. // parse look into compose project for configured resource to use, and check they are valid
  102. func (b *ecsAPIService) parse(ctx context.Context, project *types.Project, template *cloudformation.Template) (awsResources, error) {
  103. r := awsResources{}
  104. var err error
  105. r.cluster, err = b.parseClusterExtension(ctx, project, template)
  106. if err != nil {
  107. return r, err
  108. }
  109. r.vpc, r.subnets, err = b.parseVPCExtension(ctx, project)
  110. if err != nil {
  111. return r, err
  112. }
  113. r.loadBalancer, r.loadBalancerType, err = b.parseLoadBalancerExtension(ctx, project)
  114. if err != nil {
  115. return r, err
  116. }
  117. r.securityGroups, err = b.parseExternalNetworks(ctx, project)
  118. if err != nil {
  119. return r, err
  120. }
  121. r.filesystems, err = b.parseExternalVolumes(ctx, project)
  122. if err != nil {
  123. return r, err
  124. }
  125. return r, nil
  126. }
  127. func (b *ecsAPIService) parseClusterExtension(ctx context.Context, project *types.Project, template *cloudformation.Template) (awsResource, error) {
  128. if x, ok := project.Extensions[extensionCluster]; ok {
  129. nameOrArn := x.(string) // can be name _or_ ARN.
  130. cluster, err := b.aws.ResolveCluster(ctx, nameOrArn)
  131. if err != nil {
  132. return nil, err
  133. }
  134. if !ok {
  135. return nil, errors.Wrapf(errdefs.ErrNotFound, "cluster %q does not exist", cluster)
  136. }
  137. template.Metadata["Cluster"] = cluster.ARN()
  138. return cluster, nil
  139. }
  140. return nil, nil
  141. }
  142. func (b *ecsAPIService) parseVPCExtension(ctx context.Context, project *types.Project) (string, []awsResource, error) {
  143. var vpc string
  144. if x, ok := project.Extensions[extensionVPC]; ok {
  145. vpc = x.(string)
  146. err := b.aws.CheckVPC(ctx, vpc)
  147. if err != nil {
  148. return "", nil, err
  149. }
  150. } else {
  151. defaultVPC, err := b.aws.GetDefaultVPC(ctx)
  152. if err != nil {
  153. return "", nil, err
  154. }
  155. vpc = defaultVPC
  156. }
  157. subNets, err := b.aws.GetSubNets(ctx, vpc)
  158. if err != nil {
  159. return "", nil, err
  160. }
  161. var publicSubNets []awsResource
  162. for _, subNet := range subNets {
  163. isPublic, err := b.aws.IsPublicSubnet(ctx, vpc, subNet.ID())
  164. if err != nil {
  165. return "", nil, err
  166. }
  167. if isPublic {
  168. publicSubNets = append(publicSubNets, subNet)
  169. }
  170. }
  171. if len(publicSubNets) < 2 {
  172. return "", nil, fmt.Errorf("VPC %s should have at least 2 associated public subnets in different availability zones", vpc)
  173. }
  174. return vpc, publicSubNets, nil
  175. }
  176. func (b *ecsAPIService) parseLoadBalancerExtension(ctx context.Context, project *types.Project) (awsResource, string, error) {
  177. if x, ok := project.Extensions[extensionLoadBalancer]; ok {
  178. nameOrArn := x.(string)
  179. loadBalancer, loadBalancerType, err := b.aws.ResolveLoadBalancer(ctx, nameOrArn)
  180. if err != nil {
  181. return nil, "", err
  182. }
  183. required := getRequiredLoadBalancerType(project)
  184. if loadBalancerType != required {
  185. return nil, "", fmt.Errorf("load balancer %q is of type %s, project require a %s", nameOrArn, loadBalancerType, required)
  186. }
  187. return loadBalancer, loadBalancerType, err
  188. }
  189. return nil, "", nil
  190. }
  191. func (b *ecsAPIService) parseExternalNetworks(ctx context.Context, project *types.Project) (map[string]string, error) {
  192. securityGroups := make(map[string]string, len(project.Networks))
  193. for name, net := range project.Networks {
  194. // FIXME remove this for G.A
  195. if x, ok := net.Extensions[extensionSecurityGroup]; ok {
  196. logrus.Warn("to use an existing security-group, use `network.external` and `network.name` in your compose file")
  197. logrus.Debugf("Security Group for network %q set by user to %q", net.Name, x)
  198. net.External.External = true
  199. net.Name = x.(string)
  200. project.Networks[name] = net
  201. }
  202. if !net.External.External {
  203. continue
  204. }
  205. exists, err := b.aws.SecurityGroupExists(ctx, net.Name)
  206. if err != nil {
  207. return nil, err
  208. }
  209. if !exists {
  210. return nil, errors.Wrapf(errdefs.ErrNotFound, "security group %q doesn't exist", net.Name)
  211. }
  212. securityGroups[name] = net.Name
  213. }
  214. return securityGroups, nil
  215. }
  216. func (b *ecsAPIService) parseExternalVolumes(ctx context.Context, project *types.Project) (map[string]awsResource, error) {
  217. filesystems := make(map[string]awsResource, len(project.Volumes))
  218. for name, vol := range project.Volumes {
  219. if vol.External.External {
  220. arn, err := b.aws.ResolveFileSystem(ctx, vol.Name)
  221. if err != nil {
  222. return nil, err
  223. }
  224. filesystems[name] = arn
  225. continue
  226. }
  227. logrus.Debugf("searching for existing filesystem as volume %q", name)
  228. tags := map[string]string{
  229. compose.ProjectTag: project.Name,
  230. compose.VolumeTag: name,
  231. }
  232. previous, err := b.aws.ListFileSystems(ctx, tags)
  233. if err != nil {
  234. return nil, err
  235. }
  236. if len(previous) > 1 {
  237. return nil, fmt.Errorf("multiple filesystems are tags as project=%q, volume=%q", project.Name, name)
  238. }
  239. if len(previous) == 1 {
  240. filesystems[name] = previous[0]
  241. }
  242. }
  243. return filesystems, nil
  244. }
  245. // ensureResources create required resources in template if not yet defined
  246. func (b *ecsAPIService) ensureResources(resources *awsResources, project *types.Project, template *cloudformation.Template) error {
  247. b.ensureCluster(resources, project, template)
  248. b.ensureNetworks(resources, project, template)
  249. err := b.ensureVolumes(resources, project, template)
  250. if err != nil {
  251. return err
  252. }
  253. b.ensureLoadBalancer(resources, project, template)
  254. return nil
  255. }
  256. func (b *ecsAPIService) ensureCluster(r *awsResources, project *types.Project, template *cloudformation.Template) {
  257. if r.cluster != nil {
  258. return
  259. }
  260. template.Resources["Cluster"] = &ecs.Cluster{
  261. ClusterName: project.Name,
  262. Tags: projectTags(project),
  263. }
  264. r.cluster = cloudformationResource{logicalName: "Cluster"}
  265. }
  266. func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project, template *cloudformation.Template) {
  267. if r.securityGroups == nil {
  268. r.securityGroups = make(map[string]string, len(project.Networks))
  269. }
  270. for name, net := range project.Networks {
  271. if _, ok := r.securityGroups[name]; ok {
  272. continue
  273. }
  274. securityGroup := networkResourceName(name)
  275. template.Resources[securityGroup] = &ec2.SecurityGroup{
  276. GroupDescription: fmt.Sprintf("%s Security Group for %s network", project.Name, name),
  277. VpcId: r.vpc,
  278. Tags: networkTags(project, net),
  279. }
  280. ingress := securityGroup + "Ingress"
  281. template.Resources[ingress] = &ec2.SecurityGroupIngress{
  282. Description: fmt.Sprintf("Allow communication within network %s", name),
  283. IpProtocol: allProtocols,
  284. GroupId: cloudformation.Ref(securityGroup),
  285. SourceSecurityGroupId: cloudformation.Ref(securityGroup),
  286. }
  287. r.securityGroups[name] = cloudformation.Ref(securityGroup)
  288. }
  289. }
  290. func (b *ecsAPIService) ensureVolumes(r *awsResources, project *types.Project, template *cloudformation.Template) error {
  291. for name, volume := range project.Volumes {
  292. if _, ok := r.filesystems[name]; ok {
  293. continue
  294. }
  295. var backupPolicy *efs.FileSystem_BackupPolicy
  296. if backup, ok := volume.DriverOpts["backup_policy"]; ok {
  297. backupPolicy = &efs.FileSystem_BackupPolicy{
  298. Status: backup,
  299. }
  300. }
  301. var lifecyclePolicies []efs.FileSystem_LifecyclePolicy
  302. if policy, ok := volume.DriverOpts["lifecycle_policy"]; ok {
  303. lifecyclePolicies = append(lifecyclePolicies, efs.FileSystem_LifecyclePolicy{
  304. TransitionToIA: strings.TrimSpace(policy),
  305. })
  306. }
  307. var provisionedThroughputInMibps float64
  308. if t, ok := volume.DriverOpts["provisioned_throughput"]; ok {
  309. v, err := strconv.ParseFloat(t, 64)
  310. if err != nil {
  311. return err
  312. }
  313. provisionedThroughputInMibps = v
  314. }
  315. var performanceMode = volume.DriverOpts["performance_mode"]
  316. var throughputMode = volume.DriverOpts["throughput_mode"]
  317. var kmsKeyID = volume.DriverOpts["kms_key_id"]
  318. n := volumeResourceName(name)
  319. template.Resources[n] = &efs.FileSystem{
  320. BackupPolicy: backupPolicy,
  321. Encrypted: true,
  322. FileSystemPolicy: nil,
  323. FileSystemTags: []efs.FileSystem_ElasticFileSystemTag{
  324. {
  325. Key: compose.ProjectTag,
  326. Value: project.Name,
  327. },
  328. {
  329. Key: compose.VolumeTag,
  330. Value: name,
  331. },
  332. {
  333. Key: "Name",
  334. Value: fmt.Sprintf("%s_%s", project.Name, name),
  335. },
  336. },
  337. KmsKeyId: kmsKeyID,
  338. LifecyclePolicies: lifecyclePolicies,
  339. PerformanceMode: performanceMode,
  340. ProvisionedThroughputInMibps: provisionedThroughputInMibps,
  341. ThroughputMode: throughputMode,
  342. AWSCloudFormationDeletionPolicy: "Retain",
  343. }
  344. r.filesystems[name] = cloudformationResource{logicalName: n}
  345. }
  346. return nil
  347. }
  348. func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, project *types.Project, template *cloudformation.Template) {
  349. if r.loadBalancer != nil {
  350. return
  351. }
  352. if allServices(project.Services, func(it types.ServiceConfig) bool {
  353. return len(it.Ports) == 0
  354. }) {
  355. logrus.Debug("Application does not expose any public port, so no need for a LoadBalancer")
  356. return
  357. }
  358. balancerType := getRequiredLoadBalancerType(project)
  359. var securityGroups []string
  360. if balancerType == elbv2.LoadBalancerTypeEnumApplication {
  361. // see https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-register-targets.html#target-security-groups
  362. // Network Load Balancers do not have associated security groups
  363. securityGroups = r.getLoadBalancerSecurityGroups(project)
  364. }
  365. var loadBalancerAttributes []elasticloadbalancingv2.LoadBalancer_LoadBalancerAttribute
  366. if balancerType == elbv2.LoadBalancerTypeEnumNetwork {
  367. loadBalancerAttributes = append(
  368. loadBalancerAttributes,
  369. elasticloadbalancingv2.LoadBalancer_LoadBalancerAttribute{
  370. Key: "load_balancing.cross_zone.enabled",
  371. Value: "true",
  372. })
  373. }
  374. template.Resources["LoadBalancer"] = &elasticloadbalancingv2.LoadBalancer{
  375. Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing,
  376. SecurityGroups: securityGroups,
  377. Subnets: r.subnetsIDs(),
  378. Tags: projectTags(project),
  379. Type: balancerType,
  380. LoadBalancerAttributes: loadBalancerAttributes,
  381. }
  382. r.loadBalancer = cloudformationARNResource{
  383. logicalName: "LoadBalancer",
  384. nameProperty: "LoadBalancerName",
  385. }
  386. r.loadBalancerType = balancerType
  387. }
  388. func (r *awsResources) getLoadBalancerSecurityGroups(project *types.Project) []string {
  389. securityGroups := []string{}
  390. for name, network := range project.Networks {
  391. if !network.Internal {
  392. securityGroups = append(securityGroups, r.securityGroups[name])
  393. }
  394. }
  395. return securityGroups
  396. }
  397. func getRequiredLoadBalancerType(project *types.Project) string {
  398. loadBalancerType := elbv2.LoadBalancerTypeEnumNetwork
  399. if allServices(project.Services, func(it types.ServiceConfig) bool {
  400. return allPorts(it.Ports, portIsHTTP)
  401. }) {
  402. loadBalancerType = elbv2.LoadBalancerTypeEnumApplication
  403. }
  404. return loadBalancerType
  405. }
  406. func portIsHTTP(it types.ServicePortConfig) bool {
  407. if v, ok := it.Extensions[extensionProtocol]; ok {
  408. protocol := v.(string)
  409. return protocol == "http" || protocol == "https"
  410. }
  411. return it.Target == 80 || it.Target == 443
  412. }
  413. // predicate[types.ServiceConfig]
  414. type servicePredicate func(it types.ServiceConfig) bool
  415. // all[types.ServiceConfig]
  416. func allServices(services types.Services, p servicePredicate) bool {
  417. for _, s := range services {
  418. if !p(s) {
  419. return false
  420. }
  421. }
  422. return true
  423. }
  424. // predicate[types.ServicePortConfig]
  425. type portPredicate func(it types.ServicePortConfig) bool
  426. // all[types.ServicePortConfig]
  427. func allPorts(ports []types.ServicePortConfig, p portPredicate) bool {
  428. for _, s := range ports {
  429. if !p(s) {
  430. return false
  431. }
  432. }
  433. return true
  434. }