1
0

cloudformation.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. package amazon
  2. import (
  3. "fmt"
  4. "regexp"
  5. "strings"
  6. "github.com/compose-spec/compose-go/types"
  7. "github.com/sirupsen/logrus"
  8. "github.com/aws/aws-sdk-go/service/elbv2"
  9. cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
  10. ecsapi "github.com/aws/aws-sdk-go/service/ecs"
  11. "github.com/awslabs/goformation/v4/cloudformation"
  12. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  13. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  14. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  15. "github.com/awslabs/goformation/v4/cloudformation/iam"
  16. "github.com/awslabs/goformation/v4/cloudformation/logs"
  17. cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
  18. "github.com/awslabs/goformation/v4/cloudformation/tags"
  19. "github.com/docker/ecs-plugin/pkg/compose"
  20. )
  21. const (
  22. ParameterClusterName = "ParameterClusterName"
  23. ParameterVPCId = "ParameterVPCId"
  24. ParameterSubnet1Id = "ParameterSubnet1Id"
  25. ParameterSubnet2Id = "ParameterSubnet2Id"
  26. )
  27. // Convert a compose project into a CloudFormation template
  28. func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) {
  29. warnings := Check(project)
  30. for _, w := range warnings {
  31. logrus.Warn(w)
  32. }
  33. template := cloudformation.NewTemplate()
  34. template.Parameters[ParameterClusterName] = cloudformation.Parameter{
  35. Type: "String",
  36. Description: "Name of the ECS cluster to deploy to (optional)",
  37. }
  38. template.Parameters[ParameterVPCId] = cloudformation.Parameter{
  39. Type: "AWS::EC2::VPC::Id",
  40. Description: "ID of the VPC",
  41. }
  42. /*
  43. FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282
  44. template.Parameters["SubnetIds"] = cloudformation.Parameter{
  45. Type: "List<AWS::EC2::Subnet::Id>",
  46. Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC",
  47. }
  48. */
  49. template.Parameters[ParameterSubnet1Id] = cloudformation.Parameter{
  50. Type: "AWS::EC2::Subnet::Id",
  51. Description: "SubnetId, for Availability Zone 1 in the region in your VPC",
  52. }
  53. template.Parameters[ParameterSubnet2Id] = cloudformation.Parameter{
  54. Type: "AWS::EC2::Subnet::Id",
  55. Description: "SubnetId, for Availability Zone 2 in the region in your VPC",
  56. }
  57. // Create Cluster is `ParameterClusterName` parameter is not set
  58. template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(ParameterClusterName))
  59. template.Resources["Cluster"] = &ecs.Cluster{
  60. ClusterName: project.Name,
  61. Tags: []tags.Tag{
  62. {
  63. Key: ProjectTag,
  64. Value: project.Name,
  65. },
  66. },
  67. AWSCloudFormationCondition: "CreateCluster",
  68. }
  69. cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(ParameterClusterName))
  70. networks := map[string]string{}
  71. for _, net := range project.Networks {
  72. networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(ParameterVPCId), template)
  73. }
  74. logGroup := fmt.Sprintf("/docker-compose/%s", project.Name)
  75. template.Resources["LogGroup"] = &logs.LogGroup{
  76. LogGroupName: logGroup,
  77. }
  78. // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local
  79. template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{
  80. Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name),
  81. Name: fmt.Sprintf("%s.local", project.Name),
  82. Vpc: cloudformation.Ref(ParameterVPCId),
  83. }
  84. for _, service := range project.Services {
  85. definition, err := Convert(project, service)
  86. if err != nil {
  87. return nil, err
  88. }
  89. taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name))
  90. policy, err := c.getPolicy(definition)
  91. if err != nil {
  92. return nil, err
  93. }
  94. rolePolicies := []iam.Role_Policy{}
  95. if policy != nil {
  96. rolePolicies = append(rolePolicies, iam.Role_Policy{
  97. PolicyDocument: policy,
  98. PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name),
  99. })
  100. }
  101. definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole)
  102. taskDefinition := fmt.Sprintf("%sTaskDefinition", normalizeResourceName(service.Name))
  103. template.Resources[taskExecutionRole] = &iam.Role{
  104. AssumeRolePolicyDocument: assumeRolePolicyDocument,
  105. Policies: rolePolicies,
  106. ManagedPolicyArns: []string{
  107. ECSTaskExecutionPolicy,
  108. ECRReadOnlyPolicy,
  109. },
  110. }
  111. template.Resources[taskDefinition] = definition
  112. var healthCheck *cloudmap.Service_HealthCheckConfig
  113. if service.HealthCheck != nil && !service.HealthCheck.Disable {
  114. // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD
  115. }
  116. serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name))
  117. records := []cloudmap.Service_DnsRecord{
  118. {
  119. TTL: 60,
  120. Type: cloudmapapi.RecordTypeA,
  121. },
  122. }
  123. serviceRegistry := ecs.Service_ServiceRegistry{
  124. RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"),
  125. }
  126. loadBalancers := []ecs.Service_LoadBalancer{}
  127. template.Resources[serviceRegistration] = &cloudmap.Service{
  128. Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name),
  129. HealthCheckConfig: healthCheck,
  130. Name: service.Name,
  131. NamespaceId: cloudformation.Ref("CloudMap"),
  132. DnsConfig: &cloudmap.Service_DnsConfig{
  133. DnsRecords: records,
  134. RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue,
  135. },
  136. }
  137. serviceSecurityGroups := []string{}
  138. for net := range service.Networks {
  139. serviceSecurityGroups = append(serviceSecurityGroups, networks[net])
  140. }
  141. dependsOn := []string{}
  142. if len(service.Ports) > 0 {
  143. records = append(records, cloudmap.Service_DnsRecord{
  144. TTL: 60,
  145. Type: cloudmapapi.RecordTypeSrv,
  146. })
  147. //serviceRegistry.Port = int(service.Ports[0].Target)
  148. // add targetgroup for each published port
  149. for _, port := range service.Ports {
  150. targetGroupName := fmt.Sprintf(
  151. "%s%s%sTargetGroup",
  152. normalizeResourceName(service.Name),
  153. strings.ToUpper(port.Protocol),
  154. string(port.Published),
  155. )
  156. listenerName := fmt.Sprintf(
  157. "%s%s%sListener",
  158. normalizeResourceName(service.Name),
  159. strings.ToUpper(port.Protocol),
  160. string(port.Published),
  161. )
  162. loadBalancerName := fmt.Sprintf(
  163. "%s%s%sLoadBalancer",
  164. normalizeResourceName(service.Name),
  165. strings.ToUpper(port.Protocol),
  166. string(port.Published),
  167. )
  168. dependsOn = append(dependsOn, listenerName)
  169. lbType := "network"
  170. lbSecGroups := []string{}
  171. protocolType := strings.ToUpper(port.Protocol)
  172. targetType := elbv2.TargetTypeEnumInstance
  173. if port.Published == 80 || port.Published == 443 {
  174. lbType = "application"
  175. lbSecGroups = serviceSecurityGroups
  176. protocolType = "HTTPS"
  177. targetType = elbv2.TargetTypeEnumIp
  178. if port.Published == 80 {
  179. protocolType = "HTTP"
  180. }
  181. }
  182. template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{
  183. Name: targetGroupName,
  184. Port: int(port.Target),
  185. Protocol: protocolType,
  186. Tags: []tags.Tag{
  187. {
  188. Key: ProjectTag,
  189. Value: project.Name,
  190. },
  191. {
  192. Key: ServiceTag,
  193. Value: service.Name,
  194. },
  195. },
  196. VpcId: cloudformation.Ref(ParameterVPCId),
  197. TargetType: targetType,
  198. }
  199. template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{
  200. Name: loadBalancerName,
  201. Scheme: "internet-facing",
  202. SecurityGroups: lbSecGroups,
  203. Subnets: []string{
  204. cloudformation.Ref(ParameterSubnet1Id),
  205. cloudformation.Ref(ParameterSubnet2Id),
  206. },
  207. Tags: []tags.Tag{
  208. {
  209. Key: ProjectTag,
  210. Value: project.Name,
  211. },
  212. {
  213. Key: ServiceTag,
  214. Value: service.Name,
  215. },
  216. },
  217. Type: lbType,
  218. }
  219. template.Resources[listenerName] = &elasticloadbalancingv2.Listener{
  220. DefaultActions: []elasticloadbalancingv2.Listener_Action{
  221. {
  222. ForwardConfig: &elasticloadbalancingv2.Listener_ForwardConfig{
  223. TargetGroups: []elasticloadbalancingv2.Listener_TargetGroupTuple{
  224. {
  225. TargetGroupArn: cloudformation.Ref(targetGroupName),
  226. },
  227. },
  228. },
  229. Type: elbv2.ActionTypeEnumForward,
  230. },
  231. },
  232. LoadBalancerArn: cloudformation.Ref(loadBalancerName),
  233. Protocol: protocolType,
  234. Port: int(port.Published),
  235. }
  236. loadBalancers = append(loadBalancers, ecs.Service_LoadBalancer{
  237. ContainerName: service.Name,
  238. ContainerPort: int(port.Published),
  239. TargetGroupArn: cloudformation.Ref(targetGroupName),
  240. })
  241. }
  242. }
  243. desiredCount := 1
  244. if service.Deploy != nil && service.Deploy.Replicas != nil {
  245. desiredCount = int(*service.Deploy.Replicas)
  246. }
  247. for _, dependency := range service.DependsOn {
  248. dependsOn = append(dependsOn, serviceResourceName(dependency))
  249. }
  250. template.Resources[serviceResourceName(service.Name)] = &ecs.Service{
  251. AWSCloudFormationDependsOn: dependsOn,
  252. Cluster: cluster,
  253. DesiredCount: desiredCount,
  254. LaunchType: ecsapi.LaunchTypeFargate,
  255. LoadBalancers: loadBalancers,
  256. NetworkConfiguration: &ecs.Service_NetworkConfiguration{
  257. AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
  258. AssignPublicIp: ecsapi.AssignPublicIpEnabled,
  259. SecurityGroups: serviceSecurityGroups,
  260. Subnets: []string{
  261. cloudformation.Ref(ParameterSubnet1Id),
  262. cloudformation.Ref(ParameterSubnet2Id),
  263. },
  264. },
  265. },
  266. SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
  267. ServiceName: service.Name,
  268. ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry},
  269. Tags: []tags.Tag{
  270. {
  271. Key: ProjectTag,
  272. Value: project.Name,
  273. },
  274. {
  275. Key: ServiceTag,
  276. Value: service.Name,
  277. },
  278. },
  279. TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)),
  280. }
  281. }
  282. return template, nil
  283. }
  284. func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string {
  285. if sg, ok := net.Extras[ExtensionSecurityGroup]; ok {
  286. logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg)
  287. return sg.(string)
  288. }
  289. var ingresses []ec2.SecurityGroup_Ingress
  290. if !net.Internal {
  291. for _, service := range project.Services {
  292. if _, ok := service.Networks[net.Name]; ok {
  293. for _, port := range service.Ports {
  294. ingresses = append(ingresses, ec2.SecurityGroup_Ingress{
  295. CidrIp: "0.0.0.0/0",
  296. Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol),
  297. FromPort: int(port.Target),
  298. IpProtocol: strings.ToUpper(port.Protocol),
  299. ToPort: int(port.Target),
  300. })
  301. }
  302. }
  303. }
  304. }
  305. securityGroup := networkResourceName(project, net.Name)
  306. template.Resources[securityGroup] = &ec2.SecurityGroup{
  307. GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net.Name),
  308. GroupName: securityGroup,
  309. SecurityGroupIngress: ingresses,
  310. VpcId: vpc,
  311. Tags: []tags.Tag{
  312. {
  313. Key: ProjectTag,
  314. Value: project.Name,
  315. },
  316. {
  317. Key: NetworkTag,
  318. Value: net.Name,
  319. },
  320. },
  321. }
  322. ingress := securityGroup + "Ingress"
  323. template.Resources[ingress] = &ec2.SecurityGroupIngress{
  324. Description: fmt.Sprintf("Allow communication within network %s", net.Name),
  325. IpProtocol: "-1", // all protocols
  326. GroupId: cloudformation.Ref(securityGroup),
  327. SourceSecurityGroupId: cloudformation.Ref(securityGroup),
  328. }
  329. return cloudformation.Ref(securityGroup)
  330. }
  331. func networkResourceName(project *compose.Project, network string) string {
  332. return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network))
  333. }
  334. func serviceResourceName(dependency string) string {
  335. return fmt.Sprintf("%sService", normalizeResourceName(dependency))
  336. }
  337. func normalizeResourceName(s string) string {
  338. return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, ""))
  339. }
  340. func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) {
  341. arns := []string{}
  342. for _, container := range taskDef.ContainerDefinitions {
  343. if container.RepositoryCredentials != nil {
  344. arns = append(arns, container.RepositoryCredentials.CredentialsParameter)
  345. }
  346. if len(container.Secrets) > 0 {
  347. for _, s := range container.Secrets {
  348. arns = append(arns, s.ValueFrom)
  349. }
  350. }
  351. }
  352. if len(arns) > 0 {
  353. return &PolicyDocument{
  354. Statement: []PolicyStatement{
  355. {
  356. Effect: "Allow",
  357. Action: []string{ActionGetSecretValue, ActionGetParameters, ActionDecrypt},
  358. Resource: arns,
  359. }},
  360. }, nil
  361. }
  362. return nil, nil
  363. }