| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- package amazon
- import (
- "fmt"
- "strings"
- "github.com/sirupsen/logrus"
- cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
- ecsapi "github.com/aws/aws-sdk-go/service/ecs"
- "github.com/awslabs/goformation/v4/cloudformation"
- "github.com/awslabs/goformation/v4/cloudformation/ec2"
- "github.com/awslabs/goformation/v4/cloudformation/ecs"
- "github.com/awslabs/goformation/v4/cloudformation/iam"
- "github.com/awslabs/goformation/v4/cloudformation/logs"
- cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
- "github.com/awslabs/goformation/v4/cloudformation/tags"
- "github.com/docker/ecs-plugin/pkg/compose"
- )
- const (
- ParameterClusterName = "ParameterClusterName"
- ParameterVPCId = "ParameterVPCId"
- ParameterSubnet1Id = "ParameterSubnet1Id"
- ParameterSubnet2Id = "ParameterSubnet2Id"
- )
- // Convert a compose project into a CloudFormation template
- func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) {
- warnings := Check(project)
- for _, w := range warnings {
- logrus.Warn(w)
- }
- template := cloudformation.NewTemplate()
- template.Parameters[ParameterClusterName] = cloudformation.Parameter{
- Type: "String",
- Description: "Name of the ECS cluster to deploy to (optional)",
- }
- template.Parameters[ParameterVPCId] = cloudformation.Parameter{
- Type: "AWS::EC2::VPC::Id",
- Description: "ID of the VPC",
- }
- /*
- FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282
- template.Parameters["SubnetIds"] = cloudformation.Parameter{
- Type: "List<AWS::EC2::Subnet::Id>",
- Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC",
- }
- */
- template.Parameters[ParameterSubnet1Id] = cloudformation.Parameter{
- Type: "AWS::EC2::Subnet::Id",
- Description: "SubnetId,for Availability Zone 1 in the region in your VPC",
- }
- template.Parameters[ParameterSubnet2Id] = cloudformation.Parameter{
- Type: "AWS::EC2::Subnet::Id",
- Description: "SubnetId,for Availability Zone 1 in the region in your VPC",
- }
- // Create Cluster is `ParameterClusterName` parameter is not set
- template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(ParameterClusterName))
- template.Resources["Cluster"] = &ecs.Cluster{
- ClusterName: project.Name,
- Tags: []tags.Tag{
- {
- Key: ProjectTag,
- Value: project.Name,
- },
- },
- AWSCloudFormationCondition: "CreateCluster",
- }
- cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(ParameterClusterName))
- for net := range project.Networks {
- name, resource := convertNetwork(project, net, cloudformation.Ref(ParameterVPCId))
- template.Resources[name] = resource
- }
- logGroup := fmt.Sprintf("/docker-compose/%s", project.Name)
- template.Resources["LogGroup"] = &logs.LogGroup{
- LogGroupName: logGroup,
- }
- // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local
- template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{
- Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name),
- Name: fmt.Sprintf("%s.local", project.Name),
- Vpc: cloudformation.Ref(ParameterVPCId),
- }
- for _, service := range project.Services {
- definition, err := Convert(project, service)
- if err != nil {
- return nil, err
- }
- taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", service.Name)
- policy, err := c.getPolicy(definition)
- if err != nil {
- return nil, err
- }
- rolePolicies := []iam.Role_Policy{}
- if policy != nil {
- rolePolicies = append(rolePolicies, iam.Role_Policy{
- PolicyDocument: policy,
- PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name),
- })
- }
- definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole)
- taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name)
- template.Resources[taskExecutionRole] = &iam.Role{
- AssumeRolePolicyDocument: assumeRolePolicyDocument,
- Policies: rolePolicies,
- ManagedPolicyArns: []string{
- ECSTaskExecutionPolicy,
- ECRReadOnlyPolicy,
- },
- }
- template.Resources[taskDefinition] = definition
- var healthCheck *cloudmap.Service_HealthCheckConfig
- if service.HealthCheck != nil && !service.HealthCheck.Disable {
- // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD
- }
- serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", service.Name)
- records := []cloudmap.Service_DnsRecord{
- {
- TTL: 60,
- Type: cloudmapapi.RecordTypeA,
- },
- }
- serviceRegistry := ecs.Service_ServiceRegistry{
- RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"),
- }
- if len(service.Ports) > 0 {
- records = append(records, cloudmap.Service_DnsRecord{
- TTL: 60,
- Type: cloudmapapi.RecordTypeSrv,
- })
- serviceRegistry.Port = int(service.Ports[0].Target)
- }
- template.Resources[serviceRegistration] = &cloudmap.Service{
- Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name),
- HealthCheckConfig: healthCheck,
- Name: service.Name,
- NamespaceId: cloudformation.Ref("CloudMap"),
- DnsConfig: &cloudmap.Service_DnsConfig{
- DnsRecords: records,
- RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue,
- },
- }
- serviceSecurityGroups := []string{}
- for net := range service.Networks {
- logicalName := networkResourceName(project, net)
- serviceSecurityGroups = append(serviceSecurityGroups, cloudformation.Ref(logicalName))
- }
- template.Resources[fmt.Sprintf("%sService", service.Name)] = &ecs.Service{
- Cluster: cluster,
- DesiredCount: 1,
- LaunchType: ecsapi.LaunchTypeFargate,
- NetworkConfiguration: &ecs.Service_NetworkConfiguration{
- AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
- AssignPublicIp: ecsapi.AssignPublicIpEnabled,
- SecurityGroups: serviceSecurityGroups,
- Subnets: []string{
- cloudformation.Ref(ParameterSubnet1Id),
- cloudformation.Ref(ParameterSubnet2Id),
- },
- },
- },
- SchedulingStrategy: ecsapi.SchedulingStrategyReplica,
- ServiceName: service.Name,
- ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry},
- TaskDefinition: cloudformation.Ref(taskDefinition),
- }
- }
- return template, nil
- }
- func convertNetwork(project *compose.Project, net string, vpc string) (string, cloudformation.Resource) {
- var ingresses []ec2.SecurityGroup_Ingress
- for _, service := range project.Services {
- if _, ok := service.Networks[net]; ok {
- for _, port := range service.Ports {
- ingresses = append(ingresses, ec2.SecurityGroup_Ingress{
- CidrIp: "0.0.0.0/0",
- Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol),
- FromPort: int(port.Target),
- IpProtocol: strings.ToUpper(port.Protocol),
- ToPort: int(port.Target),
- })
- }
- }
- }
- securityGroup := networkResourceName(project, net)
- resource := &ec2.SecurityGroup{
- GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net),
- GroupName: securityGroup,
- SecurityGroupIngress: ingresses,
- VpcId: vpc,
- Tags: []tags.Tag{
- {
- Key: ProjectTag,
- Value: project.Name,
- },
- {
- Key: NetworkTag,
- Value: net,
- },
- },
- }
- return securityGroup, resource
- }
- func networkResourceName(project *compose.Project, network string) string {
- return fmt.Sprintf("%s%sNetwork", project.Name, strings.Title(network))
- }
- func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) {
- arns := []string{}
- for _, container := range taskDef.ContainerDefinitions {
- if container.RepositoryCredentials != nil {
- arns = append(arns, container.RepositoryCredentials.CredentialsParameter)
- }
- if len(container.Secrets) > 0 {
- for _, s := range container.Secrets {
- arns = append(arns, s.ValueFrom)
- }
- }
- }
- if len(arns) > 0 {
- return &PolicyDocument{
- Statement: []PolicyStatement{
- {
- Effect: "Allow",
- Action: []string{ActionGetSecretValue, ActionGetParameters, ActionDecrypt},
- Resource: arns,
- }},
- }, nil
- }
- return nil, nil
- }
|