cloudformation.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. /*
  2. Copyright 2020 Docker, Inc.
  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. "io/ioutil"
  18. "regexp"
  19. "strings"
  20. "github.com/docker/compose-cli/api/compose"
  21. ecsapi "github.com/aws/aws-sdk-go/service/ecs"
  22. "github.com/aws/aws-sdk-go/service/elbv2"
  23. cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
  24. "github.com/awslabs/goformation/v4/cloudformation"
  25. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  26. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  27. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  28. "github.com/awslabs/goformation/v4/cloudformation/iam"
  29. "github.com/awslabs/goformation/v4/cloudformation/logs"
  30. "github.com/awslabs/goformation/v4/cloudformation/secretsmanager"
  31. cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
  32. "github.com/awslabs/goformation/v4/cloudformation/tags"
  33. "github.com/compose-spec/compose-go/compatibility"
  34. "github.com/compose-spec/compose-go/errdefs"
  35. "github.com/compose-spec/compose-go/types"
  36. "github.com/sirupsen/logrus"
  37. )
  38. const (
  39. parameterClusterName = "ParameterClusterName"
  40. parameterVPCId = "ParameterVPCId"
  41. parameterSubnet1Id = "ParameterSubnet1Id"
  42. parameterSubnet2Id = "ParameterSubnet2Id"
  43. parameterLoadBalancerARN = "ParameterLoadBalancerARN"
  44. )
  45. func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
  46. template, err := b.convert(project)
  47. if err != nil {
  48. return nil, err
  49. }
  50. // Create a NFS inbound rule on each mount target for volumes
  51. // as "source security group" use an arbitrary network attached to service(s) who mounts target volume
  52. for n, vol := range project.Volumes {
  53. err := b.SDK.WithVolumeSecurityGroups(ctx, vol.Name, func(securityGroups []string) error {
  54. target := securityGroups[0]
  55. for _, s := range project.Services {
  56. for _, v := range s.Volumes {
  57. if v.Source != n {
  58. continue
  59. }
  60. var source string
  61. for net := range s.Networks {
  62. network := project.Networks[net]
  63. if ext, ok := network.Extensions[extensionSecurityGroup]; ok {
  64. source = ext.(string)
  65. } else {
  66. source = networkResourceName(project, net)
  67. }
  68. break
  69. }
  70. name := fmt.Sprintf("%sNFSMount%s", s.Name, n)
  71. template.Resources[name] = &ec2.SecurityGroupIngress{
  72. Description: fmt.Sprintf("Allow NFS mount for %s on %s", s.Name, n),
  73. GroupId: target,
  74. SourceSecurityGroupId: cloudformation.Ref(source),
  75. IpProtocol: "tcp",
  76. FromPort: 2049,
  77. ToPort: 2049,
  78. }
  79. service := template.Resources[serviceResourceName(s.Name)].(*ecs.Service)
  80. service.AWSCloudFormationDependsOn = append(service.AWSCloudFormationDependsOn, name)
  81. }
  82. }
  83. return nil
  84. })
  85. if err != nil {
  86. return nil, err
  87. }
  88. }
  89. return marshall(template)
  90. }
  91. // Convert a compose project into a CloudFormation template
  92. func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, error) { //nolint:gocyclo
  93. var checker compatibility.Checker = &fargateCompatibilityChecker{
  94. compatibility.AllowList{
  95. Supported: compatibleComposeAttributes,
  96. },
  97. }
  98. compatibility.Check(project, checker)
  99. for _, err := range checker.Errors() {
  100. if errdefs.IsIncompatibleError(err) {
  101. logrus.Error(err.Error())
  102. } else {
  103. logrus.Warn(err.Error())
  104. }
  105. }
  106. if !compatibility.IsCompatible(checker) {
  107. return nil, fmt.Errorf("compose file is incompatible with Amazon ECS")
  108. }
  109. template := cloudformation.NewTemplate()
  110. template.Description = "CloudFormation template created by Docker for deploying applications on Amazon ECS"
  111. template.Parameters[parameterClusterName] = cloudformation.Parameter{
  112. Type: "String",
  113. Description: "Name of the ECS cluster to deploy to (optional)",
  114. }
  115. template.Parameters[parameterVPCId] = cloudformation.Parameter{
  116. Type: "AWS::EC2::VPC::Id",
  117. Description: "ID of the VPC",
  118. }
  119. /*
  120. FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282
  121. template.Parameters["SubnetIds"] = cloudformation.Parameter{
  122. Type: "List<AWS::EC2::Subnet::Id>",
  123. Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC",
  124. }
  125. */
  126. template.Parameters[parameterSubnet1Id] = cloudformation.Parameter{
  127. Type: "AWS::EC2::Subnet::Id",
  128. Description: "SubnetId, for Availability Zone 1 in the region in your VPC",
  129. }
  130. template.Parameters[parameterSubnet2Id] = cloudformation.Parameter{
  131. Type: "AWS::EC2::Subnet::Id",
  132. Description: "SubnetId, for Availability Zone 2 in the region in your VPC",
  133. }
  134. template.Parameters[parameterLoadBalancerARN] = cloudformation.Parameter{
  135. Type: "String",
  136. Description: "Name of the LoadBalancer to connect to (optional)",
  137. }
  138. // Createmount.nfs4: Connection timed out : unsuccessful EFS utils command execution; code: 32 Cluster is `ParameterClusterName` parameter is not set
  139. template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(parameterClusterName))
  140. cluster := createCluster(project, template)
  141. networks := map[string]string{}
  142. for _, net := range project.Networks {
  143. networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(parameterVPCId), template)
  144. }
  145. for i, s := range project.Secrets {
  146. if s.External.External {
  147. continue
  148. }
  149. secret, err := ioutil.ReadFile(s.File)
  150. if err != nil {
  151. return nil, err
  152. }
  153. name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name))
  154. template.Resources[name] = &secretsmanager.Secret{
  155. Description: "",
  156. SecretString: string(secret),
  157. Tags: []tags.Tag{
  158. {
  159. Key: compose.ProjectTag,
  160. Value: project.Name,
  161. },
  162. },
  163. }
  164. s.Name = cloudformation.Ref(name)
  165. project.Secrets[i] = s
  166. }
  167. createLogGroup(project, template)
  168. // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local
  169. createCloudMap(project, template)
  170. loadBalancerARN := createLoadBalancer(project, template)
  171. for _, service := range project.Services {
  172. definition, err := convert(project, service)
  173. if err != nil {
  174. return nil, err
  175. }
  176. taskExecutionRole := createTaskExecutionRole(service, definition, template)
  177. definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole)
  178. taskRole := createTaskRole(service, template)
  179. if taskRole != "" {
  180. definition.TaskRoleArn = cloudformation.Ref(taskRole)
  181. }
  182. taskDefinition := fmt.Sprintf("%sTaskDefinition", normalizeResourceName(service.Name))
  183. template.Resources[taskDefinition] = definition
  184. var healthCheck *cloudmap.Service_HealthCheckConfig
  185. serviceRegistry := createServiceRegistry(service, template, healthCheck)
  186. serviceSecurityGroups := []string{}
  187. for net := range service.Networks {
  188. serviceSecurityGroups = append(serviceSecurityGroups, networks[net])
  189. }
  190. dependsOn := []string{}
  191. serviceLB := []ecs.Service_LoadBalancer{}
  192. if len(service.Ports) > 0 {
  193. for _, port := range service.Ports {
  194. protocol := strings.ToUpper(port.Protocol)
  195. if getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication {
  196. protocol = elbv2.ProtocolEnumHttps
  197. if port.Published == 80 {
  198. protocol = elbv2.ProtocolEnumHttp
  199. }
  200. }
  201. if loadBalancerARN != "" {
  202. targetGroupName := createTargetGroup(project, service, port, template, protocol)
  203. listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol)
  204. dependsOn = append(dependsOn, listenerName)
  205. serviceLB = append(serviceLB, ecs.Service_LoadBalancer{
  206. ContainerName: service.Name,
  207. ContainerPort: int(port.Target),
  208. TargetGroupArn: cloudformation.Ref(targetGroupName),
  209. })
  210. }
  211. }
  212. }
  213. desiredCount := 1
  214. if service.Deploy != nil && service.Deploy.Replicas != nil {
  215. desiredCount = int(*service.Deploy.Replicas)
  216. }
  217. for dependency := range service.DependsOn {
  218. dependsOn = append(dependsOn, serviceResourceName(dependency))
  219. }
  220. minPercent, maxPercent, err := computeRollingUpdateLimits(service)
  221. if err != nil {
  222. return nil, err
  223. }
  224. template.Resources[serviceResourceName(service.Name)] = &ecs.Service{
  225. AWSCloudFormationDependsOn: dependsOn,
  226. Cluster: cluster,
  227. DesiredCount: desiredCount,
  228. DeploymentController: &ecs.Service_DeploymentController{
  229. Type: ecsapi.DeploymentControllerTypeEcs,
  230. },
  231. DeploymentConfiguration: &ecs.Service_DeploymentConfiguration{
  232. MaximumPercent: maxPercent,
  233. MinimumHealthyPercent: minPercent,
  234. },
  235. LaunchType: ecsapi.LaunchTypeFargate,
  236. LoadBalancers: serviceLB,
  237. NetworkConfiguration: &ecs.Service_NetworkConfiguration{
  238. AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
  239. AssignPublicIp: ecsapi.AssignPublicIpEnabled,
  240. SecurityGroups: serviceSecurityGroups,
  241. Subnets: []string{
  242. cloudformation.Ref(parameterSubnet1Id),
  243. cloudformation.Ref(parameterSubnet2Id),
  244. },
  245. },
  246. },
  247. PlatformVersion: "1.4.0", // LATEST which is set to 1.3.0 (?) which doesn’t allow efs volumes.
  248. PropagateTags: ecsapi.PropagateTagsService,
  249. SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
  250. ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry},
  251. Tags: []tags.Tag{
  252. {
  253. Key: compose.ProjectTag,
  254. Value: project.Name,
  255. },
  256. {
  257. Key: compose.ServiceTag,
  258. Value: service.Name,
  259. },
  260. },
  261. TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)),
  262. }
  263. }
  264. return template, nil
  265. }
  266. func createLogGroup(project *types.Project, template *cloudformation.Template) {
  267. retention := 0
  268. if v, ok := project.Extensions[extensionRetention]; ok {
  269. retention = v.(int)
  270. }
  271. logGroup := fmt.Sprintf("/docker-compose/%s", project.Name)
  272. template.Resources["LogGroup"] = &logs.LogGroup{
  273. LogGroupName: logGroup,
  274. RetentionInDays: retention,
  275. }
  276. }
  277. func computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) {
  278. maxPercent := 200
  279. minPercent := 100
  280. if service.Deploy == nil || service.Deploy.UpdateConfig == nil {
  281. return minPercent, maxPercent, nil
  282. }
  283. updateConfig := service.Deploy.UpdateConfig
  284. min, okMin := updateConfig.Extensions[extensionMinPercent]
  285. if okMin {
  286. minPercent = min.(int)
  287. }
  288. max, okMax := updateConfig.Extensions[extensionMaxPercent]
  289. if okMax {
  290. maxPercent = max.(int)
  291. }
  292. if okMin && okMax {
  293. return minPercent, maxPercent, nil
  294. }
  295. if updateConfig.Parallelism != nil {
  296. parallelism := int(*updateConfig.Parallelism)
  297. if service.Deploy.Replicas == nil {
  298. return minPercent, maxPercent,
  299. fmt.Errorf("rolling update configuration require deploy.replicas to be set")
  300. }
  301. replicas := int(*service.Deploy.Replicas)
  302. if replicas < parallelism {
  303. return minPercent, maxPercent,
  304. fmt.Errorf("deploy.replicas (%d) must be greater than deploy.update_config.parallelism (%d)", replicas, parallelism)
  305. }
  306. if !okMin {
  307. minPercent = (replicas - parallelism) * 100 / replicas
  308. }
  309. if !okMax {
  310. maxPercent = (replicas + parallelism) * 100 / replicas
  311. }
  312. }
  313. return minPercent, maxPercent, nil
  314. }
  315. func getLoadBalancerType(project *types.Project) string {
  316. for _, service := range project.Services {
  317. for _, port := range service.Ports {
  318. protocol := port.Protocol
  319. v, ok := port.Extensions[extensionProtocol]
  320. if ok {
  321. protocol = v.(string)
  322. }
  323. if protocol == "http" || protocol == "https" {
  324. continue
  325. }
  326. if port.Published != 80 && port.Published != 443 {
  327. return elbv2.LoadBalancerTypeEnumNetwork
  328. }
  329. }
  330. }
  331. return elbv2.LoadBalancerTypeEnumApplication
  332. }
  333. func getLoadBalancerSecurityGroups(project *types.Project, template *cloudformation.Template) []string {
  334. securityGroups := []string{}
  335. for _, network := range project.Networks {
  336. if !network.Internal {
  337. net := convertNetwork(project, network, cloudformation.Ref(parameterVPCId), template)
  338. securityGroups = append(securityGroups, net)
  339. }
  340. }
  341. return uniqueStrings(securityGroups)
  342. }
  343. func createLoadBalancer(project *types.Project, template *cloudformation.Template) string {
  344. ports := 0
  345. for _, service := range project.Services {
  346. ports += len(service.Ports)
  347. }
  348. if ports == 0 {
  349. // Project do not expose any port (batch jobs?)
  350. // So no need to create a PortPublisher
  351. return ""
  352. }
  353. // load balancer names are limited to 32 characters total
  354. loadBalancerName := fmt.Sprintf("%.32s", fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)))
  355. // Create PortPublisher if `ParameterLoadBalancerName` is not set
  356. template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(parameterLoadBalancerARN))
  357. loadBalancerType := getLoadBalancerType(project)
  358. securityGroups := []string{}
  359. if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication {
  360. securityGroups = getLoadBalancerSecurityGroups(project, template)
  361. }
  362. template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{
  363. Name: loadBalancerName,
  364. Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing,
  365. SecurityGroups: securityGroups,
  366. Subnets: []string{
  367. cloudformation.Ref(parameterSubnet1Id),
  368. cloudformation.Ref(parameterSubnet2Id),
  369. },
  370. Tags: []tags.Tag{
  371. {
  372. Key: compose.ProjectTag,
  373. Value: project.Name,
  374. },
  375. },
  376. Type: loadBalancerType,
  377. AWSCloudFormationCondition: "CreateLoadBalancer",
  378. }
  379. return cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(parameterLoadBalancerARN))
  380. }
  381. func createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string {
  382. listenerName := fmt.Sprintf(
  383. "%s%s%dListener",
  384. normalizeResourceName(service.Name),
  385. strings.ToUpper(port.Protocol),
  386. port.Target,
  387. )
  388. //add listener to dependsOn
  389. //https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer
  390. template.Resources[listenerName] = &elasticloadbalancingv2.Listener{
  391. DefaultActions: []elasticloadbalancingv2.Listener_Action{
  392. {
  393. ForwardConfig: &elasticloadbalancingv2.Listener_ForwardConfig{
  394. TargetGroups: []elasticloadbalancingv2.Listener_TargetGroupTuple{
  395. {
  396. TargetGroupArn: cloudformation.Ref(targetGroupName),
  397. },
  398. },
  399. },
  400. Type: elbv2.ActionTypeEnumForward,
  401. },
  402. },
  403. LoadBalancerArn: loadBalancerARN,
  404. Protocol: protocol,
  405. Port: int(port.Target),
  406. }
  407. return listenerName
  408. }
  409. func createTargetGroup(project *types.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string {
  410. targetGroupName := fmt.Sprintf(
  411. "%s%s%dTargetGroup",
  412. normalizeResourceName(service.Name),
  413. strings.ToUpper(port.Protocol),
  414. port.Published,
  415. )
  416. template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{
  417. Port: int(port.Target),
  418. Protocol: protocol,
  419. Tags: []tags.Tag{
  420. {
  421. Key: compose.ProjectTag,
  422. Value: project.Name,
  423. },
  424. },
  425. VpcId: cloudformation.Ref(parameterVPCId),
  426. TargetType: elbv2.TargetTypeEnumIp,
  427. }
  428. return targetGroupName
  429. }
  430. func createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry {
  431. serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name))
  432. serviceRegistry := ecs.Service_ServiceRegistry{
  433. RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"),
  434. }
  435. template.Resources[serviceRegistration] = &cloudmap.Service{
  436. Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name),
  437. HealthCheckConfig: healthCheck,
  438. HealthCheckCustomConfig: &cloudmap.Service_HealthCheckCustomConfig{
  439. FailureThreshold: 1,
  440. },
  441. Name: service.Name,
  442. NamespaceId: cloudformation.Ref("CloudMap"),
  443. DnsConfig: &cloudmap.Service_DnsConfig{
  444. DnsRecords: []cloudmap.Service_DnsRecord{
  445. {
  446. TTL: 60,
  447. Type: cloudmapapi.RecordTypeA,
  448. },
  449. },
  450. RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue,
  451. },
  452. }
  453. return serviceRegistry
  454. }
  455. func createTaskExecutionRole(service types.ServiceConfig, definition *ecs.TaskDefinition, template *cloudformation.Template) string {
  456. taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name))
  457. policies := createPolicies(service, definition)
  458. template.Resources[taskExecutionRole] = &iam.Role{
  459. AssumeRolePolicyDocument: assumeRolePolicyDocument,
  460. Policies: policies,
  461. ManagedPolicyArns: []string{
  462. ecsTaskExecutionPolicy,
  463. ecrReadOnlyPolicy,
  464. },
  465. }
  466. return taskExecutionRole
  467. }
  468. func createTaskRole(service types.ServiceConfig, template *cloudformation.Template) string {
  469. taskRole := fmt.Sprintf("%sTaskRole", normalizeResourceName(service.Name))
  470. rolePolicies := []iam.Role_Policy{}
  471. if roles, ok := service.Extensions[extensionRole]; ok {
  472. rolePolicies = append(rolePolicies, iam.Role_Policy{
  473. PolicyDocument: roles,
  474. })
  475. }
  476. managedPolicies := []string{}
  477. if v, ok := service.Extensions[extensionManagedPolicies]; ok {
  478. for _, s := range v.([]interface{}) {
  479. managedPolicies = append(managedPolicies, s.(string))
  480. }
  481. }
  482. if len(rolePolicies) == 0 && len(managedPolicies) == 0 {
  483. return ""
  484. }
  485. template.Resources[taskRole] = &iam.Role{
  486. AssumeRolePolicyDocument: assumeRolePolicyDocument,
  487. Policies: rolePolicies,
  488. ManagedPolicyArns: managedPolicies,
  489. }
  490. return taskRole
  491. }
  492. func createCluster(project *types.Project, template *cloudformation.Template) string {
  493. template.Resources["Cluster"] = &ecs.Cluster{
  494. ClusterName: project.Name,
  495. Tags: []tags.Tag{
  496. {
  497. Key: compose.ProjectTag,
  498. Value: project.Name,
  499. },
  500. },
  501. AWSCloudFormationCondition: "CreateCluster",
  502. }
  503. cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(parameterClusterName))
  504. return cluster
  505. }
  506. func createCloudMap(project *types.Project, template *cloudformation.Template) {
  507. template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{
  508. Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name),
  509. Name: fmt.Sprintf("%s.local", project.Name),
  510. Vpc: cloudformation.Ref(parameterVPCId),
  511. }
  512. }
  513. func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string {
  514. if sg, ok := net.Extensions[extensionSecurityGroup]; ok {
  515. logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg)
  516. return sg.(string)
  517. }
  518. var ingresses []ec2.SecurityGroup_Ingress
  519. if !net.Internal {
  520. for _, service := range project.Services {
  521. if _, ok := service.Networks[net.Name]; ok {
  522. for _, port := range service.Ports {
  523. ingresses = append(ingresses, ec2.SecurityGroup_Ingress{
  524. CidrIp: "0.0.0.0/0",
  525. Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol),
  526. FromPort: int(port.Target),
  527. IpProtocol: strings.ToUpper(port.Protocol),
  528. ToPort: int(port.Target),
  529. })
  530. }
  531. }
  532. }
  533. }
  534. securityGroup := networkResourceName(project, net.Name)
  535. template.Resources[securityGroup] = &ec2.SecurityGroup{
  536. GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net.Name),
  537. GroupName: securityGroup,
  538. SecurityGroupIngress: ingresses,
  539. VpcId: vpc,
  540. Tags: []tags.Tag{
  541. {
  542. Key: compose.ProjectTag,
  543. Value: project.Name,
  544. },
  545. {
  546. Key: compose.NetworkTag,
  547. Value: net.Name,
  548. },
  549. },
  550. }
  551. ingress := securityGroup + "Ingress"
  552. template.Resources[ingress] = &ec2.SecurityGroupIngress{
  553. Description: fmt.Sprintf("Allow communication within network %s", net.Name),
  554. IpProtocol: "-1", // all protocols
  555. GroupId: cloudformation.Ref(securityGroup),
  556. SourceSecurityGroupId: cloudformation.Ref(securityGroup),
  557. }
  558. return cloudformation.Ref(securityGroup)
  559. }
  560. func networkResourceName(project *types.Project, network string) string {
  561. return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network))
  562. }
  563. func serviceResourceName(service string) string {
  564. return fmt.Sprintf("%sService", normalizeResourceName(service))
  565. }
  566. func normalizeResourceName(s string) string {
  567. return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, ""))
  568. }
  569. func createPolicies(service types.ServiceConfig, taskDef *ecs.TaskDefinition) []iam.Role_Policy {
  570. arns := []string{}
  571. for _, container := range taskDef.ContainerDefinitions {
  572. if container.RepositoryCredentials != nil {
  573. arns = append(arns, container.RepositoryCredentials.CredentialsParameter)
  574. }
  575. if len(container.Secrets) > 0 {
  576. for _, s := range container.Secrets {
  577. arns = append(arns, s.ValueFrom)
  578. }
  579. }
  580. }
  581. if len(arns) > 0 {
  582. return []iam.Role_Policy{
  583. {
  584. PolicyDocument: &PolicyDocument{
  585. Statement: []PolicyStatement{
  586. {
  587. Effect: "Allow",
  588. Action: []string{actionGetSecretValue, actionGetParameters, actionDecrypt},
  589. Resource: arns,
  590. },
  591. },
  592. },
  593. PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name),
  594. },
  595. }
  596. }
  597. return nil
  598. }
  599. func uniqueStrings(items []string) []string {
  600. keys := make(map[string]bool)
  601. unique := []string{}
  602. for _, item := range items {
  603. if _, val := keys[item]; !val {
  604. keys[item] = true
  605. unique = append(unique, item)
  606. }
  607. }
  608. return unique
  609. }