├── .github └── workflows │ └── manual.yml ├── .gitignore ├── CODEOWNERS ├── Dockerfile ├── README.md ├── aws-auth-patch.yml ├── buildspec.yml ├── ci-cd-codepipeline.cfn.yml ├── examples ├── Cloudformation │ ├── my_parameters.json │ ├── template_EC2.yml │ └── template_VPN.yml ├── Create_Pipeline │ ├── buildspec.yml │ ├── buildspec.yml.zip │ └── simple-pipeline-build.yml ├── Deploy_Flask_App │ ├── Dockerfile │ ├── app.py │ └── deployment.yml ├── flask │ ├── Dockerfile │ └── app.py └── hello │ └── Dockerfile ├── iam-role-policy.json ├── main.py ├── requirements.txt ├── simple_jwt_api.yml ├── test_main.py └── trust.json /.github/workflows/manual.yml: -------------------------------------------------------------------------------- 1 | # Workflow to ensure whenever a Github PR is submitted, 2 | # a JIRA ticket gets created automatically. 3 | name: Manual Workflow 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on pull request events but only for the master branch 8 | pull_request_target: 9 | types: [assigned, opened, reopened] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | test-transition-issue: 16 | name: Convert Github Issue to Jira Issue 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@master 21 | 22 | - name: Login 23 | uses: atlassian/gajira-login@master 24 | env: 25 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 26 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 27 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 28 | 29 | - name: Create NEW JIRA ticket 30 | id: create 31 | uses: atlassian/gajira-create@master 32 | with: 33 | project: CONUPDATE 34 | issuetype: Task 35 | summary: | 36 | Github PR - nd0044 - Full Stack Nanodegree C4 | Repo: ${{ github.repository }} | PR# ${{github.event.number}} 37 | description: | 38 | Repo link: https://github.com/${{ github.repository }} 39 | PR no. ${{ github.event.pull_request.number }} 40 | PR title: ${{ github.event.pull_request.title }} 41 | PR description: ${{ github.event.pull_request.description }} 42 | In addition, please resolve other issues, if any. 43 | fields: '{"components": [{"name":"nd0044 - Full Stack Nanodegree"}], "customfield_16449":"https://classroom.udacity.com/nanodegrees/nd0044/dashboard/overview", "customfield_16450":"Resolve the PR", "labels": ["github"], "priority":{"id": "4"}}' 44 | 45 | - name: Log created issue 46 | run: echo "Issue ${{ steps.create.outputs.issue }} was created" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env* 3 | .flaskenv 4 | *.pyc 5 | *.pyo 6 | env/ 7 | env* 8 | dist/ 9 | build/ 10 | *.egg 11 | *.egg-info/ 12 | _mailinglist 13 | .tox/ 14 | .cache/ 15 | .pytest_cache/ 16 | .idea/ 17 | docs/_build/ 18 | __pycache__ 19 | .env_file 20 | 21 | # Coverage reports 22 | htmlcov/ 23 | .coverage 24 | .coverage.* 25 | *,cover 26 | 27 | # Direnv 28 | .envrc 29 | .direnv 30 | 31 | .github/** 32 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @udacity/active-public-content -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the `python:3.9` as a source image from the Amazon ECR Public Gallery 2 | # We are not using `python:3.7.2-slim` from Dockerhub because it has put a pull rate limit. 3 | FROM public.ecr.aws/sam/build-python3.9:latest 4 | 5 | # Set up an app directory for your code 6 | COPY . /app 7 | WORKDIR /app 8 | 9 | # Install `pip` and needed Python packages from `requirements.txt` 10 | RUN pip install --upgrade pip 11 | RUN pip install -r requirements.txt 12 | 13 | # Define an entrypoint which will run the main app using the Gunicorn WSGI server. 14 | ENTRYPOINT ["gunicorn", "-b", ":8080", "main:APP"] 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploying a Flask API 2 | 3 | This is the project starter repo for the course Server Deployment, Containerization, and Testing. 4 | 5 | In this project you will containerize and deploy a Flask API to a Kubernetes cluster using Docker, AWS EKS, CodePipeline, and CodeBuild. 6 | 7 | The Flask app that will be used for this project consists of a simple API with three endpoints: 8 | 9 | - `GET '/'`: This is a simple health check, which returns the response 'Healthy'. 10 | - `POST '/auth'`: This takes a email and password as json arguments and returns a JWT based on a custom secret. 11 | - `GET '/contents'`: This requires a valid JWT, and returns the un-encrpyted contents of that token. 12 | 13 | The app relies on a secret set as the environment variable `JWT_SECRET` to produce a JWT. The built-in Flask server is adequate for local development, but not production, so you will be using the production-ready [Gunicorn](https://gunicorn.org/) server when deploying the app. 14 | 15 | 16 | 17 | ## Prerequisites 18 | 19 | * Docker Desktop - Installation instructions for all OSes can be found here. 20 | * Git: Download and install Git for your system. 21 | * Code editor: You can download and install VS code here. 22 | * AWS Account 23 | * Python version between 3.7 and 3.9. Check the current version using: 24 | ```bash 25 | # Mac/Linux/Windows 26 | python --version 27 | ``` 28 | You can download a specific release version from here. 29 | 30 | * Python package manager - PIP 19.x or higher. PIP is already installed in Python 3 >=3.4 downloaded from python.org . However, you can upgrade to a specific version, say 20.2.3, using the command: 31 | ```bash 32 | # Mac/Linux/Windows Check the current version 33 | pip --version 34 | # Mac/Linux 35 | pip install --upgrade pip==20.2.3 36 | # Windows 37 | python -m pip install --upgrade pip==20.2.3 38 | ``` 39 | * Terminal 40 | * Mac/Linux users can use the default terminal. 41 | * Windows users can use either the GitBash terminal or WSL. 42 | * Command line utilities: 43 | * AWS CLI installed and configured using the `aws configure` command. Another important configuration is the region. Do not use the us-east-1 because the cluster creation may fails mostly in us-east-1. Let's change the default region to: 44 | ```bash 45 | aws configure set region us-east-2 46 | ``` 47 | Ensure to create all your resources in a single region. 48 | * EKSCTL installed in your system. Follow the instructions [available here](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html#installing-eksctl) or here to download and install `eksctl` utility. 49 | * The KUBECTL installed in your system. Installation instructions for kubectl can be found here. 50 | 51 | 52 | ## Initial setup 53 | 54 | 1. Fork the Server and Deployment Containerization Github repo to your Github account. 55 | 1. Locally clone your forked version to begin working on the project. 56 | ```bash 57 | git clone https://github.com/SudKul/cd0157-Server-Deployment-and-Containerization.git 58 | cd cd0157-Server-Deployment-and-Containerization/ 59 | ``` 60 | 1. These are the files relevant for the current project: 61 | ```bash 62 | . 63 | ├── Dockerfile 64 | ├── README.md 65 | ├── aws-auth-patch.yml #ToDo 66 | ├── buildspec.yml #ToDo 67 | ├── ci-cd-codepipeline.cfn.yml #ToDo 68 | ├── iam-role-policy.json #ToDo 69 | ├── main.py 70 | ├── requirements.txt 71 | ├── simple_jwt_api.yml 72 | ├── test_main.py #ToDo 73 | └── trust.json #ToDo 74 | ``` 75 | 76 | 77 | ## Project Steps 78 | 79 | Completing the project involves several steps: 80 | 81 | 1. Write a Dockerfile for a simple Flask API 82 | 2. Build and test the container locally 83 | 3. Create an EKS cluster 84 | 4. Store a secret using AWS Parameter Store 85 | 5. Create a CodePipeline pipeline triggered by GitHub checkins 86 | 6. Create a CodeBuild stage which will build, test, and deploy your code 87 | 88 | For more detail about each of these steps, see the project lesson. 89 | -------------------------------------------------------------------------------- /aws-auth-patch.yml: -------------------------------------------------------------------------------- 1 | # This is a sample aws-auth-patch.yml file. 2 | # Actual aws-auth-patch.yml will be created at /System/Volumes/Data/private/tmp/aws-auth-patch.yml path. 3 | 4 | apiVersion: v1 5 | data: 6 | mapRoles: | 7 | - groups: 8 | - system:bootstrappers 9 | - system:nodes 10 | rolearn: arn:aws:iam::519002666132:role/eksctl-simple-jwt-api-nodegroup-n-NodeInstanceRole-1DBHED9TMYRZZ 11 | username: system:node:{{EC2PrivateDNSName}} 12 | - system:masters 13 | rolearn: arn:aws:iam::519002666132:role/UdacityFlaskDeployCBKubectlRole 14 | username: build 15 | kind: ConfigMap 16 | metadata: 17 | creationTimestamp: "2022-05-11T11:16:26Z" 18 | name: aws-auth 19 | namespace: kube-system 20 | resourceVersion: "1631" 21 | uid: 86402a4e-a9ff-4721-8c24-f0c4258f7440 22 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 0.2 3 | 4 | 5 | phases: 6 | install: 7 | runtime-versions: 8 | python: 3.7 9 | commands: 10 | - echo 'about to call dockerd' 11 | - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2& 12 | - timeout 15 sh -c "until docker info; do echo .; sleep 1; done" 13 | - curl -sS -o aws-iam-authenticator https://amazon-eks.s3-us-west-2.amazonaws.com/1.10.3/2018-07-26/bin/linux/amd64/aws-iam-authenticator 14 | # Download the latest stable release kubectl 15 | # - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" 16 | # You must use a kubectl version that is within one minor version difference of your Amazon EKS cluster control plane. 17 | # For example, a 1.21 kubectl client works with Kubernetes 1.20, 1.21 and 1.22 clusters. 18 | # Ref: https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html OR https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/ 19 | # To download a specific version v1.27.9 on Linux, use: 20 | - curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.27.9/2024-01-04/bin/linux/amd64/kubectl 21 | # Download the kubectl checksum file 22 | - curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.27.9/2024-01-04/bin/linux/amd64/kubectl.sha256 23 | # Validate the kubectl binary against the checksum file 24 | - sha256sum -c kubectl.sha256 25 | # Install kubectl 26 | - chmod +x ./kubectl ./aws-iam-authenticator 27 | # - mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$PATH:$HOME/bin 28 | - export PATH=$PWD/:$PATH 29 | - python --version 30 | - echo 'export PATH=$PWD/:$PATH' >> $HOME/.bashrc 31 | - echo `kubectl version --short --client` 32 | - python -m pip install --upgrade --force pip 33 | - wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add 34 | - apt-get update && apt-get -y install jq && pip install --upgrade awscli pytest 35 | pre_build: 36 | commands: 37 | - TAG="$REPOSITORY_NAME.$REPOSITORY_BRANCH.$ENVIRONMENT_NAME.$(date +%Y-%m-%d.%H.%M.%S).$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | head -c 8)" 38 | - sed -i 's@CONTAINER_IMAGE@'"$REPOSITORY_URI:$TAG"'@' simple_jwt_api.yml 39 | - $(aws ecr get-login --no-include-email) 40 | - export KUBECONFIG=$HOME/.kube/config 41 | - echo `ls -l` 42 | - pip install -r requirements.txt 43 | build: 44 | commands: 45 | - docker build --tag $REPOSITORY_URI:$TAG . 46 | 47 | post_build: 48 | commands: 49 | - docker push $REPOSITORY_URI:$TAG 50 | - echo $EKS_CLUSTER_NAME 51 | - echo $EKS_KUBECTL_ROLE_ARN 52 | - aws eks update-kubeconfig --name $EKS_CLUSTER_NAME --role-arn $EKS_KUBECTL_ROLE_ARN 53 | - kubectl apply -f simple_jwt_api.yml 54 | - printf '[{"name":"simple_jwt_api","imageUri":"%s"}]' $REPOSITORY_URI:$TAG > build.json 55 | artifacts: 56 | files: build.json 57 | env: 58 | parameter-store: 59 | JWT_SECRET: JWT_SECRET 60 | -------------------------------------------------------------------------------- /ci-cd-codepipeline.cfn.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | 4 | Description: EKSWSV1 5 | 6 | 7 | Parameters: 8 | 9 | EksClusterName: 10 | Type: String 11 | Description: The name of the EKS cluster created 12 | Default: simple-jwt-api 13 | MinLength: 1 14 | MaxLength: 100 15 | ConstraintDescription: You must enter the EKS cluster name 16 | 17 | GitSourceRepo: 18 | Type: String 19 | Description: GitHub source repository - must contain a Dockerfile and buildspec.yml in the base 20 | Default: cd0157-Server-Deployment-and-Containerization 21 | MinLength: 1 22 | MaxLength: 100 23 | ConstraintDescription: You must enter a GitHub repository name 24 | 25 | GitBranch: 26 | Type: String 27 | Default: master 28 | Description: GitHub git repository branch - change triggers a new build 29 | MinLength: 1 30 | MaxLength: 100 31 | ConstraintDescription: You must enter a GitHub repository branch name 32 | 33 | GitHubToken: 34 | Type: String 35 | NoEcho: true 36 | Description: GitHub API token - see https://github.com/blog/1509-personal-api-tokens 37 | MinLength: 3 38 | MaxLength: 100 39 | ConstraintDescription: You must enter a GitHub personal access token 40 | 41 | GitHubUser: 42 | Type: String 43 | Default: SudKul 44 | Description: GitHub username or organization 45 | MinLength: 3 46 | MaxLength: 100 47 | ConstraintDescription: You must enter a GitHub username or organization 48 | 49 | CodeBuildDockerImage: 50 | Type: String 51 | Default: aws/codebuild/standard:4.0 52 | Description: AWS CodeBuild Docker optimized image 53 | MinLength: 3 54 | MaxLength: 100 55 | ConstraintDescription: You must enter a CodeBuild Docker image 56 | 57 | KubectlRoleName: 58 | Type: String 59 | Default: UdacityFlaskDeployCBKubectlRole 60 | Description: IAM role used by kubectl to interact with EKS cluster 61 | MinLength: 3 62 | MaxLength: 100 63 | ConstraintDescription: You must enter a kubectl IAM role 64 | 65 | 66 | Metadata: 67 | AWS::CloudFormation::Interface: 68 | ParameterGroups: 69 | - Label: 70 | default: GitHub 71 | Parameters: 72 | - GitHubUser 73 | - GitHubToken 74 | - GitSourceRepo 75 | - GitBranch 76 | - Label: 77 | default: CodeBuild 78 | Parameters: 79 | - CodeBuildDockerImage 80 | - Label: 81 | default: IAM 82 | Parameters: 83 | - KubectlRoleName 84 | - Label: 85 | default: EKS 86 | Parameters: 87 | - EksClusterName 88 | ParameterLabels: 89 | GitHubUser: 90 | default: Username 91 | GitHubToken: 92 | default: Access token 93 | GitSourceRepo: 94 | default: Repository 95 | GitBranch: 96 | default: Branch 97 | CodeBuildDockerImage: 98 | default: Docker image 99 | KubectlRoleName: 100 | default: kubectl IAM role 101 | EksClusterName: 102 | default: EKS cluster name 103 | 104 | 105 | Resources: 106 | 107 | EcrDockerRepository: 108 | Type: AWS::ECR::Repository 109 | DeletionPolicy: Retain 110 | 111 | CodePipelineArtifactBucket: 112 | Type: AWS::S3::Bucket 113 | DeletionPolicy: Retain 114 | 115 | CustomResourceLambda: 116 | Type: AWS::Lambda::Function 117 | Properties: 118 | Code: 119 | ZipFile: | 120 | import json 121 | import boto3 122 | import urllib3 123 | def handler(event, context): 124 | response = { 125 | 'Status': 'SUCCESS', 126 | "Reason": "See the details in CloudWatch Log Stream: " + context.log_stream_name, 127 | 'PhysicalResourceId': context.log_stream_name, 128 | 'StackId': event['StackId'], 129 | 'RequestId': event['RequestId'], 130 | 'LogicalResourceId': event['LogicalResourceId'], 131 | 'Data': {"Message": "Resource creation successful!"}, 132 | } 133 | 134 | http = urllib3.PoolManager() 135 | client = boto3.client('iam') 136 | try: 137 | if event['RequestType'] == 'Create': 138 | kubectl_role_name = event['ResourceProperties']['KubectlRoleName'] 139 | build_role_arn = event['ResourceProperties']['CodeBuildServiceRoleArn'] 140 | 141 | assume = client.get_role(RoleName = kubectl_role_name) 142 | assume_doc = assume['Role']['AssumeRolePolicyDocument'] 143 | roles = [ { 'Effect': 'Allow', 'Principal': { 'AWS': build_role_arn }, 'Action': 'sts:AssumeRole' } ] 144 | 145 | for statement in assume_doc['Statement']: 146 | if 'AWS' in statement['Principal']: 147 | if statement['Principal']['AWS'].startswith('arn:aws:iam:'): 148 | roles.append(statement) 149 | 150 | assume_doc['Statement'] = roles 151 | update_response = client.update_assume_role_policy(RoleName = kubectl_role_name, PolicyDocument = json.dumps(assume_doc)) 152 | except Exception as e: 153 | print(e) 154 | response['Status'] = 'FAILED' 155 | response["Reason"] = e 156 | response['Data'] = {"Message": "Resource creation failed"} 157 | 158 | response_body = json.dumps(response) 159 | headers = {'Content-Type': 'application/json', "content-length": str(len(response_body)) } 160 | put_response = http.request('PUT', 161 | event['ResponseURL'], 162 | body = response_body, 163 | headers=headers) 164 | return response 165 | 166 | Handler: index.handler 167 | Role: !GetAtt CustomResourceLambdaExecutionRole.Arn 168 | Runtime: python3.9 169 | Timeout: 300 170 | 171 | CustomResourceLambdaExecutionRole: 172 | Type: AWS::IAM::Role 173 | Properties: 174 | ManagedPolicyArns: 175 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 176 | - arn:aws:iam::aws:policy/service-role/AWSLambdaENIManagementAccess 177 | Path: / 178 | AssumeRolePolicyDocument: 179 | Version: 2012-10-17 180 | Statement: 181 | - Effect: Allow 182 | Action: sts:AssumeRole 183 | Principal: 184 | Service: 185 | - lambda.amazonaws.com 186 | Policies: 187 | - PolicyName: codepipeline-access 188 | PolicyDocument: 189 | Version: 2012-10-17 190 | Statement: 191 | - Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/${KubectlRoleName} 192 | Effect: Allow 193 | Action: 194 | - iam:GetRole 195 | - iam:UpdateAssumeRolePolicy 196 | 197 | KubectlAssumeRoleCustomResource: 198 | Type: Custom::CustomResource 199 | Properties: 200 | ServiceToken: !GetAtt CustomResourceLambda.Arn 201 | KubectlRoleName: !Ref KubectlRoleName 202 | CodeBuildServiceRoleArn: !GetAtt CodeBuildServiceRole.Arn 203 | DependsOn: 204 | - CustomResourceLambda 205 | - CodeBuildServiceRole 206 | 207 | CodePipelineServiceRole: 208 | Type: AWS::IAM::Role 209 | Properties: 210 | Path: / 211 | AssumeRolePolicyDocument: 212 | Version: 2012-10-17 213 | Statement: 214 | - Effect: Allow 215 | Principal: 216 | Service: codepipeline.amazonaws.com 217 | Action: sts:AssumeRole 218 | Policies: 219 | - PolicyName: codepipeline-access 220 | PolicyDocument: 221 | Version: 2012-10-17 222 | Statement: 223 | - Resource: "*" 224 | Effect: Allow 225 | Action: 226 | - codebuild:StartBuild 227 | - codebuild:BatchGetBuilds 228 | - codecommit:GetBranch 229 | - codecommit:GetCommit 230 | - codecommit:UploadArchive 231 | - codecommit:GetUploadArchiveStatus 232 | - codecommit:CancelUploadArchive 233 | - iam:PassRole 234 | - Resource: !Sub arn:aws:s3:::${CodePipelineArtifactBucket}/* 235 | Effect: Allow 236 | Action: 237 | - s3:PutObject 238 | - s3:GetObject 239 | - s3:GetObjectVersion 240 | - s3:GetBucketVersioning 241 | DependsOn: CodePipelineArtifactBucket 242 | 243 | CodeBuildServiceRole: 244 | Type: AWS::IAM::Role 245 | Properties: 246 | Path: / 247 | AssumeRolePolicyDocument: 248 | Version: 2012-10-17 249 | Statement: 250 | - Effect: Allow 251 | Principal: 252 | Service: codebuild.amazonaws.com 253 | Action: sts:AssumeRole 254 | Policies: 255 | - PolicyName: root 256 | PolicyDocument: 257 | Version: 2012-10-17 258 | Statement: 259 | - Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/${KubectlRoleName} 260 | Effect: Allow 261 | Action: 262 | - sts:AssumeRole 263 | - Resource: '*' 264 | Effect: Allow 265 | Action: 266 | - eks:Describe* 267 | - Resource: '*' 268 | Effect: Allow 269 | Action: 270 | - ssm:GetParameters 271 | - Resource: '*' 272 | Effect: Allow 273 | Action: 274 | - logs:CreateLogGroup 275 | - logs:CreateLogStream 276 | - logs:PutLogEvents 277 | - Resource: '*' 278 | Effect: Allow 279 | Action: 280 | - ecr:GetAuthorizationToken 281 | - Resource: '*' 282 | Effect: Allow 283 | Action: 284 | - ec2:CreateNetworkInterface 285 | - ec2:DescribeDhcpOptions 286 | - ec2:DescribeNetworkInterfaces 287 | - ec2:DeleteNetworkInterface 288 | - ec2:DescribeSubnets 289 | - ec2:DescribeSecurityGroups 290 | - ec2:DescribeVpcs 291 | - ec2:CreateNetworkInterfacePermission 292 | - Resource: !Sub arn:aws:s3:::${CodePipelineArtifactBucket}/* 293 | Effect: Allow 294 | Action: 295 | - s3:GetObject 296 | - s3:PutObject 297 | - s3:GetObjectVersion 298 | - Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${EcrDockerRepository} 299 | Effect: Allow 300 | Action: 301 | - ecr:GetDownloadUrlForLayer 302 | - ecr:BatchGetImage 303 | - ecr:BatchCheckLayerAvailability 304 | - ecr:PutImage 305 | - ecr:InitiateLayerUpload 306 | - ecr:UploadLayerPart 307 | - ecr:CompleteLayerUpload 308 | 309 | CodeBuildProject: 310 | Type: AWS::CodeBuild::Project 311 | Properties: 312 | Artifacts: 313 | Type: CODEPIPELINE 314 | Source: 315 | Type: CODEPIPELINE 316 | Environment: 317 | ComputeType: BUILD_GENERAL1_SMALL 318 | Type: LINUX_CONTAINER 319 | Image: !Ref CodeBuildDockerImage 320 | EnvironmentVariables: 321 | - Name: REPOSITORY_URI 322 | Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrDockerRepository} 323 | - Name: REPOSITORY_NAME 324 | Value: !Ref GitSourceRepo 325 | - Name: REPOSITORY_BRANCH 326 | Value: !Ref GitBranch 327 | - Name: EKS_CLUSTER_NAME 328 | Value: !Ref EksClusterName 329 | - Name: EKS_KUBECTL_ROLE_ARN 330 | Value: !Sub arn:aws:iam::${AWS::AccountId}:role/${KubectlRoleName} 331 | PrivilegedMode: true 332 | Name: !Ref AWS::StackName 333 | ServiceRole: !GetAtt CodeBuildServiceRole.Arn 334 | DependsOn: KubectlAssumeRoleCustomResource 335 | 336 | CodePipelineGitHub: 337 | Type: AWS::CodePipeline::Pipeline 338 | Properties: 339 | RoleArn: !GetAtt CodePipelineServiceRole.Arn 340 | ArtifactStore: 341 | Type: S3 342 | Location: !Ref CodePipelineArtifactBucket 343 | Stages: 344 | - Name: Source 345 | Actions: 346 | - Name: App 347 | ActionTypeId: 348 | Category: Source 349 | Owner: ThirdParty 350 | Version: 1 351 | Provider: GitHub 352 | Configuration: 353 | Owner: !Ref GitHubUser 354 | Repo: !Ref GitSourceRepo 355 | Branch: !Ref GitBranch 356 | OAuthToken: !Ref GitHubToken 357 | OutputArtifacts: 358 | - Name: App 359 | RunOrder: 1 360 | - Name: Build 361 | Actions: 362 | - Name: Build 363 | ActionTypeId: 364 | Category: Build 365 | Owner: AWS 366 | Version: 1 367 | Provider: CodeBuild 368 | Configuration: 369 | ProjectName: !Ref CodeBuildProject 370 | InputArtifacts: 371 | - Name: App 372 | OutputArtifacts: 373 | - Name: BuildOutput 374 | RunOrder: 1 375 | DependsOn: CodeBuildProject 376 | -------------------------------------------------------------------------------- /examples/Cloudformation/my_parameters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "myVPC", 4 | "ParameterValue": "vpc-098defbc811aad87c" 5 | }, 6 | { 7 | "ParameterKey": "PublicSubnet", 8 | "ParameterValue": "subnet-031d7c02210432b1c" 9 | }, 10 | { 11 | "ParameterKey": "AMItoUse", 12 | "ParameterValue": "ami-0fa49cc9dc8d62c84" 13 | } 14 | ] -------------------------------------------------------------------------------- /examples/Cloudformation/template_EC2.yml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | myVPC: 3 | Description: VPC used to deploy our resources below 4 | Type: AWS::EC2::VPC::Id 5 | PublicSubnet: 6 | Description: Subnet to be used for our Web Server 7 | Type: AWS::EC2::Subnet::Id 8 | AMItoUse: 9 | Description: AMI to use for our base image 10 | Type: String 11 | Resources: 12 | myWebAccessSecurityGroup: 13 | Type: AWS::EC2::SecurityGroup 14 | Properties: 15 | GroupDescription: Allow http to our test host 16 | VpcId: 17 | Ref: myVPC 18 | SecurityGroupIngress: 19 | - IpProtocol: tcp 20 | FromPort: 80 21 | ToPort: 80 22 | CidrIp: 0.0.0.0/0 23 | SecurityGroupEgress: 24 | - IpProtocol: -1 25 | FromPort: -1 26 | ToPort: -1 27 | CidrIp: 0.0.0.0/0 28 | myWebServerInstance: 29 | Type: AWS::EC2::Instance 30 | Properties: 31 | InstanceType: t3.micro 32 | ImageId: !Ref AMItoUse 33 | NetworkInterfaces: 34 | - AssociatePublicIpAddress: "true" 35 | DeviceIndex: "0" 36 | GroupSet: 37 | - Ref: "myWebAccessSecurityGroup" 38 | SubnetId: 39 | Ref: "PublicSubnet" 40 | UserData: 41 | Fn::Base64: !Sub | 42 | #!/bin/bash 43 | sudo yum update -y 44 | sudo yum install -y httpd 45 | sudo systemctl start httpd 46 | sudo systemctl enable httpd -------------------------------------------------------------------------------- /examples/Cloudformation/template_VPN.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Udacity - This template deploys a Virtual Private Network 3 | Resources: 4 | UdacityVPC: 5 | Type: 'AWS::EC2::VPC' 6 | Properties: 7 | CidrBlock: 10.0.0.0/16 8 | EnableDnsHostnames: 'true' 9 | Tags: 10 | - Key: name 11 | Value: myfirsttestvpc -------------------------------------------------------------------------------- /examples/Create_Pipeline/buildspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 0.2 3 | 4 | 5 | phases: 6 | build: 7 | commands: 8 | - echo Hello from Code Build! 9 | 10 | -------------------------------------------------------------------------------- /examples/Create_Pipeline/buildspec.yml.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0157-Server-Deployment-and-Containerization/b9f5782f42fb7d6ea2aa18b8a5f5ee54337e64bc/examples/Create_Pipeline/buildspec.yml.zip -------------------------------------------------------------------------------- /examples/Create_Pipeline/simple-pipeline-build.yml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | SourceObjectKey: 3 | Description: 'S3 source artifact' 4 | Type: String 5 | Default: buildspec.yml.zip 6 | CodeBuildDockerImage: 7 | Type: String 8 | Default: aws/codebuild/standard:4.0 9 | Description: AWS CodeBuild Docker optimized image 10 | MinLength: 3 11 | MaxLength: 100 12 | ConstraintDescription: You must enter a CodeBuild Docker image 13 | 14 | Resources: 15 | SourceBucket: 16 | Type: AWS::S3::Bucket 17 | Properties: 18 | VersioningConfiguration: 19 | Status: Enabled 20 | CodePipelineArtifactStoreBucket: 21 | Type: AWS::S3::Bucket 22 | CodePipelineArtifactStoreBucketPolicy: 23 | Type: AWS::S3::BucketPolicy 24 | Properties: 25 | Bucket: !Ref CodePipelineArtifactStoreBucket 26 | PolicyDocument: 27 | Version: 2012-10-17 28 | Statement: 29 | - 30 | Sid: DenyUnEncryptedObjectUploads 31 | Effect: Deny 32 | Principal: '*' 33 | Action: s3:PutObject 34 | Resource: !Join [ '', [ !GetAtt CodePipelineArtifactStoreBucket.Arn, '/*' ] ] 35 | Condition: 36 | StringNotEquals: 37 | s3:x-amz-server-side-encryption: aws:kms 38 | - 39 | Sid: DenyInsecureConnections 40 | Effect: Deny 41 | Principal: '*' 42 | Action: s3:* 43 | Resource: !Join [ '', [ !GetAtt CodePipelineArtifactStoreBucket.Arn, '/*' ] ] 44 | Condition: 45 | Bool: 46 | aws:SecureTransport: false 47 | CodePipelineServiceRole: 48 | Type: AWS::IAM::Role 49 | Properties: 50 | AssumeRolePolicyDocument: 51 | Version: 2012-10-17 52 | Statement: 53 | - 54 | Effect: Allow 55 | Principal: 56 | Service: 57 | - codepipeline.amazonaws.com 58 | Action: sts:AssumeRole 59 | Path: / 60 | Policies: 61 | - 62 | PolicyName: AWS-CodePipeline-Service-3 63 | PolicyDocument: 64 | Version: 2012-10-17 65 | Statement: 66 | - 67 | Effect: Allow 68 | Action: 69 | - codecommit:CancelUploadArchive 70 | - codecommit:GetBranch 71 | - codecommit:GetCommit 72 | - codecommit:GetUploadArchiveStatus 73 | - codecommit:UploadArchive 74 | Resource: '*' 75 | - 76 | Effect: Allow 77 | Action: 78 | - codedeploy:CreateDeployment 79 | - codedeploy:GetApplicationRevision 80 | - codedeploy:GetDeployment 81 | - codedeploy:GetDeploymentConfig 82 | - codedeploy:RegisterApplicationRevision 83 | Resource: '*' 84 | - 85 | Effect: Allow 86 | Action: 87 | - codebuild:BatchGetBuilds 88 | - codebuild:StartBuild 89 | Resource: '*' 90 | - 91 | Effect: Allow 92 | Action: 93 | - devicefarm:ListProjects 94 | - devicefarm:ListDevicePools 95 | - devicefarm:GetRun 96 | - devicefarm:GetUpload 97 | - devicefarm:CreateUpload 98 | - devicefarm:ScheduleRun 99 | Resource: '*' 100 | - 101 | Effect: Allow 102 | Action: 103 | - lambda:InvokeFunction 104 | - lambda:ListFunctions 105 | Resource: '*' 106 | - 107 | Effect: Allow 108 | Action: 109 | - iam:PassRole 110 | Resource: '*' 111 | - 112 | Effect: Allow 113 | Action: 114 | - elasticbeanstalk:* 115 | - ec2:* 116 | - elasticloadbalancing:* 117 | - autoscaling:* 118 | - cloudwatch:* 119 | - s3:* 120 | - sns:* 121 | - cloudformation:* 122 | - rds:* 123 | - sqs:* 124 | - ecs:* 125 | Resource: '*' 126 | AppPipeline: 127 | Type: AWS::CodePipeline::Pipeline 128 | Properties: 129 | Name: s3-events-pipeline 130 | RoleArn: 131 | !GetAtt CodePipelineServiceRole.Arn 132 | Stages: 133 | - 134 | Name: Source 135 | Actions: 136 | - 137 | Name: SourceAction 138 | ActionTypeId: 139 | Category: Source 140 | Owner: AWS 141 | Version: 1 142 | Provider: S3 143 | OutputArtifacts: 144 | - Name: SourceOutput 145 | Configuration: 146 | S3Bucket: !Ref SourceBucket 147 | S3ObjectKey: !Ref SourceObjectKey 148 | PollForSourceChanges: false 149 | RunOrder: 1 150 | - Name: Build 151 | Actions: 152 | - Name: Build 153 | ActionTypeId: 154 | Category: Build 155 | Owner: AWS 156 | Version: 1 157 | Provider: CodeBuild 158 | Configuration: 159 | ProjectName: !Ref CodeBuildProject 160 | InputArtifacts: 161 | - Name: SourceOutput 162 | OutputArtifacts: 163 | - Name: BuildOutput 164 | RunOrder: 1 165 | ArtifactStore: 166 | Type: S3 167 | Location: !Ref CodePipelineArtifactStoreBucket 168 | CodeBuildProject: 169 | Type: AWS::CodeBuild::Project 170 | Properties: 171 | Artifacts: 172 | Type: CODEPIPELINE 173 | Source: 174 | Type: CODEPIPELINE 175 | Environment: 176 | ComputeType: BUILD_GENERAL1_SMALL 177 | Type: LINUX_CONTAINER 178 | Image: !Ref CodeBuildDockerImage 179 | PrivilegedMode: true 180 | Name: !Ref AWS::StackName 181 | ServiceRole: !GetAtt CodeBuildServiceRole.Arn 182 | CodeBuildServiceRole: 183 | Type: AWS::IAM::Role 184 | Properties: 185 | Path: / 186 | AssumeRolePolicyDocument: 187 | Version: 2012-10-17 188 | Statement: 189 | - Effect: Allow 190 | Principal: 191 | Service: codebuild.amazonaws.com 192 | Action: sts:AssumeRole 193 | Policies: 194 | - PolicyName: root 195 | PolicyDocument: 196 | Version: 2012-10-17 197 | Statement: 198 | - Resource: '*' 199 | Effect: Allow 200 | Action: 201 | - ssm:GetParameters 202 | - Resource: '*' 203 | Effect: Allow 204 | Action: 205 | - logs:CreateLogGroup 206 | - logs:CreateLogStream 207 | - logs:PutLogEvents 208 | - Resource: '*' 209 | Effect: Allow 210 | Action: 211 | - ecr:GetAuthorizationToken 212 | - Resource: '*' 213 | Effect: Allow 214 | Action: 215 | - ec2:CreateNetworkInterface 216 | - ec2:DescribeDhcpOptions 217 | - ec2:DescribeNetworkInterfaces 218 | - ec2:DeleteNetworkInterface 219 | - ec2:DescribeSubnets 220 | - ec2:DescribeSecurityGroups 221 | - ec2:DescribeVpcs 222 | - ec2:CreateNetworkInterfacePermission 223 | - Resource: !Sub arn:aws:s3:::${CodePipelineArtifactStoreBucket}/* 224 | Effect: Allow 225 | Action: 226 | - s3:GetObject 227 | - s3:PutObject 228 | - s3:GetObjectVersion 229 | -------------------------------------------------------------------------------- /examples/Deploy_Flask_App/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.2-slim 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | RUN pip install --upgrade pip 7 | RUN pip install flask 8 | 9 | 10 | ENTRYPOINT ["python", "app.py"] 11 | 12 | -------------------------------------------------------------------------------- /examples/Deploy_Flask_App/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | APP = Flask(__name__) 3 | 4 | 5 | @APP.route('/') 6 | def hello_world(): 7 | return 'Hello, World from Flask!\n' 8 | 9 | 10 | 11 | if __name__ == '__main__': 12 | APP.run(host='0.0.0.0', port=8080, debug=True) 13 | -------------------------------------------------------------------------------- /examples/Deploy_Flask_App/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: simple-flask-deployment 5 | labels: 6 | app: simple-flask 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: simple-flask 12 | strategy: 13 | type: RollingUpdate 14 | rollingUpdate: 15 | maxUnavailable: 2 16 | maxSurge: 2 17 | template: 18 | metadata: 19 | labels: 20 | app: simple-flask 21 | spec: 22 | containers: 23 | - name: simple-flask 24 | # image: IMAGE_TAG 25 | image: sudkul/simple-flask 26 | securityContext: 27 | privileged: false 28 | readOnlyRootFilesystem: false 29 | allowPrivilegeEscalation: false 30 | ports: 31 | - containerPort: 8080 32 | -------------------------------------------------------------------------------- /examples/flask/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.2-slim 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | RUN pip install --upgrade pip 7 | RUN pip install flask 8 | 9 | 10 | ENTRYPOINT ["python", "app.py"] 11 | 12 | -------------------------------------------------------------------------------- /examples/flask/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | APP = Flask(__name__) 3 | 4 | 5 | @APP.route('/') 6 | def hello_world(): 7 | return 'Hello, World from Flask!\n' 8 | 9 | 10 | 11 | if __name__ == '__main__': 12 | APP.run(host='0.0.0.0', port=8080, debug=True) 13 | -------------------------------------------------------------------------------- /examples/hello/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jessie-slim 2 | 3 | ENTRYPOINT ["echo", "hello world"] 4 | -------------------------------------------------------------------------------- /iam-role-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement":[{ 4 | "Effect": "Allow", 5 | "Action": ["eks:Describe*", "ssm:GetParameters"], 6 | "Resource":"*" 7 | }] 8 | } 9 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | A simple app to create a JWT token. 4 | """ 5 | import os 6 | import logging 7 | import datetime 8 | import functools 9 | import jwt 10 | 11 | # pylint: disable=import-error 12 | from flask import Flask, jsonify, request, abort 13 | 14 | 15 | JWT_SECRET = os.environ.get('JWT_SECRET', 'abc123abc1234') 16 | LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') 17 | 18 | 19 | def _logger(): 20 | ''' 21 | Setup logger format, level, and handler. 22 | 23 | RETURNS: log object 24 | ''' 25 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 26 | 27 | log = logging.getLogger(__name__) 28 | log.setLevel(LOG_LEVEL) 29 | 30 | stream_handler = logging.StreamHandler() 31 | stream_handler.setFormatter(formatter) 32 | 33 | log.addHandler(stream_handler) 34 | return log 35 | 36 | 37 | LOG = _logger() 38 | LOG.debug("Starting with log level: %s" % LOG_LEVEL ) 39 | APP = Flask(__name__) 40 | 41 | def require_jwt(function): 42 | """ 43 | Decorator to check valid jwt is present. 44 | """ 45 | @functools.wraps(function) 46 | def decorated_function(*args, **kws): 47 | if not 'Authorization' in request.headers: 48 | abort(401) 49 | data = request.headers['Authorization'] 50 | token = str.replace(str(data), 'Bearer ', '') 51 | try: 52 | jwt.decode(token, JWT_SECRET, algorithms=['HS256']) 53 | except: # pylint: disable=bare-except 54 | abort(401) 55 | 56 | return function(*args, **kws) 57 | return decorated_function 58 | 59 | 60 | @APP.route('/', methods=['POST', 'GET']) 61 | def health(): 62 | return jsonify("Healthy") 63 | 64 | 65 | @APP.route('/auth', methods=['POST']) 66 | def auth(): 67 | """ 68 | Create JWT token based on email. 69 | """ 70 | request_data = request.get_json() 71 | email = request_data.get('email') 72 | password = request_data.get('password') 73 | if not email: 74 | LOG.error("No email provided") 75 | return jsonify({"message": "Missing parameter: email"}, 400) 76 | if not password: 77 | LOG.error("No password provided") 78 | return jsonify({"message": "Missing parameter: password"}, 400) 79 | body = {'email': email, 'password': password} 80 | 81 | user_data = body 82 | 83 | return jsonify(token=_get_jwt(user_data).decode('utf-8')) 84 | 85 | 86 | @APP.route('/contents', methods=['GET']) 87 | def decode_jwt(): 88 | """ 89 | Check user token and return non-secret data 90 | """ 91 | if not 'Authorization' in request.headers: 92 | abort(401) 93 | data = request.headers['Authorization'] 94 | token = str.replace(str(data), 'Bearer ', '') 95 | try: 96 | data = jwt.decode(token, JWT_SECRET, algorithms=['HS256']) 97 | except: # pylint: disable=bare-except 98 | abort(401) 99 | 100 | 101 | response = {'email': data['email'], 102 | 'exp': data['exp'], 103 | 'nbf': data['nbf'] } 104 | return jsonify(**response) 105 | 106 | 107 | def _get_jwt(user_data): 108 | exp_time = datetime.datetime.utcnow() + datetime.timedelta(weeks=2) 109 | payload = {'exp': exp_time, 110 | 'nbf': datetime.datetime.utcnow(), 111 | 'email': user_data['email']} 112 | return jwt.encode(payload, JWT_SECRET, algorithm='HS256') 113 | 114 | if __name__ == '__main__': 115 | APP.run(host='127.0.0.1', port=8080, debug=True) 116 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyjwt==1.7.1 2 | flask==1.1.2 3 | Jinja2<3.0.0 4 | MarkupSafe<2.0.0 5 | ruamel.yaml==0.16.5 6 | itsdangerous==2.0.1 7 | werkzeug==2.0.3 8 | gunicorn==20.0.4 9 | pytest==6.2.2 -------------------------------------------------------------------------------- /simple_jwt_api.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: simple-jwt-api 5 | spec: 6 | type: LoadBalancer 7 | ports: 8 | - port: 80 9 | targetPort: 8080 10 | selector: 11 | app: simple-jwt-api 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: simple-jwt-api 17 | spec: 18 | replicas: 3 19 | strategy: 20 | type: RollingUpdate 21 | rollingUpdate: 22 | maxUnavailable: 2 23 | maxSurge: 2 24 | selector: 25 | matchLabels: 26 | app: simple-jwt-api 27 | template: 28 | metadata: 29 | labels: 30 | app: simple-jwt-api 31 | spec: 32 | containers: 33 | - name: simple-jwt-api 34 | image: CONTAINER_IMAGE 35 | securityContext: 36 | privileged: false 37 | readOnlyRootFilesystem: false 38 | allowPrivilegeEscalation: false 39 | ports: 40 | - containerPort: 8080 41 | -------------------------------------------------------------------------------- /test_main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Tests for jwt flask app. 3 | ''' 4 | import os 5 | import json 6 | import pytest 7 | 8 | import main 9 | 10 | SECRET = 'TestSecret' 11 | TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjEzMDY3OTAsIm5iZiI6MTU2MDA5NzE5MCwiZW1haWwiOiJ3b2xmQHRoZWRvb3IuY29tIn0.IpM4VMnqIgOoQeJxUbLT-cRcAjK41jronkVrqRLFmmk' 12 | EMAIL = 'wolf@thedoor.com' 13 | PASSWORD = 'huff-puff' 14 | 15 | @pytest.fixture 16 | def client(): 17 | os.environ['JWT_SECRET'] = SECRET 18 | main.APP.config['TESTING'] = True 19 | client = main.APP.test_client() 20 | 21 | yield client 22 | 23 | 24 | 25 | def test_health(client): 26 | response = client.get('/') 27 | assert response.status_code == 200 28 | assert response.json == 'Healthy' 29 | 30 | 31 | def test_auth(client): 32 | body = {'email': EMAIL, 33 | 'password': PASSWORD} 34 | response = client.post('/auth', 35 | data=json.dumps(body), 36 | content_type='application/json') 37 | 38 | assert response.status_code == 200 39 | token = response.json['token'] 40 | assert token is not None 41 | -------------------------------------------------------------------------------- /trust.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWS": "arn:aws:iam::519002666132:root" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } 13 | --------------------------------------------------------------------------------