aws / copilot-cli

The AWS Copilot CLI is a tool for developers to build, release and operate production ready containerized applications on AWS App Runner or Amazon ECS on AWS Fargate.
https://aws.github.io/copilot-cli/
Apache License 2.0
3.5k stars 408 forks source link

workload addon needs output from another addon in same workload #4706

Closed str11ngfello closed 1 year ago

str11ngfello commented 1 year ago

Hello! Using the latest copilot 1.26.

I have an Aurora cluster in my api workload, created with "copilot storage init" and it has no modifications. Great!

I used to then go into the AWS console and create a micro ec2 for bastion host, open port 22 and add the apiClusterSecurityGroup and EnvironmentSecurityGroup (created by copilot storage init) to the ec2 and bam! - I'd have a simple way to tunnel into Aurora.

I've decided to create a simple addon in the same work load that will automate this ec2 creation. (both ymls are presented in full below)

When it comes to importing the security groups from the aurora addon and applying to my ec2 bastion within my bastion addon, I'm running into the issue where those values aren't available yet. I added the "Export" line to the apiSecurityGroup in Aurora template thinking that if I did that, then in my bastion I could Import it.

I simply added the last Export line to apiclusterSecurityGroup from the yaml generated for Aurora template by copilot.

Outputs:
  apiclusterSecret: # injected as APICLUSTER_SECRET environment variable by Copilot.
    Description: "The JSON secret that holds the database username and password. Fields are 'host', 'port', 'dbname', 'username', 'password', 'dbClusterIdentifier' and 'engine'"
    Value: !Ref apiclusterAuroraSecret
  apiclusterSecurityGroup:
    Description: "The security group to attach to the workload."
    Value: !Ref apiclusterSecurityGroup
    Export:
      Name: !Sub 'copilot-${App}-${Env}-apiclusterSecurityGroup'

In my bastion.yml for my ec2, I thought I could use Import now. See the SecurityGroupIds in this part of my bastion.yml. you can see I'm !ImportValue-ing them.

BastionHost:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-02f97949d306b597a
      InstanceType: t2.micro
      SecurityGroupIds:
        - !GetAtt 'BastionSecurityGroup.GroupId'
        - !ImportValue 
            Fn::Sub: 'copilot-${App}-${Env}-apiclusterSecurityGroup'
        - !ImportValue
            Fn::Sub: '${App}-${Env}-EnvironmentSecurityGroup'
      SubnetId: !Select [0, !Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PublicSubnets' }]]
      KeyName: !Ref BastionHostKeyPair
      Tags:
        - Key: Name
          Value: !Sub 'copilot-bastion-${App}-${Env}'

This will not work as evidenced by the error when I try to deploy the workload. I see =>

No export named copilot-akva-staging-apiclusterSecurityGroup found. Ro                                              
    llback requested by user.                                                                                           

So the entire stack doesn't deploy. To work around this, I remove my bastion.yml and deployed the stack. Then I add my bastion.yml back in and deploy again. No problems. These Exports and Imports between my two files work if I've already created the stack with Aurora once without bastion. This is obviously a dependency problem between my two addons. I read that Export and Import were really for cross stack referencing but I think copilot puts all the these addons in the same stack. I cruised the CloudFormation docs but not sure if this is a CloudFormation user error on my part, or something in the way Copilot has made assumptions on dependencies between same workload addons.

What's the right way to get values out of addons in the same workload, ie. how do I solve this dependency problem such that the output security groups are available to addons in the same workload?

For reference here are my complete ymls for aurora and ec2.

Thank you!


Aurora cluster template addon from copilot storage init (only thing I've added is the Export of the output)

Parameters:
  App:
    Type: String
    Description: Your application's name.
  Env:
    Type: String
    Description: The environment name your service, job, or workflow is being deployed to.
  Name:
    Type: String
    Description: The name of the service, job, or workflow being deployed.
  # Customize your Aurora Serverless cluster by setting the default value of the following parameters.
  apiclusterDBName:
    Type: String
    Description: The name of the initial database to be created in the Aurora Serverless v2 cluster.
    Default: akva
    # Cannot have special characters
    # Naming constraints: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints
Mappings:
  apiclusterEnvScalingConfigurationMap: 
    All:
      "DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128
      "DBMaxCapacity": 8   # AllowedValues: from 0.5 through 128

Resources:
  apiclusterDBSubnetGroup:
    Type: 'AWS::RDS::DBSubnetGroup'
    Properties:
      DBSubnetGroupDescription: Group of Copilot private subnets for Aurora Serverless v2 cluster.
      SubnetIds:
        !Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' }]
  apiclusterSecurityGroup:
    Metadata:
      'aws:copilot:description': 'A security group for your workload to access the Aurora Serverless v2 cluster apicluster'
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: !Sub 'The Security Group for ${Name} to access Aurora Serverless v2 cluster apicluster.'
      VpcId:
        Fn::ImportValue:
          !Sub '${App}-${Env}-VpcId'
      Tags:
        - Key: Name
          Value: !Sub 'copilot-${App}-${Env}-${Name}-Aurora'
  apiclusterDBClusterSecurityGroup:
    Metadata:
      'aws:copilot:description': 'A security group for your Aurora Serverless v2 cluster apicluster'
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: The Security Group for the Aurora Serverless v2 cluster.
      SecurityGroupIngress:
        - ToPort: 5432
          FromPort: 5432
          IpProtocol: tcp
          Description: !Sub 'From the Aurora Security Group of the workload ${Name}.'
          SourceSecurityGroupId: !Ref apiclusterSecurityGroup
      VpcId:
        Fn::ImportValue:
          !Sub '${App}-${Env}-VpcId'
  apiclusterAuroraSecret:
    Metadata:
      'aws:copilot:description': 'A Secrets Manager secret to store your DB credentials'
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: !Sub Aurora main user secret for ${AWS::StackName}
      GenerateSecretString:
        SecretStringTemplate: '{"username": "postgres"}'
        GenerateStringKey: "password"
        ExcludePunctuation: true
        IncludeSpace: false
        PasswordLength: 16
  apiclusterDBClusterParameterGroup:
    Metadata:
      'aws:copilot:description': 'A DB parameter group for engine configuration values'
    Type: 'AWS::RDS::DBClusterParameterGroup'
    Properties:
      Description: !Ref 'AWS::StackName'
      Family: 'aurora-postgresql14'
      Parameters:
        client_encoding: 'UTF8'
  apiclusterDBCluster:
    Metadata:
      'aws:copilot:description': 'The apicluster Aurora Serverless v2 database cluster'
    Type: 'AWS::RDS::DBCluster'
    Properties:
      MasterUsername:
        !Join [ "",  [ '{{resolve:secretsmanager:', !Ref apiclusterAuroraSecret, ":SecretString:username}}" ]]
      MasterUserPassword:
        !Join [ "",  [ '{{resolve:secretsmanager:', !Ref apiclusterAuroraSecret, ":SecretString:password}}" ]]
      DatabaseName: !Ref apiclusterDBName
      Engine: 'aurora-postgresql'
      EngineVersion: '14.4'
      DBClusterParameterGroupName: !Ref apiclusterDBClusterParameterGroup
      DBSubnetGroupName: !Ref apiclusterDBSubnetGroup
      Port: 5432
      VpcSecurityGroupIds:
        - !Ref apiclusterDBClusterSecurityGroup
      ServerlessV2ScalingConfiguration:
        # Replace "All" below with "!Ref Env" to set different autoscaling limits per environment.
        MinCapacity: !FindInMap [apiclusterEnvScalingConfigurationMap, All, DBMinCapacity]
        MaxCapacity: !FindInMap [apiclusterEnvScalingConfigurationMap, All, DBMaxCapacity]
  apiclusterDBWriterInstance:
    Metadata:
      'aws:copilot:description': 'The apicluster Aurora Serverless v2 writer instance'
    Type: 'AWS::RDS::DBInstance'
    Properties:
      DBClusterIdentifier: !Ref apiclusterDBCluster
      DBInstanceClass: db.serverless
      Engine: 'aurora-postgresql'
      PromotionTier: 1
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region

  apiclusterSecretAuroraClusterAttachment:
    Type: AWS::SecretsManager::SecretTargetAttachment
    Properties:
      SecretId: !Ref apiclusterAuroraSecret
      TargetId: !Ref apiclusterDBCluster
      TargetType: AWS::RDS::DBCluster
Outputs:
  apiclusterSecret: # injected as APICLUSTER_SECRET environment variable by Copilot.
    Description: "The JSON secret that holds the database username and password. Fields are 'host', 'port', 'dbname', 'username', 'password', 'dbClusterIdentifier' and 'engine'"
    Value: !Ref apiclusterAuroraSecret
  apiclusterSecurityGroup:
    Description: "The security group to attach to the workload."
    Value: !Ref apiclusterSecurityGroup
    Export:
      Name: !Sub 'copilot-${App}-${Env}-apiclusterSecurityGroup'

bastion.yml (you can see the Imports I'm attempting for security groups)

Parameters:
  App:
    Type: String
    Description: Your application's name.
  Env:
    Type: String
    Description: The environment name your service, job, or workflow is being deployed to.
  Name:
    Type: String
    Description: The name of the service, job, or workflow being deployed.

Resources:

  BastionSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Enable SSH access via port 22
      VpcId:
        Fn::ImportValue: !Sub '${App}-${Env}-VpcId'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0

  BastionHostKeyPair:
    Type: 'AWS::EC2::KeyPair'
    Properties:
      KeyName: !Sub 'copilot-bastion-${App}-${Env}-keypair'

  BastionHost:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-02f97949d306b597a
      InstanceType: t2.micro
      SecurityGroupIds:
        - !GetAtt 'BastionSecurityGroup.GroupId'
        - !ImportValue 
            Fn::Sub: 'copilot-${App}-${Env}-apiclusterSecurityGroup'
        - !ImportValue
            Fn::Sub: '${App}-${Env}-EnvironmentSecurityGroup'
      SubnetId: !Select [0, !Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PublicSubnets' }]]
      KeyName: !Ref BastionHostKeyPair
      Tags:
        - Key: Name
          Value: !Sub 'copilot-bastion-${App}-${Env}'

Outputs:
  BastionHostPublicIP:
    Description: The public IP address of the bastion host
    Value: !GetAtt BastionHost.PublicIp
paragbhingre commented 1 year ago

@str11ngfello I have found this article from the Knowledge Center of AWS that shows how we can pass values between nested stacks. In your case, Aurora and Bastion for EC2 are two nested stacks. There is also stackoverflow solution for the similar problem that you can take look at.
I haven't tried this solution myself yet, but it looks promising to me. Basically, it mentions that you should follow the below syntax to refer to the parameter from another nested stack. 

"BastionHost" : {
        Type: 'AWS::EC2::Instance'
        "Properties" : {
            "SecurityGroupIds:" : {
              - "Fn::GetAtt" : [ "Aurora Stack Name Goes Here", "Outputs.<copilot-${App}-${Env}-apiclusterSecurityGroup>" ]
              - ...
              - ...

            },
            "TemplateURL" : "https://s3.amazonaws.com/url/templates/publicRouteStack.json",
            "TimeoutInMinutes" : "5"
        }
    }

Do let us know if that works fine for you?

str11ngfello commented 1 year ago

Thanks for the response @paragbhingre , but looking in the console that doesn't seem to be the case. Copilot is creating a single nested stack for ALL addons. I have 5 files in addons directory, including the two resources in question (Bastion and Aurora) as well as others like an S3 bucket, redis database,etc,.

They're all in one nested stack. 🤔

image
paragbhingre commented 1 year ago

Oh, got it. I think what we can try here is DependsOn attribute of the CloudFomration. Because all the addons are in one stack, we can add DependsOn: apiclusterSecurityGroup in your bastion template. Can you try that and let us know if that works?

Edit -- You can use !Ref logicalName directly to access another resource from the same nested stack. It should be accessible by Ref.

github-actions[bot] commented 1 year ago

This issue is stale because it has been open 60 days with no response activity. Remove the stale label, add a comment, or this will be closed in 14 days.

github-actions[bot] commented 1 year ago

This issue is closed due to inactivity. Feel free to reopen the issue if you have any further questions!