cloudformation.go 9.4 KB

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