AWS Cloud Development Kit (CDK)

AWS Cloud Development Kit (CDK)

  1. https://docs.aws.amazon.com/cdk/v2/guide/home.html
  2. https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US
  3. cdk project structure: https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US/6000-go-workshop/200-create-project/220-structure

cdk.json

The cdk.json file tells the CDK Toolkit how to execute your app.

CDK commands

Command Desription Notes
cdk synth emits the synthesized CloudFormation template https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US/6000-go-workshop/200-create-project/230-synth
cdk diff compare deployed stack with current state
cdk bootstrap Bootstrapping an environment: The first time you deploy an AWS CDK app into an environment (account/region), you can install a “bootstrap stack”. This stack includes resources that are used in the toolkit’s operation. For example, the stack includes an S3 bucket that is used to store templates and assets during the deployment process. https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US/6000-go-workshop/200-create-project/240-deploy
cdk deploy deploy this stack to your default AWS account/region https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US/6000-go-workshop/200-create-project/240-deploy

AWS Construct Library

https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US/6000-go-workshop/300-create-hello-cdk-app/320-lambda#install-the-aws-lambda-construct-library

The AWS CDK is shipped with an extensive library of constructs called the AWS Construct Library. The construct library is divided into modules, one for each AWS service. For example, if you want to define an AWS Lambda function, we will need to use the AWS Lambda construct library.

Things to note

  1. The code in the cdk folder can be written in any language I want.
  2. The code in the src folder can be written in any language I want.
  3. The only important thing is to refer to the handlers in src code from the cdk functions.

How to set-up more than one handler using cdk?

Use api gateway.

See https://github.com/explorer436/programming-playground/tree/main/aws

Runtimes to use

The go1.x runtime is deprecated, but it doesn’t mean that golang executing in a lambda is deprecated. You just need to

  1. specify that you’re going to use a custom runtime like provided.al2 (See https://docs.aws.amazon.com/lambda/latest/dg/lambda-golang.html#golang-al1) and
  2. name your compiled binary “bootstrap” in the root of the zip that gets deployed
  3. or you can deploy using containers stored in ECR and specify any given entrypoint in the Dockerfile if you need more flexibility.

Common errors

[explorer436@explorer436-lenovo-legion-82b1 cdk-workshop-golang]$ cdk bootstrap
 ⏳  Bootstrapping environment aws://920372989293/us-east-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
 ❌  Environment aws://920372989293/us-east-1 failed bootstrapping: _ToolkitError: The stack named CDKToolkit is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED (The following resource(s) failed to delete: [ContainerAssetsRepository]. )

Resolving the errors

  1. Manually delete the stack from the aws console.
  2. Update the permissions for the IAM user.
  3. Run the bootstrap command again.

Permissions

The IAM user needed these policies to be able to bootstrap the stack successfully.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:GenerateCredentialReport",
                "iam:GenerateServiceLastAccessedDetails",
                "iam:Get*",
                "iam:List*",
                "iam:SimulateCustomPolicy",
                "iam:SimulatePrincipalPolicy",
                "iam:CreateRole",
                "iam:TagRole",
                "iam:DeleteRole",
                "iam:AttachRolePolicy",
                "iam:PutRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:DetachRolePolicy",
                "iam:PassRole"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ECR",
            "Effect": "Allow",
            "Action": [
                "ecr:PutLifecyclePolicy",
                "ecr:DeleteRepository",
                "ecr:SetRepositoryPolicy",
                "ecr:CreateRepository",
                "ecr:DescribeRepositories"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:DescribeStacks",
                "cloudformation:DescribeStackEvents",
                "cloudformation:DescribeStackResource",
                "cloudformation:DescribeStackResources",
                "cloudformation:DeleteStack",
                "cloudformation:CreateChangeSet",
                "cloudformation:DescribeChangeSet",
                "cloudformation:GetTemplate",
                "cloudformation:DeleteChangeSet",
                "cloudformation:ExecuteChangeSet"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:Describe*",
                "ssm:Get*",
                "ssm:List*",
                "ssm:PutParameter"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:CreateBucket",
                "s3:PutEncryptionConfiguration",
                "s3:PutLifecycleConfiguration",
                "s3:PutBucketVersioning",
                "s3:PutBucketPublicAccessBlock",
                "s3:PutBucketPolicy",
                "s3:DeleteBucketPolicy",
                "s3:GetBucketLocation",
                "s3:ListBucket",
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": "*"
        }
    ]
}

Exploring the resources after the Stack is created

https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US/6000-go-workshop/200-create-project/240-deploy#the-cloudformation-console

  1. In the aws console, Navigate to CloudFormation > Stacks > CDKToolkit
  2. Look at the Resources, Outputs and Template tab.

Sample template that is generated

Description: This stack includes resources needed to deploy AWS CDK apps into this environment
Parameters:
  TrustedAccounts:
    Description: List of AWS accounts that are trusted to publish assets and deploy stacks to this environment
    Default: ""
    Type: CommaDelimitedList
  TrustedAccountsForLookup:
    Description: List of AWS accounts that are trusted to look up values in this environment
    Default: ""
    Type: CommaDelimitedList
  CloudFormationExecutionPolicies:
    Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation deployment role
    Default: ""
    Type: CommaDelimitedList
  FileAssetsBucketName:
    Description: The name of the S3 bucket used for file assets
    Default: ""
    Type: String
  FileAssetsBucketKmsKeyId:
    Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed S3 key, or the ID/ARN of an existing key.
    Default: ""
    Type: String
  ContainerAssetsRepositoryName:
    Description: A user-provided custom name to use for the container assets ECR repository
    Default: ""
    Type: String
  Qualifier:
    Description: An identifier to distinguish multiple bootstrap stacks in the same environment
    Default: hnb659fds
    Type: String
    AllowedPattern: "[A-Za-z0-9_-]{1,10}"
    ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters
  PublicAccessBlockConfiguration:
    Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration
    Default: "true"
    Type: String
    AllowedValues:
      - "true"
      - "false"
  InputPermissionsBoundary:
    Description: Whether or not to use either the CDK supplied or custom permissions boundary
    Default: ""
    Type: String
  UseExamplePermissionsBoundary:
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Type: String
  BootstrapVariant:
    Type: String
    Default: "AWS CDK: Default Resources"
    Description: Describe the provenance of the resources in this bootstrap stack. Change this when you customize the template. To prevent accidents, the CDK CLI will not overwrite bootstrap stacks with a different variant.
Conditions:
  HasTrustedAccounts:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Fn::Join:
              - ""
              - Ref: TrustedAccounts
  HasTrustedAccountsForLookup:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Fn::Join:
              - ""
              - Ref: TrustedAccountsForLookup
  HasCloudFormationExecutionPolicies:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Fn::Join:
              - ""
              - Ref: CloudFormationExecutionPolicies
  HasCustomFileAssetsBucketName:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Ref: FileAssetsBucketName
  CreateNewKey:
    Fn::Equals:
      - ""
      - Ref: FileAssetsBucketKmsKeyId
  UseAwsManagedKey:
    Fn::Equals:
      - AWS_MANAGED_KEY
      - Ref: FileAssetsBucketKmsKeyId
  ShouldCreatePermissionsBoundary:
    Fn::Equals:
      - "true"
      - Ref: UseExamplePermissionsBoundary
  PermissionsBoundarySet:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Ref: InputPermissionsBoundary
  HasCustomContainerAssetsRepositoryName:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Ref: ContainerAssetsRepositoryName
  UsePublicAccessBlockConfiguration:
    Fn::Equals:
      - "true"
      - Ref: PublicAccessBlockConfiguration
Resources:
  FileAssetsBucketEncryptionKey:
    Type: AWS::KMS::Key
    Properties:
      KeyPolicy:
        Statement:
          - Action:
              - kms:Create*
              - kms:Describe*
              - kms:Enable*
              - kms:List*
              - kms:Put*
              - kms:Update*
              - kms:Revoke*
              - kms:Disable*
              - kms:Get*
              - kms:Delete*
              - kms:ScheduleKeyDeletion
              - kms:CancelKeyDeletion
              - kms:GenerateDataKey
              - kms:TagResource
              - kms:UntagResource
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
            Resource: "*"
          - Action:
              - kms:Decrypt
              - kms:DescribeKey
              - kms:Encrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
            Effect: Allow
            Principal:
              AWS: "*"
            Resource: "*"
            Condition:
              StringEquals:
                kms:CallerAccount:
                  Ref: AWS::AccountId
                kms:ViaService:
                  - Fn::Sub: s3.${AWS::Region}.amazonaws.com
          - Action:
              - kms:Decrypt
              - kms:DescribeKey
              - kms:Encrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
            Effect: Allow
            Principal:
              AWS:
                Fn::Sub: ${FilePublishingRole.Arn}
            Resource: "*"
    Condition: CreateNewKey
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  FileAssetsBucketEncryptionKeyAlias:
    Condition: CreateNewKey
    Type: AWS::KMS::Alias
    Properties:
      AliasName:
        Fn::Sub: alias/cdk-${Qualifier}-assets-key
      TargetKeyId:
        Ref: FileAssetsBucketEncryptionKey
  StagingBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName:
        Fn::If:
          - HasCustomFileAssetsBucketName
          - Fn::Sub: ${FileAssetsBucketName}
          - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region}
      AccessControl: Private
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
              KMSMasterKeyID:
                Fn::If:
                  - CreateNewKey
                  - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
                  - Fn::If:
                      - UseAwsManagedKey
                      - Ref: AWS::NoValue
                      - Fn::Sub: ${FileAssetsBucketKmsKeyId}
      PublicAccessBlockConfiguration:
        Fn::If:
          - UsePublicAccessBlockConfiguration
          - BlockPublicAcls: true
            BlockPublicPolicy: true
            IgnorePublicAcls: true
            RestrictPublicBuckets: true
          - Ref: AWS::NoValue
      VersioningConfiguration:
        Status: Enabled
      LifecycleConfiguration:
        Rules:
          - Id: CleanupOldVersions
            Status: Enabled
            NoncurrentVersionExpiration:
              NoncurrentDays: 30
          - Id: AbortIncompleteMultipartUploads
            Status: Enabled
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 1
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  StagingBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: StagingBucket
      PolicyDocument:
        Id: AccessControl
        Version: "2012-10-17"
        Statement:
          - Sid: AllowSSLRequestsOnly
            Action: s3:*
            Effect: Deny
            Resource:
              - Fn::Sub: ${StagingBucket.Arn}
              - Fn::Sub: ${StagingBucket.Arn}/*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Principal: "*"
  ContainerAssetsRepository:
    Type: AWS::ECR::Repository
    Properties:
      ImageTagMutability: IMMUTABLE
      LifecyclePolicy:
        LifecyclePolicyText: |
          {
            "rules": [
              {
                "rulePriority": 1,
                "description": "Untagged images should not exist, but expire any older than one year",
                "selection": {
                  "tagStatus": "untagged",
                  "countType": "sinceImagePushed",
                  "countUnit": "days",
                  "countNumber": 365
                },
                "action": { "type": "expire" }
              }
            ]
          }
      RepositoryName:
        Fn::If:
          - HasCustomContainerAssetsRepositoryName
          - Fn::Sub: ${ContainerAssetsRepositoryName}
          - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region}
      RepositoryPolicyText:
        Version: "2012-10-17"
        Statement:
          - Sid: LambdaECRImageRetrievalPolicy
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - ecr:BatchGetImage
              - ecr:GetDownloadUrlForLayer
            Condition:
              StringLike:
                aws:sourceArn:
                  Fn::Sub: arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*
          - Sid: EmrServerlessImageRetrievalPolicy
            Effect: Allow
            Principal:
              Service: emr-serverless.amazonaws.com
            Action:
              - ecr:BatchGetImage
              - ecr:GetDownloadUrlForLayer
              - ecr:DescribeImages
            Condition:
              StringLike:
                aws:sourceArn:
                  Fn::Sub: arn:${AWS::Partition}:emr-serverless:${AWS::Region}:${AWS::AccountId}:/applications/*
  FilePublishingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:TagSession
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Fn::If:
              - HasTrustedAccounts
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccounts
              - Ref: AWS::NoValue
      RoleName:
        Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region}
      Tags:
        - Key: aws-cdk:bootstrap-role
          Value: file-publishing
  ImagePublishingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:TagSession
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Fn::If:
              - HasTrustedAccounts
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccounts
              - Ref: AWS::NoValue
      RoleName:
        Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region}
      Tags:
        - Key: aws-cdk:bootstrap-role
          Value: image-publishing
  LookupRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:TagSession
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Fn::If:
              - HasTrustedAccountsForLookup
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccountsForLookup
              - Ref: AWS::NoValue
          - Fn::If:
              - HasTrustedAccounts
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccounts
              - Ref: AWS::NoValue
      RoleName:
        Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region}
      ManagedPolicyArns:
        - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess
      Policies:
        - PolicyDocument:
            Statement:
              - Sid: DontReadSecrets
                Effect: Deny
                Action:
                  - kms:Decrypt
                Resource: "*"
            Version: "2012-10-17"
          PolicyName: LookupRolePolicy
      Tags:
        - Key: aws-cdk:bootstrap-role
          Value: lookup
  FilePublishingRoleDefaultPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetObject*
              - s3:GetBucket*
              - s3:GetEncryptionConfiguration
              - s3:List*
              - s3:DeleteObject*
              - s3:PutObject*
              - s3:Abort*
            Resource:
              - Fn::Sub: ${StagingBucket.Arn}
              - Fn::Sub: ${StagingBucket.Arn}/*
            Condition:
              StringEquals:
                aws:ResourceAccount:
                  - Fn::Sub: ${AWS::AccountId}
            Effect: Allow
          - Action:
              - kms:Decrypt
              - kms:DescribeKey
              - kms:Encrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
            Effect: Allow
            Resource:
              Fn::If:
                - CreateNewKey
                - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
                - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId}
        Version: "2012-10-17"
      Roles:
        - Ref: FilePublishingRole
      PolicyName:
        Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region}
  ImagePublishingRoleDefaultPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - ecr:PutImage
              - ecr:InitiateLayerUpload
              - ecr:UploadLayerPart
              - ecr:CompleteLayerUpload
              - ecr:BatchCheckLayerAvailability
              - ecr:DescribeRepositories
              - ecr:DescribeImages
              - ecr:BatchGetImage
              - ecr:GetDownloadUrlForLayer
            Resource:
              Fn::Sub: ${ContainerAssetsRepository.Arn}
            Effect: Allow
          - Action:
              - ecr:GetAuthorizationToken
            Resource: "*"
            Effect: Allow
        Version: "2012-10-17"
      Roles:
        - Ref: ImagePublishingRole
      PolicyName:
        Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region}
  DeploymentActionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:TagSession
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Fn::If:
              - HasTrustedAccounts
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccounts
              - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Sid: CloudFormationPermissions
                Effect: Allow
                Action:
                  - cloudformation:CreateChangeSet
                  - cloudformation:DeleteChangeSet
                  - cloudformation:DescribeChangeSet
                  - cloudformation:DescribeStacks
                  - cloudformation:ExecuteChangeSet
                  - cloudformation:CreateStack
                  - cloudformation:UpdateStack
                  - cloudformation:RollbackStack
                  - cloudformation:ContinueUpdateRollback
                Resource: "*"
              - Sid: PipelineCrossAccountArtifactsBucket
                Effect: Allow
                Action:
                  - s3:GetObject*
                  - s3:GetBucket*
                  - s3:List*
                  - s3:Abort*
                  - s3:DeleteObject*
                  - s3:PutObject*
                Resource: "*"
                Condition:
                  StringNotEquals:
                    s3:ResourceAccount:
                      Ref: AWS::AccountId
              - Sid: PipelineCrossAccountArtifactsKey
                Effect: Allow
                Action:
                  - kms:Decrypt
                  - kms:DescribeKey
                  - kms:Encrypt
                  - kms:ReEncrypt*
                  - kms:GenerateDataKey*
                Resource: "*"
                Condition:
                  StringEquals:
                    kms:ViaService:
                      Fn::Sub: s3.${AWS::Region}.amazonaws.com
              - Action: iam:PassRole
                Resource:
                  Fn::Sub: ${CloudFormationExecutionRole.Arn}
                Effect: Allow
              - Sid: CliPermissions
                Action:
                  - cloudformation:DescribeStackEvents
                  - cloudformation:GetTemplate
                  - cloudformation:DeleteStack
                  - cloudformation:UpdateTerminationProtection
                  - sts:GetCallerIdentity
                  - cloudformation:GetTemplateSummary
                Resource: "*"
                Effect: Allow
              - Sid: CliStagingBucket
                Effect: Allow
                Action:
                  - s3:GetObject*
                  - s3:GetBucket*
                  - s3:List*
                Resource:
                  - Fn::Sub: ${StagingBucket.Arn}
                  - Fn::Sub: ${StagingBucket.Arn}/*
              - Sid: ReadVersion
                Effect: Allow
                Action:
                  - ssm:GetParameter
                  - ssm:GetParameters
                Resource:
                  - Fn::Sub: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}
              - Sid: Refactor
                Effect: Allow
                Action:
                  - cloudformation:CreateStackRefactor
                  - cloudformation:DescribeStackRefactor
                  - cloudformation:ExecuteStackRefactor
                  - cloudformation:ListStackRefactorActions
                  - cloudformation:ListStackRefactors
                  - cloudformation:ListStacks
                Resource: "*"
            Version: "2012-10-17"
          PolicyName: default
      RoleName:
        Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region}
      Tags:
        - Key: aws-cdk:bootstrap-role
          Value: deploy
  CloudFormationExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        Fn::If:
          - HasCloudFormationExecutionPolicies
          - Ref: CloudFormationExecutionPolicies
          - Fn::If:
              - HasTrustedAccounts
              - Ref: AWS::NoValue
              - - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess
      RoleName:
        Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region}
      PermissionsBoundary:
        Fn::If:
          - PermissionsBoundarySet
          - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}
          - Ref: AWS::NoValue
  CdkBoostrapPermissionsBoundaryPolicy:
    Condition: ShouldCreatePermissionsBoundary
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Statement:
          - Sid: ExplicitAllowAll
            Action:
              - "*"
            Effect: Allow
            Resource: "*"
          - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied
            Action:
              - iam:CreateUser
              - iam:CreateRole
              - iam:PutRolePermissionsBoundary
              - iam:PutUserPermissionsBoundary
            Condition:
              StringNotEquals:
                iam:PermissionsBoundary:
                  Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
            Effect: Deny
            Resource: "*"
          - Sid: DenyPermBoundaryIAMPolicyAlteration
            Action:
              - iam:CreatePolicyVersion
              - iam:DeletePolicy
              - iam:DeletePolicyVersion
              - iam:SetDefaultPolicyVersion
            Effect: Deny
            Resource:
              Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
          - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole
            Action:
              - iam:DeleteUserPermissionsBoundary
              - iam:DeleteRolePermissionsBoundary
            Effect: Deny
            Resource: "*"
        Version: "2012-10-17"
      Description: Bootstrap Permission Boundary
      ManagedPolicyName:
        Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
      Path: /
  CdkBootstrapVersion:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Name:
        Fn::Sub: /cdk-bootstrap/${Qualifier}/version
      Value: "28"
Outputs:
  BucketName:
    Description: The name of the S3 bucket owned by the CDK toolkit stack
    Value:
      Fn::Sub: ${StagingBucket}
  BucketDomainName:
    Description: The domain name of the S3 bucket owned by the CDK toolkit stack
    Value:
      Fn::Sub: ${StagingBucket.RegionalDomainName}
  FileAssetKeyArn:
    Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated)
    Value:
      Fn::If:
        - CreateNewKey
        - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
        - Fn::Sub: ${FileAssetsBucketKmsKeyId}
    Export:
      Name:
        Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn
  ImageRepositoryName:
    Description: The name of the ECR repository which hosts docker image assets
    Value:
      Fn::Sub: ${ContainerAssetsRepository}
  BootstrapVersion:
    Description: The version of the bootstrap resources that are currently mastered in this stack
    Value:
      Fn::GetAtt:
        - CdkBootstrapVersion
        - Value

Testing through API Gateway

Use your favorite REST client like Postman, Insomnia or ReadyAPI. Setup a new test case and enter the following information.

Example VPC Endpoint - vpce-0a65e5d27ad21a9d0-kmk0ra23.execute-api.us-east-1.vpce.amazonaws.com <br /> Example Host Header - 0rdq6lojwl.execute-api.us-east-1.amazonaws.com <br /> Example Stage - prod <br /> Example Resource - briefs <br />

POST - https://<VPC Endpoint>/<stage>/<resource>
Headers
   - Host
   - Content-Type application/json
Body
{
  "firstName": "bob",
  "lastName": "alice"
}

Where to find the VPC Endpoint

  1. Go To the AWS Console, Search for the VPC Dashboard <br />
  2. On the left hand menu, choose Endpoints <br/>
  3. Filter the list by “API” <br/>
  4. Click the checkbox beside the VPC Endpoint used by the code <br />
  5. Look for the Section DNS Names. Pick the first one.

Where to Find the Host Header, Stage and Resource

  1. Go To the AWS Console, Search for the API Gateway <br />
  2. In the Search Box, type the name of your API Gateway service, and click the link when you find it <br/>
  3. IN the left hand menu, choose Stages <br/>
  4. Expand the stage to see all the endpoints (Note this is the stage, and resources) <br />
  5. There is an info bubble containing the URL that can be used as the host header

Local Development Testing

Prerequisites

  • Docker is Setup and running
  • SAM is setup

if these are not setup, see the Setup document for more details

Testing locally - via API Gateway

Open a command line to the application folder. Run yarn start. This will build the application and deploy it to the running docker instance.

After the command starts, the output in the console will list all of the endpoints that are exposed. You can take these endpoints and set them up in a testing tool of your choice (curl/insomnia/postman).

hitting https://127.0.0.1/health in your browser should give you a message that the service is up and running.

The start command will also watch for any changes to the code and auto deploy the changes to the local running instance. Changes to the CDK stack or configuration files will require a restart of the application.

Testing locally - Isolate a Lambda

Most functions are exposed via the previous command. If you need to test, for example Authorization Lambda, you can not do so via api gateway. That endpoint is not exposed via api gateway and the SAM tool does not support Authorization via API gateway at this time. In order to test a lambda directly you will need to utilizes two commands.

npm run prestart and sam local invoke <lambda to execute> <event>

prestart will run the build and generate the necessary template files for you. Running the sam local command does take some extra effort to setup.

After prestart finishes, open up the template.yml file that is generated at the root of the application. Search for AWS::Lambda::Function. You will see a few search results that looks like the following

hello26396490:
    Type: AWS::Lambda::Function

The first line is the name of the Lambda. Copy that name for later

Run the following command to execute the lambda (replace the name with the name of your lambda)

sam local invoke hello26396490 --no-event

The above command works by not sending any event details to the lambda. In most cases you will want to send event details to the lambda. Below are a few examples of sending event details to the lambda.

sam local invoke nameOfTheLambdaE3E69214 -e test/sam/testMyLambdaAPIGatewayEvent.json
echo '{"body": {"firstname": "John"}}' | sam local invoke hello26396490

The commands above are equivalent. In the first case the event details would be provided in a file. This is good when working with bigger data sets. If you are working with smaller data sets, the second option may be better as you do not have the overhead of creating a file.

Tip - Utilize the templates in the AWS console to help generate the event data.

Note - The first run of this command will take a few minutes as the docker container image is downloaded.

Testing locally with Environment Variables

To test locally using environment variables, perform the following steps

  1. Create a file called env.json in the folder test/sam (may already exist)
  2. In the file place the following contents
    {
      myLambdaNameE3E69214 : {
      }
    }
    
  3. Follow the instructions to invoke a lambda (see Testing locally - Isolate a Lambda) adding –env-vars to the command
    sam local invoke --env-vars test/sam/env.json myLambdaNameE3E69214
    
    or if using the start command
    npm start -- --env-vars test/sam/env.json
    

Reference: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-invoke.html

Stack And Resources

  1. SAML (Security Assertion Markup Language)

Dynamically Getting AWS Environment Info Using CustomResources

  1. CustomResources are used in the Stack to dynamically get the VPC for each environment.

How we deploy to Multiple Environments

By default, the accelerator project that this application was generated from creates an application in AWS with the same name as the name of the application. In AWS terms, all of the components that are created, Lambdas, API Gateway etc, are stored under the umbrella of the application. There can only be one uniquely named application per region per account. So by default if the team wanted to deploy to the Development region (Note our Bamboo setup only support a Sandbox, Development and/or Production region) we could only deploy one stack. What we found was if we tried to setup multiple environments in the same classification, we would simply update and not create a new environment. This limited us to the number of environments we could create.

As a team we decided we would want to have Development, Test and Performance deploys. In order to do this we had to make a change to the unique name that is auto generated for us by the Accelerator.

The stack constructor is what defines the application name. In order to deploy the 3 environments into one region we had to change the name that was being generated. In the code bin/proof-of-insurance-cdk-app.ts the following line was modified from

new ProofOfInsuranceCDKStack(app, StackConfiguration.stackName);

to

new ProofOfInsuranceCDKStack(app, StackConfiguration.appName);

The stack configuration class was also changed to add a new application name field. This property consists of the application name plus the environment name given in Bamboo (not to be confused with the classification in bamboo, which is Sandbox, Development or Production).

"appName": process.env.stack_name || "employee-application" + "-" + (process.env.bamboo_forge_deployment_key || "sandbox"),

With this change, the application name in AWS will now be, for example: employee-application-performance

Now when a new environment is stood up in the Bamboo Pipeline, we get a unique application name for it.

Reference Documents on how the AWS Stack works

  1. https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Stack.html
  2. https://docs.aws.amazon.com/cdk/latest/guide/stacks.html

How local Development works

AWS released a tool called SAM (Serverless Application Model) which works very similar to the CDK. SAM also includes tooling to work locally and simulate an AWS environment.

For SAM to work, Docker is required. When you initialize a lambda for the first time, SAM pulls down a docker container that will simulate a deployed instance of the lambda. SAM utilizes a generated template.yml file which contains the definition of the stack.

Breaking down the main testing commands, the team utilizes npm start npm prestart and npm watch for local testing

npm start - This starts a local SAM server. The SAM server exposes API Gateway definitions. In turn, exposing the lambdas associated to them. The start method utilizes the start-api mechanics of SAM. This is defined as

> Sets up a local endpoint you can use to test your API. Supports hot-reloading so you don’t need to restart this service when you make changes to your function.

Which means that any changes to the build/output folder will automatically be mapped and deployed to the running service. You do not have to start/stop the server after each change. This is achieved by the SAM docker container exposing a volume mapping /var/task (internal) to the applications output folder dist/functions.

One thing missing from testing locally is that Lambda authorization is not supported at this time. This allows you to bypass authorization checks when testing locally. This also means that you can not test Authorization lambdas locally by this command. You will want to look at a combination of npm prestart and sam local invoke commands if you need to test Lambdas not exposed by API gateway.

npm run prestart - This command is the workhorse of this set of commands. Prestart runs the webpack build and then runs a synthesis of the stack in order to generate the necessary cloud formation template that SAM needs to run.

The most important flag in the prestart function is --no-staging. This allows CDK and SAM to talk the same language. Without this flag the root folder would be flooded with asset.1.1. folders and SAM would not startup properly.

When start is run, prestart is automatically run before it. As npm wraps all of its main commands with pre and post commands.

Utilize prestart individually when you want to test a lambda without api gateway

npm run watch - Pairs really nicely with start. After starting the SAM service, run watch. Watch will auto build your lambda functions whenever there are changes and since they are auto mapped into the docker container you do not need to rebuild or restart any services.

The SAM service and this watch will need to be shut down and restarted whenever there are changes made to the stack.

Troubleshooting

API Gateway “Forbidden” after Policy changes

The team ran into an issue where we had created a Lambda with API gateway fronting the function. The team made the API gateway private, but were tweaking the security policy for the VPC. What we found was that making changes to the policy statement attached to the API gateway did not make the changes we expected.

Running cdk synth and cdk diff we would see our changes. When deployed to AWS using cdk deploy (or via Bamboo Pipeline) we would also see the changes listed in API Gateway under the Resource Policy. But the changes never seemed to take effect.

It was found in another document that any changes to a Policy Document/Statement do not initialize a new deployment. Even though the new configuration can be seen in the AWS console, they still have to be deployed manually.

As reference here is a link to the document and and exert > If you update the resource policy after the API is created, you’ll need to deploy the API to propagate the changes after you’ve attached the updated policy. Updating or saving the policy alone won’t change the runtime behavior of the API.

To do a manual deployment, go to the AWS console -> pick api gateway -> find your resource -> actions -> Deploy API

API Gateway is responding with 403 “Missing Authentication Token”

This error occurs when you are attempting to hit an endpoint that has not been exposed. To resolve this issue, perform the following checks

  1. Are you using the correct VPC endpoint?
  2. Is the Host value set properly
  3. Check if you are calling a GET/POST/PUT
  4. Check the URL you are using
  5. Ensure the proper endpoints are exposed in the Policy Document/Statement

Deployed a stack to AWS - How do I delete it

If it is just the AWS Stack that needs to be deleted it can be done via the cloud forge console.

1.Warning1. :: If other resources like repositories and Bamboo pipelines need to be deleted, this is not the right guide.

  1. Open Public Cloud

  2. From the top menu choose AWS > Accounts

  3. Pick the account for your stack. This is the account used to deploy the stack and can be found in the bamboo deploy logs.

  4. Click on the Resources tab

  5. Under management & Governance: choose CloudFormation Stacks

  6. Find your stack, use the filter in the top right to find your stack

  7. Click the stack for the application you want to delete

  8. On the main screen you will see a button to delete the stack.

  9. Click the button and follow the confirmation prompts

As the stack deletes, you can follow the log to see the status of the cleanup. The log can be found on the same page as the delete button. Scroll down on the page and you should see a list of resources and their current state.

Deploy Takes a long time and there are delete failures

When a delete is in progress it may take 5-15 minutes before it errors out. You will see a DELETE_IN_PROGRESS which seems to hang while the deployment is occurring. The message in the log may look similar to

DELETE_FAILED | AWS::IAM::Role | helloServiceRole353607D3 Cannot delete entity, must detach all policies first.

or

DELETE_FAILED | AWS::EC2::SecurityGroup | defaultRouteSecurityGroupC8F16279 resource sg-0e5f515e48512e240 has a dependent object

The reason these deletes fail is due to Lambdas using VPCs. When a lambda is removed from a VPC or the name of the Lambda changes, so does the roles and security groups that are generated.

When a lambda is put in a VPC it creates a network interface. When the Lambda is removed from the VPC, the network interface is not deleted right away. It takes up to 4 hours for AWS to remove the network interface. This is a known issue in AWS.

Because the network interface takes time to delete the security group, which it is dependent on, can not be deleted. This causes the build to hold up.

Though the timing is poor for these deployments, there should be no impacts to your application. In the background after your deployment is compete, AWS seems to quietly clean up these resources for you after the network-interface is cleaned up. After the cleanup is completed by AWS, future deploys should be much faster.

To avoid this issue, use caution when removing Lambdas from VPCs or when changing the ID of any Lambda

Testing Locally with SAM - Gives error after fetching container

If you are on windows, you may not have setup docker correctly. Make sure to read the setup guide and pay attention to the “use linux container” and “shared drive” options. Without these turned on you will continue to get mount issues with the container as the container won’t be able to map your applications output folder to the internal docker volume.

The error you will get will be something like “can not mount to /var/task”

more information here

Analyze the Lambda Stack Size

Size and memory have a major impact on the performance of a Lambda. To perform an analysis on the bundle sizes run the following commands

npm install --g webpack-bundle-analyzer

npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json

Running the 3rd command will pop open a browser window showing a breakdown of all the node modules and package sizes

Details about the webpack analyzer can be found here https://github.com/webpack-contrib/webpack-bundle-analyzer