aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.58k stars 3.88k forks source link

(@aws-cdk/aws-appconfig): Deployment number 1 already exists #18745

Closed samkio closed 2 years ago

samkio commented 2 years ago

What is the problem?

When using the CfnDeployment contruct the following error is observed (transiently) when creating new stacks with this resource.

Error:

PR633-ServerStage | 26/80 | 11:24:45 AM | CREATE_FAILED        | AWS::AppConfig::Deployment                 | PR633/ServerStage/Config/PR633ActivityTypesConfigDeployment (ConfigPR633ActivityTypesConfigDeployment264826C2) Deployment number 1 already exists (Service: AmazonAppConfig; Status Code: 409; Error Code: ConflictException; Request ID: cb454205-4df1-4c91-8a57-932669bbebc5; Proxy: null)

This happens when new stacks are created so there is no pre-existing deployment or configuration to compete against. I've tried things such as changing the physical id but the issue persists. Not sure if this is an CDK issue but appears to be an issue with the CfnDeploymentStrategy construct at least.

Reproduction Steps

Simplified construct.

import { Construct } from "@aws-cdk/core";
import {
  CfnApplication,
  CfnConfigurationProfile,
  CfnDeployment,
  CfnDeploymentStrategy,
  CfnEnvironment,
  CfnHostedConfigurationVersion,
} from "@aws-cdk/aws-appconfig";

const ARRAY_OF_STRINGS_JSON_VALIDATOR = JSON.stringify({
  $schema: "http://json-schema.org/draft-04/schema#",
  description: "Array of strings",
  type: "array",
  items: {
    type: "string",
    minLength: 1,
  },
  uniqueItems: true,
});

// eslint-disable-next-line import/prefer-default-export
export class ConfigConstruct extends Construct {
  public readonly application: string;

  public readonly environment: string;

  private readonly applicationId: string;

  private readonly environmentId: string;

  private readonly configProfile1id: string;

  private readonly configProfile2Id: string;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    this.application = `PR633-app`;
    this.environment = "PR633";

    const appConfigApplication = new CfnApplication(this, "Store", {
      name: this.application,
    });
    this.applicationId = appConfigApplication.ref;

    const appConfigEnvironment = new CfnEnvironment(this, "Environment", {
      applicationId: this.applicationId,
      name: this.environment,
    });
    this.environmentId = appConfigEnvironment.ref;

    const configProfile1 = new CfnConfigurationProfile(
      this,
      "ConfigurationProfile1",
      {
        applicationId: this.applicationId,
        name: "Config1",
        locationUri: "hosted",
        validators: [
          { type: "JSON_SCHEMA", content: ARRAY_OF_STRINGS_JSON_VALIDATOR },
        ],
      }
    );
    this.configProfile1id = configProfile1.ref;

    const configProfile2 = new CfnConfigurationProfile(
      this,
      "ConfigurationProfile2",
      {
        applicationId: this.applicationId,
        name: "Config2",
        locationUri: "hosted",
        validators: [
          { type: "JSON_SCHEMA", content: ARRAY_OF_STRINGS_JSON_VALIDATOR },
        ],
      }
    );
    this.configProfile2Id = configProfile2.ref;

    const deploymentStrategy = new CfnDeploymentStrategy(
      this,
      `DeploymentStrategy`,
      {
        name: `quick-deploy`,
        deploymentDurationInMinutes: 0,
        growthFactor: 100,
        finalBakeTimeInMinutes: 0,
        replicateTo: "NONE",
        growthType: "LINEAR",
        description: "Deploy all with no bake time.",
      }
    );

    const config1 = new CfnHostedConfigurationVersion(this, `Config1`, {
      applicationId: this.applicationId,
      configurationProfileId: this.configProfile1id,
      contentType: "application/json",
      content: JSON.stringify(["1", "2"]),
    });
    new CfnDeployment(this, `ConfigDeployment1`, {
      applicationId: this.applicationId,
      configurationProfileId: this.configProfile1id,
      configurationVersion: config1.ref,
      deploymentStrategyId: deploymentStrategy.ref,
      environmentId: this.environmentId,
    });
    const config2 = new CfnHostedConfigurationVersion(this, `Config2`, {
      applicationId: this.applicationId,
      configurationProfileId: this.configProfile2Id,
      contentType: "application/json",
      content: JSON.stringify(["a", "b"]),
    });
    new CfnDeployment(this, `ConfigDeployment2`, {
      applicationId: this.applicationId,
      configurationProfileId: this.configProfile2Id,
      configurationVersion: config2.ref,
      deploymentStrategyId: deploymentStrategy.ref,
      environmentId: this.environmentId,
    });
  }
}

What did you expect to happen?

AppConfig deployment to be performed successfully.

What actually happened?

CloudFormation deploy fails with:

PR633-ServerStage | 26/80 | 11:24:45 AM | CREATE_FAILED        | AWS::AppConfig::Deployment                 | PR633/ServerStage/Config/PR633ActivityTypesConfigDeployment (ConfigPR633ActivityTypesConfigDeployment264826C2) Deployment number 1 already exists (Service: AmazonAppConfig; Status Code: 409; Error Code: ConflictException; Request ID: cb454205-4df1-4c91-8a57-932669bbebc5; Proxy: null)

CDK CLI Version

1.138.0

Framework Version

No response

Node.js Version

14

OS

CodeBuild Linux

Language

Typescript

Language Version

TypeScript (3.9.7)

Other information

No response

rix0rrr commented 2 years ago

I'm sorry, I cannot help you with this. When you are using classes that start with the Cfn prefix, you are effectively writing raw CloudFormation templates. These classes are provided for completeness, but they haven't had any attention from the CDK team in order to provide sensible defaults or add validations (as opposed to the classes that do not start with Cfn). We also haven't had time to investigate this service and become experts at them.

I suggest you read the CloudFormation documentation for this service to see how these constructs should be used, and search for CloudFormation examples or ask for help from the CloudFormation or service team in this matter.

github-actions[bot] commented 2 years ago

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see. If you need more assistance, please either tag a team member or open a new issue that references this one. If you wish to keep having a conversation with other community members under this issue feel free to do so.

OwensCode commented 2 years ago

I think I've resolved this same issue occurring in a SAM deploy by adding a dependency on the first AppConfig deployment to the second AppConfig deployment.

johschmidt42 commented 8 months ago

Also worked for me, thank you @OwensCode!

Here's an AWS CDK Python example:

import json
from pathlib import Path
from typing import List, Optional

from aws_cdk import Stack
from aws_cdk.aws_appconfig import (
    CfnApplication,
    CfnConfigurationProfile,
    CfnDeployment,
    CfnDeploymentStrategy,
    CfnEnvironment,
    CfnHostedConfigurationVersion,
)
from config import Config, read_json_config
from constructs import Construct

class MyStack(Stack):
    def __init__(self, scope: Construct, id: str, config: Config, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        self.config: Config = config
        self._build()

    def _build(self) -> None:
        # aws app config application
        self.app_config: CfnApplication = CfnApplication(
            self,
            f"ApplicationConfig{self.config.APP_CONFIG_ENV_NAME}",
            name=f"{self.config.APP_CONFIG_NAME}_{self.config.APP_CONFIG_ENV_NAME}",
        )

        # aws app config env
        self.app_env: CfnEnvironment = CfnEnvironment(
            self,
            f"Environment{self.config.APP_CONFIG_ENV_NAME}",
            application_id=self.app_config.attr_application_id,
            name=f"{self.config.APP_CONFIG_ENV_NAME}",
        )

        # aws app config deployment strategy
        self.app_deployment_strategy: CfnDeploymentStrategy = CfnDeploymentStrategy(
            self,
            f"AppConfigDeploymentStrategy{self.config.APP_CONFIG_ENV_NAME}",
            deployment_duration_in_minutes=0,
            growth_factor=1.0,
            replicate_to="NONE",
            name=f"{self.config.APP_CONFIG_DEPLOYMENT_STRATEGY_NAME}_{self.config.APP_CONFIG_ENV_NAME}",
        )

        # paths
        config_paths: List[Path] = [
            self.config.input_config_path,
            self.config.output_config_path,
            self.config.secrets_config_path,
            self.config.transform_config_path,
        ]

        # make each deployment depend on the previous deployment
        dep: Optional[CfnDeployment] = None

        for config_path in config_paths:
            dep: Optional[CfnDeployment] = self.deploy_app_env(config_path, dep)

    def deploy_app_env(self, path, deployment: Optional[CfnDeployment] = None):
        json_data: dict = read_json_config(path)

        app_profile: CfnConfigurationProfile = CfnConfigurationProfile(
            self,
            f"Profile_{self.config.APP_CONFIG_ENV_NAME}_{path.stem}",
            application_id=self.app_config.attr_application_id,
            name=path.stem,
            location_uri="hosted",
        )

        hosted_configuration_version: CfnHostedConfigurationVersion = (
            CfnHostedConfigurationVersion(
                self,
                f"{self.config.APP_CONFIG_ENV_NAME}_{path.stem}",
                application_id=self.app_config.attr_application_id,
                configuration_profile_id=app_profile.attr_configuration_profile_id,
                content=json.dumps(json_data),
                content_type="application/json",
            )
        )

        app_deployment: CfnDeployment = CfnDeployment(  # noqa: F841
            self,
            f"Deployment_{self.config.APP_CONFIG_ENV_NAME}_{path.stem}",
            application_id=self.app_config.attr_application_id,
            configuration_profile_id=app_profile.attr_configuration_profile_id,
            configuration_version=hosted_configuration_version.ref,
            deployment_strategy_id=self.app_deployment_strategy.ref,
            environment_id=self.app_env.ref,
        )

        if deployment is not None:
            app_deployment.add_dependency(deployment)

        return app_deployment