├── .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 |
--------------------------------------------------------------------------------