cloudformation.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. package amazon
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/sirupsen/logrus"
  6. cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
  7. ecsapi "github.com/aws/aws-sdk-go/service/ecs"
  8. "github.com/awslabs/goformation/v4/cloudformation"
  9. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  10. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  11. "github.com/awslabs/goformation/v4/cloudformation/iam"
  12. "github.com/awslabs/goformation/v4/cloudformation/logs"
  13. cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
  14. "github.com/awslabs/goformation/v4/cloudformation/tags"
  15. "github.com/docker/ecs-plugin/pkg/compose"
  16. )
  17. const (
  18. ParameterClusterName = "ParameterClusterName"
  19. ParameterVPCId = "ParameterVPCId"
  20. ParameterSubnet1Id = "ParameterSubnet1Id"
  21. ParameterSubnet2Id = "ParameterSubnet2Id"
  22. )
  23. // Convert a compose project into a CloudFormation template
  24. func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) {
  25. warnings := Check(project)
  26. for _, w := range warnings {
  27. logrus.Warn(w)
  28. }
  29. template := cloudformation.NewTemplate()
  30. template.Parameters[ParameterClusterName] = cloudformation.Parameter{
  31. Type: "String",
  32. Description: "Name of the ECS cluster to deploy to (optional)",
  33. }
  34. template.Parameters[ParameterVPCId] = cloudformation.Parameter{
  35. Type: "AWS::EC2::VPC::Id",
  36. Description: "ID of the VPC",
  37. }
  38. /*
  39. FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282
  40. template.Parameters["SubnetIds"] = cloudformation.Parameter{
  41. Type: "List<AWS::EC2::Subnet::Id>",
  42. Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC",
  43. }
  44. */
  45. template.Parameters[ParameterSubnet1Id] = cloudformation.Parameter{
  46. Type: "AWS::EC2::Subnet::Id",
  47. Description: "SubnetId,for Availability Zone 1 in the region in your VPC",
  48. }
  49. template.Parameters[ParameterSubnet2Id] = cloudformation.Parameter{
  50. Type: "AWS::EC2::Subnet::Id",
  51. Description: "SubnetId,for Availability Zone 1 in the region in your VPC",
  52. }
  53. // Create Cluster is `ParameterClusterName` parameter is not set
  54. template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(ParameterClusterName))
  55. template.Resources["Cluster"] = &ecs.Cluster{
  56. ClusterName: project.Name,
  57. Tags: []tags.Tag{
  58. {
  59. Key: ProjectTag,
  60. Value: project.Name,
  61. },
  62. },
  63. AWSCloudFormationCondition: "CreateCluster",
  64. }
  65. cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(ParameterClusterName))
  66. for net := range project.Networks {
  67. name, resource := convertNetwork(project, net, cloudformation.Ref(ParameterVPCId))
  68. template.Resources[name] = resource
  69. }
  70. logGroup := fmt.Sprintf("/docker-compose/%s", project.Name)
  71. template.Resources["LogGroup"] = &logs.LogGroup{
  72. LogGroupName: logGroup,
  73. }
  74. // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local
  75. template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{
  76. Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name),
  77. Name: fmt.Sprintf("%s.local", project.Name),
  78. Vpc: cloudformation.Ref(ParameterVPCId),
  79. }
  80. for _, service := range project.Services {
  81. definition, err := Convert(project, service)
  82. if err != nil {
  83. return nil, err
  84. }
  85. taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", service.Name)
  86. policy, err := c.getPolicy(definition)
  87. if err != nil {
  88. return nil, err
  89. }
  90. rolePolicies := []iam.Role_Policy{}
  91. if policy != nil {
  92. rolePolicies = append(rolePolicies, iam.Role_Policy{
  93. PolicyDocument: policy,
  94. PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name),
  95. })
  96. }
  97. definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole)
  98. taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name)
  99. template.Resources[taskExecutionRole] = &iam.Role{
  100. AssumeRolePolicyDocument: assumeRolePolicyDocument,
  101. Policies: rolePolicies,
  102. ManagedPolicyArns: []string{
  103. ECSTaskExecutionPolicy,
  104. ECRReadOnlyPolicy,
  105. },
  106. }
  107. template.Resources[taskDefinition] = definition
  108. var healthCheck *cloudmap.Service_HealthCheckConfig
  109. if service.HealthCheck != nil && !service.HealthCheck.Disable {
  110. // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD
  111. }
  112. serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", service.Name)
  113. records := []cloudmap.Service_DnsRecord{
  114. {
  115. TTL: 60,
  116. Type: cloudmapapi.RecordTypeA,
  117. },
  118. }
  119. serviceRegistry := ecs.Service_ServiceRegistry{
  120. RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"),
  121. }
  122. if len(service.Ports) > 0 {
  123. records = append(records, cloudmap.Service_DnsRecord{
  124. TTL: 60,
  125. Type: cloudmapapi.RecordTypeSrv,
  126. })
  127. serviceRegistry.Port = int(service.Ports[0].Target)
  128. }
  129. template.Resources[serviceRegistration] = &cloudmap.Service{
  130. Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name),
  131. HealthCheckConfig: healthCheck,
  132. Name: service.Name,
  133. NamespaceId: cloudformation.Ref("CloudMap"),
  134. DnsConfig: &cloudmap.Service_DnsConfig{
  135. DnsRecords: records,
  136. RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue,
  137. },
  138. }
  139. serviceSecurityGroups := []string{}
  140. for net := range service.Networks {
  141. logicalName := networkResourceName(project, net)
  142. serviceSecurityGroups = append(serviceSecurityGroups, cloudformation.Ref(logicalName))
  143. }
  144. template.Resources[fmt.Sprintf("%sService", service.Name)] = &ecs.Service{
  145. Cluster: cluster,
  146. DesiredCount: 1,
  147. LaunchType: ecsapi.LaunchTypeFargate,
  148. NetworkConfiguration: &ecs.Service_NetworkConfiguration{
  149. AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
  150. AssignPublicIp: ecsapi.AssignPublicIpEnabled,
  151. SecurityGroups: serviceSecurityGroups,
  152. Subnets: []string{
  153. cloudformation.Ref(ParameterSubnet1Id),
  154. cloudformation.Ref(ParameterSubnet2Id),
  155. },
  156. },
  157. },
  158. SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
  159. ServiceName: service.Name,
  160. ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry},
  161. TaskDefinition: cloudformation.Ref(taskDefinition),
  162. }
  163. }
  164. return template, nil
  165. }
  166. func convertNetwork(project *compose.Project, net string, vpc string) (string, cloudformation.Resource) {
  167. var ingresses []ec2.SecurityGroup_Ingress
  168. for _, service := range project.Services {
  169. if _, ok := service.Networks[net]; ok {
  170. for _, port := range service.Ports {
  171. ingresses = append(ingresses, ec2.SecurityGroup_Ingress{
  172. CidrIp: "0.0.0.0/0",
  173. Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol),
  174. FromPort: int(port.Target),
  175. IpProtocol: strings.ToUpper(port.Protocol),
  176. ToPort: int(port.Target),
  177. })
  178. }
  179. }
  180. }
  181. securityGroup := networkResourceName(project, net)
  182. resource := &ec2.SecurityGroup{
  183. GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net),
  184. GroupName: securityGroup,
  185. SecurityGroupIngress: ingresses,
  186. VpcId: vpc,
  187. Tags: []tags.Tag{
  188. {
  189. Key: ProjectTag,
  190. Value: project.Name,
  191. },
  192. {
  193. Key: NetworkTag,
  194. Value: net,
  195. },
  196. },
  197. }
  198. return securityGroup, resource
  199. }
  200. func networkResourceName(project *compose.Project, network string) string {
  201. return fmt.Sprintf("%s%sNetwork", project.Name, strings.Title(network))
  202. }
  203. func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) {
  204. arns := []string{}
  205. for _, container := range taskDef.ContainerDefinitions {
  206. if container.RepositoryCredentials != nil {
  207. arns = append(arns, container.RepositoryCredentials.CredentialsParameter)
  208. }
  209. if len(container.Secrets) > 0 {
  210. for _, s := range container.Secrets {
  211. arns = append(arns, s.ValueFrom)
  212. }
  213. }
  214. }
  215. if len(arns) > 0 {
  216. return &PolicyDocument{
  217. Statement: []PolicyStatement{
  218. {
  219. Effect: "Allow",
  220. Action: []string{ActionGetSecretValue, ActionGetParameters, ActionDecrypt},
  221. Resource: arns,
  222. }},
  223. }, nil
  224. }
  225. return nil, nil
  226. }