DevSecOps with AWS - Multi Environment deployments - Part 2
Tags: aws, devsecops, cdk, devops
Published: 2022-10-10 13:11:59

**Level 300**

According to the [first part](https://dev.to/avelez/devsecops-with-aws-multi-environment-deployments-part-1-16i6) in this delivery you can verify the deployment using unique credentials and central account. Suppose that the goal is create a simple stack, for demo purpose an s3 bucket, but in the next sessions the goal is create an end-to-end DevSecOps pipeline for serverless applications, containers and more use cases based on this pattern.

## Hands On

### Requirements

- cdk >= 2.43.0
- AWS CLI >= 2.7.0
- Python >= 3.10.4

### AWS Services

- [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/): is an open-source software development framework to define your cloud application resources using familiar programming languages.
- [AWS Identity and Access Management (IAM)](https://aws.amazon.com/iam/?nc2=h_ql_prod_se_iam): Securely manage identities and access to AWS services and resources.
- [AWS IAM Identity Center (Successor to AWS Single Sign-On)](https://aws.amazon.com/iam/identity-center/): helps you securely create or connect your workforce identities and manage their access centrally across AWS accounts and applications.
- [AWS CodeBuild](https://aws.amazon.com/codebuild/): fully managed continuous integration service that compiles source code, runs tests, and produces software packages that are ready to deploy.
- [AWS CodeCommit](https://aws.amazon.com/codecommit/): secure, highly scalable, managed source control service that hosts private Git repositories.
- [AWS CodePipeline](https://aws.amazon.com/codepipeline/): fully managed continuous delivery service that helps you automate your release pipelines for fast and reliable application and infrastructure updates.
- [AWS Key Management Service (AWS KMS)](https://aws.amazon.com/kms/): lets you create, manage, and control cryptographic keys across your applications and more than 100 AWS services.
- [AWS CloudFormation](https://aws.amazon.com/cloudformation/): Speed up cloud provisioning with infrastructure as code

### Solution Overview

![Solution Overview - Simple CDK Pipeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kxuo1yngv66xisnxh6ka.png)
Figure 1. Solution Overview – Simple CDK Pipeline


The Figure 1 shows the steps to accomplish this task. Also, it shows a cross account pipeline using AWS CodePipeline, AWS CodeCommit, AWS Codebuild and AWS CloudFormation. But, how can you construct this pipeline secure and with the minimum effort? The answer [CDK Pipelines](https://docs.aws.amazon.com/cdk/v2/guide/cdk_pipeline.html).

This pipeline is a default pipeline composed by next steps:

1.The changes are detected and activate de pipeline. For this demo the branch `master` is the default branch.
2.The CDK project is synthesized.
3.The pipeline run self-update action.
4.The Cloudformation stack is prepared for developer environment.
5.The Cloudformation stack is deployed for developer environment.
6.To move between environments a manual approval step is added. 
7.The Cloudformation stack is prepared for staging environment.
8.The Cloudformation stack is deployed for staging environment.

### Step by Step

1. Setup AWS cli profile using [aws configure](aws configure) command for DevSecOps Account.

```bash
aws configure sso --profile labxl-devsecops
SSO start URL [None]: https://my-sso-portal.awsapps.com/start
SSO region [None]:us-east-1
Using a browser, open the following URL: 
   https://my-sso-portal.awsapps.com/verify
	and enter the following code:
	QCFK-N451
        There are 3 AWS accounts available to you.
	Using the account ID 123456789012
	The only role available to you is: AWSAdministratorAccess
	Using the role name "AWSAdministratorAccess"
	CLI default client Region [None]: us-east-2
	CLI default output format [None]: json
	
	To use this profile, specify the profile name using --profile, as shown:
	
	aws s3 ls --profile labxl-devsecops 

```
2. Create and deploy CDK projects.

> If you are new using CDK you can visit https://docs.aws.amazon.com/cdk/v2/guide/hello_world.html  for more information. 

First, create the project folder and init CDK app.

```
$ mkdir cdk_pipeline_multienvironment
$ cdk init --language python
$ tree
.
├── app.py
├── cdk.json
├── cdk_pipeline_multienvironment
│   ├── cdk_pipeline_multienvironment_stack.py
│   └── __init__.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── source.bat
└── tests
    ├── __init__.py
    └── unit
        ├── __init__.py
        └── test_cdk_pipeline_multienvironment_stack.py

3 directories, 11 files

```

This structure is good but if you prefer can apply changes and customization based on best practices. For example:

- **A good practice is to separate declarative definitions from code**, in this case in a folder `project_configurations` and logic will be in `src` folder.
- For other hand, **is recommended separate the stack from constructs and stateful services** such databases, object storages and files systems **from stales services **as containers, lambdas, Api Gateways and others. For this purpose, the folder stacks and constructs are created.
- The main idea is creating a pipeline with de infrastructure definitions the folder pipeline will be contains the definitions and stages.

Finally, the project looks like:

```bash
.
├── app.py
├── cdk.json
├── project_configs
│   ├── environment_options
│   │   ├── environment_options_template.yaml
│   │   └── environment_options.yaml
│   ├── helpers
│   │   ├── helper.py
│   │   ├── __init__.py
│   │   ├── project_configs.py
│   │   └── __pycache__
│   ├── __init__.py
│   └── __pycache__
│       └── __init__.cpython-310.pyc
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── source.bat
├── src
│   ├── constructs
│   ├── __init__.py
│   ├── pipeline
│   │   ├── cdk_pipeline_multienvironment_stack.py
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   └── stages
│   ├── __pycache__
│   │   └── __init__.cpython-310.pyc
│   └── stacks
│       ├── __init__.py
│       ├── __pycache__
│       └── simple_s3_stack.py
└── tests
    ├── __init__.py
    └── unit
        ├── __init__.py
        └── test_cdk_pipeline_multienvironment_stack.py

```
### Some Code in Deep
Please visit github for downloading the template.
{% embed https://github.com/velez94/cdk_pipeline_multienvironment.git %}

#### Project parameters
All project definitions are in `project_configs/environment_options/environment_options.yaml`

```yaml
project_name: "multiDev"
# Repository definitions
repository_properties:
  repository_name: "cdk_pipeline_multienvironment"
  create_repository: "true"
  description: "Repository for pipeline multiaccount demo"
  branch: "master"

# Multi Environment setup
devsecops_account: "123456789012"
devsecops_region: "us-east-2"

deployment_account: "123456789013"
deployment_region: "us-east-2"

stg_account: "123456789014"
stg_region: "us-east-2"

storage_resources:
  s3:
    - bucket_name: "multi-env-demo"
      versioned: "enable"

# Tags definitions align with corporation instructions
tags:
  - key: "Project"
    value: "multiDev"
  - key: "Environment"
    value: "dev"
  - key: "Owner"
    value: "DevSecOpsAdm"


```

#### The infrastructure
The file `src/stacks/simple_s3_stack.py` has the infrastructure based on project parameters:

```python
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    CfnOutput,
    RemovalPolicy
)
from constructs import Construct


class SimpleS3Stack(Stack):

    def __init__(self, scope: Construct, construct_id: str, props: dict = None, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # The code that defines your stack goes here

        bucket = s3.Bucket(self, id=props["bucket_name"],
                           bucket_name=f'{props["bucket_name"]}-{props["environment"]}',
                           versioned=True if props["versioned"] == "enable" else None,
                           enforce_ssl=True,
                           encryption=s3.BucketEncryption.S3_MANAGED,
                           removal_policy= RemovalPolicy.DESTROY
                           )
        # Define outputs
        CfnOutput(self, id="S3ARNOutput", value=bucket.bucket_arn, description="Bucket ARN")
        

```
#### The Pipeline
The file `src/pipeline/cdk_pipeline_multienvironment_stack.py` defines the pipelien with CDK pipelines construct.

> Suppose that the repository also will be created.
```python
from aws_cdk import (
    # Duration,
    Stack,
    pipelines,
    aws_codecommit as codecommit,
    Environment,
    CfnOutput

)
from constructs import Construct
from .stages.deploy_app_stage import PipelineStageDeployApp


class CdkPipelineMultienvironmentStack(Stack):

    def __init__(self, scope: Construct, construct_id: str,
                 dev_env=Environment,
                 stg_env=Environment,
                 props: dict = None, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # The code that defines your stack goes here
        # Create repository

        rep = codecommit.Repository(
            self,
            props["repository_properties"]["repository_name"],
            repository_name=props["repository_properties"]["repository_name"],
            description=props["repository_properties"]["description"],

        )

        # Create pipeline source based on codecommit repository
        source = pipelines.CodePipelineSource.code_commit(
            repository=rep,
            branch=props["repository_properties"]["branch"]
        )

        # Create pipeline
        pipeline = pipelines.CodePipeline(
            self,
            f"Pipeline{props['project_name']}",
            cross_account_keys=True,
            pipeline_name=f"Pipeline{props['project_name']}",
            synth=pipelines.ShellStep("Synth",
                                      input=source,
                                      commands=[
                                          "npm install -g aws-cdk",
                                          "pip install -r requirements.txt",
                                          "npx cdk synth"
                                      ]
                                      ),
            self_mutation=True,

        )
        # Modify properties, bucket name
        props["storage_resources"]["s3"][0]["environment"]='dev'

        # Create stages
        deploy_dev = PipelineStageDeployApp(self, "DeployDev",
                                            props=props["storage_resources"]["s3"][0], env=dev_env)
        # Add Stage
        deploy_dev_stg= pipeline.add_stage(deploy_dev)

        # Add manual approval to promote staging
        manual_approval = pipelines.ManualApprovalStep("PromoteToStg", comment="Promote to Staging Gate")
        # Define Dependency
        deploy_dev_stg.add_post(manual_approval)


        # Create Stage for Staging Environment
        # Modify properties, bucket name
        props["storage_resources"]["s3"][0]["environment"] = 'stg'

        # Create stages
        deploy_stg = PipelineStageDeployApp(self, "DeployStg", props=props["storage_resources"]["s3"][0], env=stg_env)
        # Add Stage
        pipeline.add_stage(deploy_stg)

        # Build Pipeline
        pipeline.build_pipeline()

        # Define Outputs
        CfnOutput(self, "GRCRepoUrl", value=rep.repository_clone_url_grc, description="GRC Repository Url")
        CfnOutput(self, "PipelineArn", value=pipeline.pipeline.pipeline_arn, description="Pipeline ARN")
        CfnOutput(self, "StageDev", value=deploy_dev.stage_name, description="Stage Dev Name")


```

> If you don’t pass a codepipeline to construct `pipelines.CodePipeline(...` a new underlying pipeline is created. **This is a good option if you want to experiment but in production environments to manage limits and have more control over resources is recommended create custom CodePipeline with an artifact bucket and pass to cdk Pipelines construct.**

#### The app
The CDK app is defined in `app.py` file:

```python
#!/usr/bin/env python3
import os

import aws_cdk as cdk

from src.stacks.simple_s3_stack import SimpleS3Stack
from src.pipeline.cdk_pipeline_multienvironment_stack import CdkPipelineMultienvironmentStack
from project_configs.helpers.project_configs import props, env_client_deployment_account, env_devsecops_account, env_client_stg_account
from project_configs.helpers.helper import set_tags

app = cdk.App()
pipeline_stack = CdkPipelineMultienvironmentStack(
    app,
    "CdkPipelineMultienvironmentStack",
    props=props,
    env=env_devsecops_account,
    dev_env= env_client_deployment_account,
    stg_env=env_client_stg_account
)
set_tags(pipeline_stack, tags=props["tags"])
app.synth()


```
#### Deployment
Use CDK command to verify and deploy. Now, you can see the stacks. 

```commandline
$ cdk ls 
CdkPipelineMultienvironmentStack
CdkPipelineMultienvironmentStack/DeployDev/SimpleS3Stack
CdkPipelineMultienvironmentStack/DeployStg/SimpleS3Stack

```
These 3 stacks are:
1. Main stack `CdkPipelineMultienvironmentStack` create the codepipeline Pipeline with stages and codecommit repository.
2. Deployment stack for development account `CdkPipelineMultienvironmentStack/DeployDev/SimpleS3Stack`.
3. Deployment stack for Staging account `CdkPipelineMultienvironmentStack/DeployStg/SimpleS3Stack`

Finally, you can deploy using the following command:

```commandline
$ cdk deploy -e CdkPipelineMultienvironmentStack --profile labxl-devsecops 
...
Do you wish to deploy these changes (y/n)? y
... 

✨  Synthesis time: 13.01s

CdkPipelineMultienvironmentStack: building assets...

[0%] start: Building 9e7ae300d6878e9ed61c1139167fb9b5edde5d66b34eb6dfb3a0519c95375328:105171185823-us-east-2
[100%] success: Built 9e7ae300d6878e9ed61c1139167fb9b5edde5d66b34eb6dfb3a0519c95375328:105171185823-us-east-2

CdkPipelineMultienvironmentStack: assets built

CdkPipelineMultienvironmentStack: deploying...
[0%] start: Publishing 9e7ae300d6878e9ed61c1139167fb9b5edde5d66b34eb6dfb3a0519c95375328:105171185823-us-east-2
[100%] success: Published 9e7ae300d6878e9ed61c1139167fb9b5edde5d66b34eb6dfb3a0519c95375328:105171185823-us-east-2
CdkPipelineMultienvironmentStack: creating CloudFormation changeset...

 ✅  CdkPipelineMultienvironmentStack


Outputs:
CdkPipelineMultienvironmentStack.GRCRepoUrl = codecommit::us-east-2://cdk_pipeline_multienvironment
CdkPipelineMultienvironmentStack.PipelineArn = arn:aws:codepipeline:us-east-2:123456789012:PipelinemultiDev
CdkPipelineMultienvironmentStack.StageDev = DeployDev
...

```
You can verify the resources into DevSecOps Account in Developer Tools >> CodePipeline



![Pipeline Multi Environment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j7ya1p0bwnommp2oq8gp.png)
Figure 2. Pipeline Multi Environment- AWS Console


The first pipeline execution will fail, you need to add code to repository using git add command with grc url with your devsecops profile, `labxl-devsecops` for demo:

```commandline
git remote add origin codecommit::us-east-2://labxl-devsecops@cdk_pipeline_multienvironment
```
Push the changes with:
 
```commandline
 git push --set-upstream origin master
```
Now the pipeline will deploy the infrastructure stack for each environment.


![Pipeline Multi Environment- AWS Console Codepipeline running](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wvsffau3zhx6z1vznsm.png)
Figure 3. Pipeline Multi Environment- AWS Console Codepipeline running


Now you must approve to promote to Staging environment. The Figure 4 shows the result

![Pipeline Multi Environment- AWS Console Codepipeline and Cloudformation in Dev Account](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/93tje2vxoqthurg2w80l.png)
Figure 4. Pipeline Multi Environment- AWS Console Codepipeline and Cloudformation in Dev Account


#### Clean up

Destroy the stacks individually from you IDE using `cdk destroy `command.
 You should run something like this:

```commandline
cdk destroy -e CdkPipelineMultienvironmentStack --profile labxl-devsecops
...
```


> In the next post you will see how add some practices such SAST, unit test and create reports for you infrastructure.

 
Enjoy and thanks for reading!