Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(EcsDeployAction): define service attributes instead of service itself #17917

Closed

Conversation

tobytipton
Copy link
Contributor

This implements feature request #17911 by providing a service attributes to the Deploy Action.

Also increase the tests to cover the the attribute, as well as provides tests where using service fails and how serviceAttributes can resolve the issue.

closes #17911


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@gitpod-io
Copy link

gitpod-io bot commented Dec 8, 2021

@github-actions github-actions bot added the @aws-cdk/custom-resources Related to AWS CDK Custom Resources label Dec 8, 2021
@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject89A8053A-LhjRyN9kxr8o
  • Commit ID: 7c6c78b
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution @tobytipton, but this is not a change we will make. You can have the same effect by using the FargateService.fromFargateServiceAttributes() and Ec2Service.fromEc2ServiceAttributes(), and passing the name and region (using the ARN) there, and using the existing service property of EcsDeployAction. So, this doesn't add anything to the expressivity of the CDK, while making the API more complex.

@tobytipton
Copy link
Contributor Author

So looking further at FargateService.fromFargateServiceAttributes() seems to work however, ran into a slight issue when the service is created in a stack inside of a pipeline stage, the serviceArn is a token.

Levaraging FargateService.fromFargateServiceAttributes() to get the service in the region with the clusterName (as a const), and serviceName to get out the serviceArn from FargateService.fromFargateServiceAttributes() inside a stack in a stage and then using FargateService.fromFargateServiceAttributes() with the serviceArn and clusterName to create the service to pass to EcsDeployAction. Produces the correct Configuration and Region in the CFN.

      const app = new cdk.App();
      const stack = new cdk.Stack(app, 'PipelineStack', {
        env: {
          region: 'us-east-1',
          account: '234567890123',
        },
      });


      const artifact = new codepipeline.Artifact('Artifact');
      const bucket = new s3.Bucket(stack, 'PipelineBucket', {
        versioned: true,
        removalPolicy: cdk.RemovalPolicy.DESTROY,
      });
      const source = new cpactions.S3SourceAction({
        actionName: 'Source',
        output: artifact,
        bucket,
        bucketKey: 'key',
      });

      class TestStack extends cdk.Stack {
        public readonly serviceArn: string;
        public readonly clusterName: string;
        // eslint-disable-next-line @aws-cdk/no-core-construct
        constructor(scope: Construct, id: string, props: cdk.StackProps) {
          super(scope, id, props);
          const vpc = new ec2.Vpc(this, 'Vpc');
          this.clusterName = 'cluster-name';
          const service = ecs.FargateService.fromFargateServiceAttributes(this, 'FargateService', {
            serviceName: 'service-name',
            cluster: ecs.Cluster.fromClusterAttributes(this, 'Cluster', {
              vpc,
              securityGroups: [],
              clusterName: this.clusterName,
            }),
          });
          this.serviceArn = service.serviceArn;
        }
      }
      class TestStage extends cdk.Stage {
        public readonly serviceArn: string;
        public readonly clusterName: string;
        // eslint-disable-next-line @aws-cdk/no-core-construct
        constructor(scope: Construct, id: string, props: cdk.StageProps) {
          super(scope, id, props);
          const testStack = new TestStack(this, 'ecsStack', {});
          this.serviceArn = testStack.serviceArn;
          this.clusterName = testStack.clusterName;
        }
      }

      const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
        stages: [
          {
            stageName: 'Source',
            actions: [source],
          },
        ],
      });
      const testStage = new TestStage(
        stack,
        'TestStage',
        {
          env: {
            region: 'us-west-2',
            account: '123456789012',
          },
        },
      );
      const testIStage = pipeline.addStage(testStage);
      const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'FargateService', {
        serviceArn: testStage.serviceArn,
        cluster: ecs.Cluster.fromClusterAttributes(stack, 'Cluster', {
          vpc: new ec2.Vpc(stack, 'Vpc'),
          securityGroups: [],
          clusterName: testStage.clusterName,
        }),
      });
      const deployAction = new cpactions.EcsDeployAction({
        actionName: 'ECS',
        service: service,
        imageFile: artifact.atPath('imageFile.json'),
      });
      testIStage.addAction(deployAction);

      app.synth();

generates the region, configuration correctly.

            Actions: [
              {
                Name: 'ECS',
                ActionTypeId: {
                  Category: 'Deploy',
                  Provider: 'ECS',
                },
                Configuration: {
                  ClusterName: 'cluster-name',
                  ServiceName: 'service-name',
                  FileName: 'imageFile.json',
                },
                Region: 'us-west-2',
              },

However if I create the ECS service in the region stack and then "export" out the clusterName and serviceArn, it doesn't set the region correctly, I added the role here because with out it, it was causing The 'account' property must be a concrete value

      const app = new cdk.App();
      const stack = new cdk.Stack(app, 'PipelineStack', {
        env: {
          region: 'us-east-1',
          account: '234567890123',
        },
      });
      const artifact = new codepipeline.Artifact('Artifact');
      const bucket = new s3.Bucket(stack, 'PipelineBucket', {
        versioned: true,
        removalPolicy: cdk.RemovalPolicy.DESTROY,
      });
      const source = new cpactions.S3SourceAction({
        actionName: 'Source',
        output: artifact,
        bucket,
        bucketKey: 'key',
      });
      const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
        stages: [
          {
            stageName: 'Source',
            actions: [source],
          },
        ],
      });

      class TestStack extends cdk.Stack {
        public readonly serviceArn: string;
        public readonly clusterName: string;
        public readonly deployRole: iam.IRole;
        // eslint-disable-next-line @aws-cdk/no-core-construct
        constructor(scope: Construct, id: string, props: cdk.StackProps) {
          super(scope, id, props);
          const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition');
          taskDefinition.addContainer('MainContainer', {
            image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
          });
          const vpc = new ec2.Vpc(this, 'VPC');
          const cluster = new ecs.Cluster(this, 'Cluster', {
            clusterName: cdk.PhysicalName.GENERATE_IF_NEEDED,
            vpc,
          });
          const service = new ecs.FargateService(this, 'FargateService', {
            cluster,
            taskDefinition,
            serviceName: cdk.PhysicalName.GENERATE_IF_NEEDED,
          });
          const importService = ecs.FargateService.fromFargateServiceAttributes(this, 'ImportFargateService', {
            serviceArn: service.serviceArn,
            cluster: cluster,
          });
          this.serviceArn = importService.serviceArn;
          this.clusterName = cluster.clusterName;
          const deployArn = this.formatArn({
            service: 'iam',
            resource: 'role',
            resourceName: 'deployrole',
          });
          this.deployRole = iam.Role.fromRoleArn(
            this,
            'DeployRole',
            deployArn,
          );
        }
      }
      class TestStage extends cdk.Stage {
        public readonly serviceArn: string;
        public readonly clusterName: string;
        public readonly deployRole: iam.IRole;
        // eslint-disable-next-line @aws-cdk/no-core-construct
        constructor(scope: Construct, id: string, props: cdk.StageProps) {
          super(scope, id, props);
          const testStack = new TestStack(this, 'ecsStack', {});
          this.serviceArn = testStack.serviceArn;
          this.clusterName = testStack.clusterName;
          this.deployRole = testStack.deployRole;
        }
      }
      const testStage = new TestStage(
        stack,
        'TestStage',
        {
          env: {
            region: 'us-west-2',
            account: '123456789012',
          },
        },
      );
      const testIStage = pipeline.addStage(testStage);
      const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'FargateService', {
        serviceArn: testStage.serviceArn,
        cluster: ecs.Cluster.fromClusterAttributes(stack, 'Cluster', {
          vpc: new ec2.Vpc(stack, 'Vpc'),
          securityGroups: [],
          clusterName: testStage.clusterName,
        }),
      });
      const deployAction = new cpactions.EcsDeployAction({
        actionName: 'ECS',
        service: service,
        imageFile: artifact.atPath('imageFile.json'),
        role: testStage.deployRole,
      });
      testIStage.addAction(deployAction);

      app.synth();

Generates something like this which has ClusterName, ServiceName, and RoleArn so it should target the correct account, service and cluster however, there is no Region which means it will not work as expected.

         "Actions": [
                  {
                    "ActionTypeId": {
                      "Category": "Deploy",
                      "Owner": "AWS",
                      "Provider": "ECS",
                      "Version": "1"
                    },
                    "Configuration": {
                      "ClusterName": "teststage-ecsstackeecsstackcluster631300db487421429f3b",
                      "ServiceName": {
                        "Fn::Select": [
                          1,
                          {
                            "Fn::Split": [
                              "/",
                              {
                                "Fn::Select": [
                                  5,
                                  {
                                    "Fn::Split": [
                                      ":",
                                      {
                                        "Fn::Join": [
                                          "",
                                          [
                                            "arn:",
                                            {
                                              "Ref": "AWS::Partition"
                                            },
                                            ":ecs:us-west-2:123456789012:service/teststage-ecsstackeecsstackcluster631300db487421429f3b/teststage-ecsstackckfargateservicecb7fe40e2cebe441d9da"
                                          ]
                                        ]
                                      }
                                    ]
                                  }
                                ]
                              }
                            ]
                          }
                        ]
                      },
                      "FileName": "imageFile.json"
                    },
                    "InputArtifacts": [
                      {
                        "Name": "Artifact"
                      }
                    ],
                    "Name": "ECS",
                    "RoleArn": {
                      "Fn::Join": [
                        "",
                        [
                          "arn:",
                          {
                            "Ref": "AWS::Partition"
                          },
                          ":iam:us-west-2:123456789012:role/deployrole"
                        ]
                      ]
                    },
                    "RunOrder": 1
                  }
                ],
                "Name": "TestStage"
              }

It appears in the working example where the import is using names to import the resource serviceArn is "arn:${Token[AWS.Partition.9]}:ecs:us-west-2:123456789012:service/service-name" where the create of new resource gives a token for the serviceArn on the service, which is why the region doesn't get generated.

To workaround this had to do define the serviceArn on the stack which creates the service like so rather than trying to use the service.serviceArn directly.

const serviceArn = this.formatArn({
    service: 'ecs',
    resource: 'service',
    resourceName: service.serviceName,
});

that generates the serviceArn to "arn:${Token[AWS.Partition.8]}:ecs:us-west-2:123456789012:service/${Token[TOKEN.1785]}" so when the FargateService.fromFargateServiceAttributes is passed the serviceArn the Region can correctly be determined from the serviceArn, as well determine the ServiceName.

@tobytipton tobytipton closed this Dec 14, 2021
@tobytipton tobytipton deleted the ecsDeployActionServiceAttributes branch December 14, 2021 18:21
@skinny85
Copy link
Contributor

Thanks for the detailed explanation @tobytipton!

Do you think it's worth it to update our documentation with your findings?

@tobytipton
Copy link
Contributor Author

I think it would be helpful to provide some additional documentation around this. Part of the reason I provided this detail was to make it easier for later reference for others which might be running into a similar scenario.

I created my self some test with the above examples for testing them out to make sure I had it working correctly.

@skinny85
Copy link
Contributor

@tobytipton I see you opened #18042, I assume that's the example you're referencing here? Anyway, thanks for all your work on this!

@tobytipton
Copy link
Contributor Author

Sorry I was referring to the example I did in #17917 which is for just the aws-codepipelines Pipeline API #18042 is the example/test for modern CDK PIpelines If you would like I can do test/example for the aws-codepipelines Pipeline API as well.

@skinny85
Copy link
Contributor

If you have the time, it would of course be awesome to get an example for the @aws-cdk/aws-codepipeline API too 🙂.

@tobytipton
Copy link
Contributor Author

I added the example/test to #18042 if that is ok, since both are leveraging ECS Deploy Action.

@skinny85
Copy link
Contributor

Actually, can you split that out to a separate PR?

Those two modules are owned by different owners, so splitting them up will make them easier to review and then merge.

@tobytipton
Copy link
Contributor Author

Separate PR created #18059

@skinny85
Copy link
Contributor

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/custom-resources Related to AWS CDK Custom Resources
Projects
None yet
Development

Successfully merging this pull request may close these issues.

(EcsDeployAction): define service attributes instead of service itself.
4 participants