cloudformation.go 22 KB

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