├── requirements.txt
├── .streamlit
└── config.toml
├── .gitignore
├── architecture-cicd.png
├── architecture-development.png
├── app.py
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── CONTRIBUTING.md
├── cfn_stack
├── development
│ ├── infrastructure.yaml
│ └── development.yaml
└── pipeline
│ ├── infrastructure.yaml
│ ├── deploy.yaml
│ └── codepipeline.yaml
└── README.md
/requirements.txt:
--------------------------------------------------------------------------------
1 | streamlit
2 | boto3
--------------------------------------------------------------------------------
/.streamlit/config.toml:
--------------------------------------------------------------------------------
1 | [browser]
2 | gatherUsageStats = false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | secrets.toml
2 | .venv
3 | __pycache__
4 | .DS_Store
5 | *.zip
6 |
--------------------------------------------------------------------------------
/architecture-cicd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/streamlit-deploy/HEAD/architecture-cicd.png
--------------------------------------------------------------------------------
/architecture-development.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/streamlit-deploy/HEAD/architecture-development.png
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 |
3 | st.set_page_config(
4 | page_title="AWS",
5 | page_icon="👋",
6 | layout="wide"
7 | )
8 |
9 | st.header("Hi, welcome!")
10 | st.subheader("This is your Amazon Web Services (AWS) Streamlit deployment!", divider="rainbow")
11 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM public.ecr.aws/docker/library/python:3.12.2-slim
2 |
3 | WORKDIR /frontend
4 |
5 | RUN apt-get update && apt-get install -y \
6 | build-essential \
7 | curl \
8 | software-properties-common \
9 | git \
10 | && rm -rf /var/lib/apt/lists/*
11 |
12 | COPY . .
13 |
14 | RUN pip3 install -r requirements.txt
15 |
16 | EXPOSE 80
17 |
18 | HEALTHCHECK CMD curl --fail http://localhost:80/_stcore/health
19 |
20 | ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=80"]
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT No Attribution
2 |
3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 |
18 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/cfn_stack/development/infrastructure.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: 2010-09-09
2 | Description: This CloudFormation template creates a Virtual Private Cloud (VPC) with two public subnets and two private subnets across two Availability Zones (AZs). It also sets up an ECS Cluster, Internet Gateway, NAT Gateways, Route Tables, and other necessary resources for a secure and highly available network infrastructure.
3 | Metadata:
4 | 'AWS::CloudFormation::Interface':
5 | ParameterGroups:
6 | - Label:
7 | default: 'VPCConfig'
8 | Parameters:
9 | - Vpccidr
10 | - PublicSubnetAcidr
11 | - PublicSubnetBcidr
12 | - PrivateSubnetAcidr
13 | - PrivateSubnetBcidr
14 |
15 | Parameters:
16 | Vpccidr:
17 | Description: Please enter the IP range (CIDR notation) for the VPC
18 | Type: String
19 | Default: 10.0.0.0/16
20 |
21 | PublicSubnetAcidr:
22 | Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
23 | Type: String
24 | Default: 10.0.0.0/24
25 |
26 | PublicSubnetBcidr:
27 | Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone
28 | Type: String
29 | Default: 10.0.1.0/24
30 |
31 | PrivateSubnetAcidr:
32 | Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone
33 | Type: String
34 | Default: 10.0.2.0/24
35 |
36 | PrivateSubnetBcidr:
37 | Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone
38 | Type: String
39 | Default: 10.0.3.0/24
40 |
41 | Resources:
42 |
43 | ################
44 | ##### VPC #####
45 | ##############
46 |
47 | VPC:
48 | Type: AWS::EC2::VPC
49 | Properties:
50 | CidrBlock: !Ref Vpccidr
51 | EnableDnsSupport: true
52 | EnableDnsHostnames: true
53 | Tags:
54 | - Key: Name
55 | Value: 'VPC'
56 |
57 | VPCLogGroup:
58 | DeletionPolicy: Retain
59 | UpdateReplacePolicy: Retain
60 | Type: 'AWS::Logs::LogGroup'
61 | Properties:
62 | RetentionInDays: 7
63 |
64 | VPCLogRole:
65 | Type: AWS::IAM::Role
66 | Properties:
67 | AssumeRolePolicyDocument:
68 | Version: "2012-10-17"
69 | Statement:
70 | - Effect: Allow
71 | Principal:
72 | Service: vpc-flow-logs.amazonaws.com
73 | Action: sts:AssumeRole
74 | Policies:
75 | - PolicyName: "LogsPolicy"
76 | PolicyDocument:
77 | Version: '2012-10-17'
78 | Statement:
79 | - Effect: 'Allow'
80 | Action:
81 | - 'logs:CreateLogGroup'
82 | - 'logs:CreateLogStream'
83 | - 'logs:PutLogEvents'
84 | - 'logs:PutRetentionPolicy'
85 | Resource: '*'
86 |
87 | VPCFlowLog:
88 | Type: "AWS::EC2::FlowLog"
89 | Properties:
90 | ResourceId: !Ref VPC
91 | ResourceType: VPC
92 | TrafficType: ALL
93 | LogGroupName: !Ref VPCLogGroup
94 | DeliverLogsPermissionArn: !GetAtt VPCLogRole.Arn
95 |
96 | ##########################
97 | ##### Public Subnet #####
98 | ########################
99 |
100 | InternetGateway:
101 | Type: AWS::EC2::InternetGateway
102 | Properties:
103 | Tags:
104 | - Key: Name
105 | Value: InternetGateway
106 |
107 | InternetGatewayAttachment:
108 | Type: AWS::EC2::VPCGatewayAttachment
109 | Properties:
110 | InternetGatewayId: !Ref InternetGateway
111 | VpcId: !Ref VPC
112 |
113 | # Create a Subnet
114 | PublicSubnetA:
115 | Type: AWS::EC2::Subnet
116 | Properties:
117 | CidrBlock: !Ref PublicSubnetAcidr
118 | VpcId: !Ref VPC
119 | AvailabilityZone: !Select
120 | - 0
121 | - Fn::GetAZs: !Ref 'AWS::Region'
122 | Tags:
123 | - Key: Name
124 | Value: PublicSubnetA
125 |
126 | PublicSubnetB:
127 | Type: AWS::EC2::Subnet
128 | Properties:
129 | CidrBlock: !Ref PublicSubnetBcidr
130 | VpcId: !Ref VPC
131 | AvailabilityZone: !Select
132 | - 1
133 | - Fn::GetAZs: !Ref 'AWS::Region'
134 | Tags:
135 | - Key: Name
136 | Value: PublicSubnetB
137 |
138 | # Public Route Table
139 | PublicRouteTable:
140 | Type: AWS::EC2::RouteTable
141 | Properties:
142 | VpcId: !Ref VPC
143 | Tags:
144 | - Key: Name
145 | Value: PublicRouteTable
146 |
147 | DefaultPublicRoute:
148 | Type: AWS::EC2::Route
149 | DependsOn: InternetGatewayAttachment
150 | Properties:
151 | RouteTableId: !Ref PublicRouteTable
152 | DestinationCidrBlock: 0.0.0.0/0
153 | GatewayId: !Ref InternetGateway
154 |
155 | PublicSubnetARouteTableAssociation:
156 | Type: AWS::EC2::SubnetRouteTableAssociation
157 | Properties:
158 | RouteTableId: !Ref PublicRouteTable
159 | SubnetId: !Ref PublicSubnetA
160 |
161 | PublicSubnetBRouteTableAssociation:
162 | Type: AWS::EC2::SubnetRouteTableAssociation
163 | Properties:
164 | RouteTableId: !Ref PublicRouteTable
165 | SubnetId: !Ref PublicSubnetB
166 |
167 | ##########################
168 | ##### Private Subnet #####
169 | ########################
170 |
171 | PrivateSubnetA:
172 | Type: AWS::EC2::Subnet
173 | Properties:
174 | CidrBlock: !Ref PrivateSubnetAcidr
175 | VpcId: !Ref VPC
176 | AvailabilityZone: !Select
177 | - 0
178 | - Fn::GetAZs: !Ref 'AWS::Region'
179 | Tags:
180 | - Key: Name
181 | Value: PrivateSubnetA
182 |
183 | PrivateSubnetB:
184 | Type: AWS::EC2::Subnet
185 | Properties:
186 | CidrBlock: !Ref PrivateSubnetBcidr
187 | VpcId: !Ref VPC
188 | AvailabilityZone: !Select
189 | - 1
190 | - Fn::GetAZs: !Ref 'AWS::Region'
191 | Tags:
192 | - Key: Name
193 | Value: PrivateSubnetB
194 |
195 | # NAT Gateway
196 | NatGatewayAEIP:
197 | Type: AWS::EC2::EIP
198 | DependsOn: InternetGatewayAttachment
199 | Properties:
200 | Domain: vpc
201 |
202 | NatGatewayBEIP:
203 | Type: AWS::EC2::EIP
204 | DependsOn: InternetGatewayAttachment
205 | Properties:
206 | Domain: vpc
207 |
208 | NatGatewayA:
209 | Type: AWS::EC2::NatGateway
210 | Properties:
211 | AllocationId: !GetAtt NatGatewayAEIP.AllocationId
212 | SubnetId: !Ref PublicSubnetA
213 |
214 | NatGatewayB:
215 | Type: AWS::EC2::NatGateway
216 | Properties:
217 | AllocationId: !GetAtt NatGatewayBEIP.AllocationId
218 | SubnetId: !Ref PublicSubnetB
219 |
220 | PrivateRouteTableA:
221 | Type: AWS::EC2::RouteTable
222 | Properties:
223 | VpcId: !Ref VPC
224 | Tags:
225 | - Key: Name
226 | Value: PrivateRouteTableA
227 |
228 | DefaultPrivateRouteA:
229 | Type: AWS::EC2::Route
230 | Properties:
231 | RouteTableId: !Ref PrivateRouteTableA
232 | DestinationCidrBlock: 0.0.0.0/0
233 | NatGatewayId: !Ref NatGatewayA
234 |
235 | PrivateSubnetARouteTableAssociation:
236 | Type: AWS::EC2::SubnetRouteTableAssociation
237 | Properties:
238 | RouteTableId: !Ref PrivateRouteTableA
239 | SubnetId: !Ref PrivateSubnetA
240 |
241 | PrivateRouteTableB:
242 | Type: AWS::EC2::RouteTable
243 | Properties:
244 | VpcId: !Ref VPC
245 | Tags:
246 | - Key: Name
247 | Value: PrivateRouteTableB
248 |
249 | DefaultPrivateRouteB:
250 | Type: AWS::EC2::Route
251 | Properties:
252 | RouteTableId: !Ref PrivateRouteTableB
253 | DestinationCidrBlock: 0.0.0.0/0
254 | NatGatewayId: !Ref NatGatewayB
255 |
256 | PrivateSubnetBRouteTableAssociation:
257 | Type: AWS::EC2::SubnetRouteTableAssociation
258 | Properties:
259 | RouteTableId: !Ref PrivateRouteTableB
260 | SubnetId: !Ref PrivateSubnetB
261 |
262 | Outputs:
263 | VPC:
264 | Description: "VPC"
265 | Value: !Ref VPC
266 | Export:
267 | Name: Basic-VPC
268 |
269 | PublicSubnetA:
270 | Description: "PublicSubnetA"
271 | Value: !Ref PublicSubnetA
272 | Export:
273 | Name: Basic-PublicSubnetA
274 |
275 | PublicSubnetB:
276 | Description: "PublicSubnetB"
277 | Value: !Ref PublicSubnetB
278 | Export:
279 | Name: Basic-PublicSubnetB
280 |
281 | PrivateSubnetA:
282 | Description: "PrivateSubnetA"
283 | Value: !Ref PrivateSubnetA
284 | Export:
285 | Name: Basic-PrivateSubnetA
286 |
287 | PrivateSubnetB:
288 | Description: "PrivateSubnetB"
289 | Value: !Ref PrivateSubnetB
290 | Export:
291 | Name: Basic-PrivateSubnetB
--------------------------------------------------------------------------------
/cfn_stack/pipeline/infrastructure.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: 2010-09-09
2 | Description: This CloudFormation template creates a Virtual Private Cloud (VPC) with two public subnets and two private subnets across two Availability Zones (AZs). It also sets up an ECS Cluster, Internet Gateway, NAT Gateways, Route Tables, and other necessary resources for a secure and highly available network infrastructure.
3 | Metadata:
4 | 'AWS::CloudFormation::Interface':
5 | ParameterGroups:
6 | - Label:
7 | default: 'VPCConfig'
8 | Parameters:
9 | - Vpccidr
10 | - PublicSubnetAcidr
11 | - PublicSubnetBcidr
12 | - PrivateSubnetAcidr
13 | - PrivateSubnetBcidr
14 |
15 | Parameters:
16 | Vpccidr:
17 | Description: Please enter the IP range (CIDR notation) for the VPC
18 | Type: String
19 | Default: 10.0.0.0/16
20 |
21 | PublicSubnetAcidr:
22 | Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
23 | Type: String
24 | Default: 10.0.0.0/24
25 |
26 | PublicSubnetBcidr:
27 | Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone
28 | Type: String
29 | Default: 10.0.1.0/24
30 |
31 | PrivateSubnetAcidr:
32 | Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone
33 | Type: String
34 | Default: 10.0.2.0/24
35 |
36 | PrivateSubnetBcidr:
37 | Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone
38 | Type: String
39 | Default: 10.0.3.0/24
40 |
41 | Resources:
42 | ##############################
43 | ##### Streamlit Cluster #####
44 | ############################
45 |
46 | StreamlitCluster:
47 | Type: AWS::ECS::Cluster
48 | Properties:
49 | ClusterSettings:
50 | - Name: containerInsights
51 | Value: enabled
52 |
53 | ################
54 | ##### VPC #####
55 | ##############
56 |
57 | VPC:
58 | Type: AWS::EC2::VPC
59 | Properties:
60 | CidrBlock: !Ref Vpccidr
61 | EnableDnsSupport: true
62 | EnableDnsHostnames: true
63 | Tags:
64 | - Key: Name
65 | Value: 'VPC'
66 |
67 | VPCLogGroup:
68 | DeletionPolicy: Retain
69 | UpdateReplacePolicy: Retain
70 | Type: 'AWS::Logs::LogGroup'
71 | Properties:
72 | RetentionInDays: 7
73 |
74 | VPCLogRole:
75 | Type: AWS::IAM::Role
76 | Properties:
77 | AssumeRolePolicyDocument:
78 | Version: "2012-10-17"
79 | Statement:
80 | - Effect: Allow
81 | Principal:
82 | Service: vpc-flow-logs.amazonaws.com
83 | Action: sts:AssumeRole
84 | Policies:
85 | - PolicyName: "LogsPolicy"
86 | PolicyDocument:
87 | Version: '2012-10-17'
88 | Statement:
89 | - Effect: 'Allow'
90 | Action:
91 | - 'logs:CreateLogGroup'
92 | - 'logs:CreateLogStream'
93 | - 'logs:PutLogEvents'
94 | - 'logs:PutRetentionPolicy'
95 | Resource: '*'
96 |
97 | VPCFlowLog:
98 | Type: "AWS::EC2::FlowLog"
99 | Properties:
100 | ResourceId: !Ref VPC
101 | ResourceType: VPC
102 | TrafficType: ALL
103 | LogGroupName: !Ref VPCLogGroup
104 | DeliverLogsPermissionArn: !GetAtt VPCLogRole.Arn
105 |
106 | ##########################
107 | ##### Public Subnet #####
108 | ########################
109 |
110 | InternetGateway:
111 | Type: AWS::EC2::InternetGateway
112 | Properties:
113 | Tags:
114 | - Key: Name
115 | Value: InternetGateway
116 |
117 | InternetGatewayAttachment:
118 | Type: AWS::EC2::VPCGatewayAttachment
119 | Properties:
120 | InternetGatewayId: !Ref InternetGateway
121 | VpcId: !Ref VPC
122 |
123 | # Create a Subnet
124 | PublicSubnetA:
125 | Type: AWS::EC2::Subnet
126 | Properties:
127 | CidrBlock: !Ref PublicSubnetAcidr
128 | VpcId: !Ref VPC
129 | AvailabilityZone: !Select
130 | - 0
131 | - Fn::GetAZs: !Ref 'AWS::Region'
132 | Tags:
133 | - Key: Name
134 | Value: PublicSubnetA
135 |
136 | PublicSubnetB:
137 | Type: AWS::EC2::Subnet
138 | Properties:
139 | CidrBlock: !Ref PublicSubnetBcidr
140 | VpcId: !Ref VPC
141 | AvailabilityZone: !Select
142 | - 1
143 | - Fn::GetAZs: !Ref 'AWS::Region'
144 | Tags:
145 | - Key: Name
146 | Value: PublicSubnetB
147 |
148 | # Public Route Table
149 | PublicRouteTable:
150 | Type: AWS::EC2::RouteTable
151 | Properties:
152 | VpcId: !Ref VPC
153 | Tags:
154 | - Key: Name
155 | Value: PublicRouteTable
156 |
157 | DefaultPublicRoute:
158 | Type: AWS::EC2::Route
159 | DependsOn: InternetGatewayAttachment
160 | Properties:
161 | RouteTableId: !Ref PublicRouteTable
162 | DestinationCidrBlock: 0.0.0.0/0
163 | GatewayId: !Ref InternetGateway
164 |
165 | PublicSubnetARouteTableAssociation:
166 | Type: AWS::EC2::SubnetRouteTableAssociation
167 | Properties:
168 | RouteTableId: !Ref PublicRouteTable
169 | SubnetId: !Ref PublicSubnetA
170 |
171 | PublicSubnetBRouteTableAssociation:
172 | Type: AWS::EC2::SubnetRouteTableAssociation
173 | Properties:
174 | RouteTableId: !Ref PublicRouteTable
175 | SubnetId: !Ref PublicSubnetB
176 |
177 | ##########################
178 | ##### Private Subnet #####
179 | ########################
180 |
181 | PrivateSubnetA:
182 | Type: AWS::EC2::Subnet
183 | Properties:
184 | CidrBlock: !Ref PrivateSubnetAcidr
185 | VpcId: !Ref VPC
186 | AvailabilityZone: !Select
187 | - 0
188 | - Fn::GetAZs: !Ref 'AWS::Region'
189 | Tags:
190 | - Key: Name
191 | Value: PrivateSubnetA
192 |
193 | PrivateSubnetB:
194 | Type: AWS::EC2::Subnet
195 | Properties:
196 | CidrBlock: !Ref PrivateSubnetBcidr
197 | VpcId: !Ref VPC
198 | AvailabilityZone: !Select
199 | - 1
200 | - Fn::GetAZs: !Ref 'AWS::Region'
201 | Tags:
202 | - Key: Name
203 | Value: PrivateSubnetB
204 |
205 | # NAT Gateway
206 | NatGatewayAEIP:
207 | Type: AWS::EC2::EIP
208 | DependsOn: InternetGatewayAttachment
209 | Properties:
210 | Domain: vpc
211 |
212 | NatGatewayBEIP:
213 | Type: AWS::EC2::EIP
214 | DependsOn: InternetGatewayAttachment
215 | Properties:
216 | Domain: vpc
217 |
218 | NatGatewayA:
219 | Type: AWS::EC2::NatGateway
220 | Properties:
221 | AllocationId: !GetAtt NatGatewayAEIP.AllocationId
222 | SubnetId: !Ref PublicSubnetA
223 |
224 | NatGatewayB:
225 | Type: AWS::EC2::NatGateway
226 | Properties:
227 | AllocationId: !GetAtt NatGatewayBEIP.AllocationId
228 | SubnetId: !Ref PublicSubnetB
229 |
230 | PrivateRouteTableA:
231 | Type: AWS::EC2::RouteTable
232 | Properties:
233 | VpcId: !Ref VPC
234 | Tags:
235 | - Key: Name
236 | Value: PrivateRouteTableA
237 |
238 | DefaultPrivateRouteA:
239 | Type: AWS::EC2::Route
240 | Properties:
241 | RouteTableId: !Ref PrivateRouteTableA
242 | DestinationCidrBlock: 0.0.0.0/0
243 | NatGatewayId: !Ref NatGatewayA
244 |
245 | PrivateSubnetARouteTableAssociation:
246 | Type: AWS::EC2::SubnetRouteTableAssociation
247 | Properties:
248 | RouteTableId: !Ref PrivateRouteTableA
249 | SubnetId: !Ref PrivateSubnetA
250 |
251 | PrivateRouteTableB:
252 | Type: AWS::EC2::RouteTable
253 | Properties:
254 | VpcId: !Ref VPC
255 | Tags:
256 | - Key: Name
257 | Value: PrivateRouteTableB
258 |
259 | DefaultPrivateRouteB:
260 | Type: AWS::EC2::Route
261 | Properties:
262 | RouteTableId: !Ref PrivateRouteTableB
263 | DestinationCidrBlock: 0.0.0.0/0
264 | NatGatewayId: !Ref NatGatewayB
265 |
266 | PrivateSubnetBRouteTableAssociation:
267 | Type: AWS::EC2::SubnetRouteTableAssociation
268 | Properties:
269 | RouteTableId: !Ref PrivateRouteTableB
270 | SubnetId: !Ref PrivateSubnetB
271 |
272 | Outputs:
273 | VPC:
274 | Description: "VPC"
275 | Value: !Ref VPC
276 | Export:
277 | Name: Basic-VPC
278 |
279 | PublicSubnetA:
280 | Description: "PublicSubnetA"
281 | Value: !Ref PublicSubnetA
282 | Export:
283 | Name: Basic-PublicSubnetA
284 |
285 | PublicSubnetB:
286 | Description: "PublicSubnetB"
287 | Value: !Ref PublicSubnetB
288 | Export:
289 | Name: Basic-PublicSubnetB
290 |
291 | PrivateSubnetA:
292 | Description: "PrivateSubnetA"
293 | Value: !Ref PrivateSubnetA
294 | Export:
295 | Name: Basic-PrivateSubnetA
296 |
297 | PrivateSubnetB:
298 | Description: "PrivateSubnetB"
299 | Value: !Ref PrivateSubnetB
300 | Export:
301 | Name: Basic-PrivateSubnetB
302 |
303 | StreamlitCluster:
304 | Description: "StreamlitCluster"
305 | Value: !Ref StreamlitCluster
306 | Export:
307 | Name: StreamlitCluster
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Deploy Streamlit App on ECS
4 |
5 | ## Description
6 |
7 | This project is dedicated to providing an all-encompassing solution for hosting Streamlit applications on AWS by leveraging AWS CloudFormation and a robust Continuous Integration/Continuous Deployment (CI/CD) pipeline. By combining the power of infrastructure as code and automated deployment workflows, developers can effortlessly host and update their Streamlit apps, ensuring a seamless and efficient user experience.
8 |
9 | This repository provides base code for Streamlit application's and is not production ready. It is your responsibility as a developer to test and vet the application according to your security guidlines.
10 |
11 | ## Prerequisite
12 |
13 | An AWS Account, to deploy the infrastructure. You can find more instructions to create your account [here](https://aws.amazon.com/free).
14 |
15 | ## Table of contents
16 |
17 | You can choose to deploy your Streamlit web application using two different deployment options. The first option provides a CI/CD (Continuous Integration/Continuous Deployment) pipeline, which is great for development and production environments. The second option provides a quick setup for Proof of Concept (PoC) purposes.
18 |
19 | 1. [Continuous Integration and Continuous Delivery Deployment](#architecture-cicd-deployment)
20 | 1. [Steps to Deploy Hello World App](#steps-to-deploy-hello-world-app-cicd-deployment)
21 | 2. [Steps to Customize Web App](#steps-to-customize-web-app-cicd-deployment)
22 | 3. [Streamlit Secrets Management](#streamlit-secrets-management-cicd-deployment)
23 | 4. [Invoking AWS Services from Web App](#invoking-aws-services-from-web-app-cicd-deployment)
24 | 5. [Clean Up](#clean-up-cicd-deployment)
25 |
26 | 2. [Simple deployment](#architecture-simple-deployment)
27 | 1. [Steps to Deploy Hello World App](#steps-to-deploy-hello-world-app-development-deployment)
28 | 2. [Clean Up](#clean-up-simple-deployment)
29 |
30 | ## Architecture CICD Deployment
31 |
32 | 
33 |
34 | 1. Developer manually deploys [codepipeline.yaml](/cfn_stack/pipeline/codepipeline.yaml) stack, [infrastructure.yaml](/cfn_stack/pipeline/infrastructure.yaml) is deployed as nested stack.
35 | 2. Lambda triggers the CodeBuild project.
36 | 3. The CodeBuild project zip's this repository content into app.zip file.
37 | 4. CodeBuild copies app.zip into S3 bucket.
38 | 5. app.zip PUT event triggers the CodePipeline and triggers the CodeBuild stage.
39 | 6. This CodeBuild is responsible for creating a container image using the DockerFile and pushing this image into ECR.
40 | 7. Deploy stage is trigged.
41 | 8. Cloudformation stage deploys the [deploy.yaml](/cfn_stack/pipeline/deploy.yaml) stack. This stack takes the new docker image URI as input. This stage creates the Hello world app. Follow steps [here](#steps-to-deploy-hello-world-app-cicd-deployment).
42 | 9. After successfull creation of [deploy.yaml](/cfn_stack/pipeline/deploy.yaml) stack, Cloudfront invalidate cache stage is triggered.
43 | 10. Developer Customize's the Web App, zip's new content and uploads it into Amazon S3. This triggers the CodePipeline which results in new Docker image. These docker images replaces the old Fargate tasks. Follow steps [here](#steps-to-customize-web-app-cicd-deployment) to customize app.
44 |
45 | > [!NOTE]
46 | > Steps 2, 3 and 4 are run only once when Codepipeline.yaml is created. To Trigger the changes to the Streamlit web applicaiton manually follow steps [here](#steps-to-customize-web-app-cicd-deployment).
47 |
48 | ## Steps to Deploy Hello World App CICD deployment
49 |
50 | ### Step :one: Clone the forked repository
51 | ```
52 | git clone https://github.com/aws-samples/streamlit-deploy.git
53 | ```
54 |
55 | ### Step :two: Deploy codePipeline.yaml
56 |
57 | Create a CloudFormation Stack using the [codepipeline.yaml](/cfn_stack/pipeline/codepipeline.yaml) file.
58 |
59 | | Region | codepipeline.yaml |
60 | | ---------- | ----------------- |
61 | | us-east-1 | [](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=StreamlitDeploy&templateURL=https://ws-assets-prod-iad-r-iad-ed304a55c2ca1aee.s3.us-east-1.amazonaws.com/0a9f7588-a2c4-4484-b051-6658ce32605c/streamlit-deploy/pipeline/codepipeline.yaml)|
62 | | us-west-2 | [](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=StreamlitDeploy&templateURL=https://ws-assets-prod-iad-r-pdx-f3b3f9f1a7d6a3d0.s3.us-west-2.amazonaws.com/0a9f7588-a2c4-4484-b051-6658ce32605c/streamlit-deploy/pipeline/codepipeline.yaml)|
63 |
64 |
65 | ### Step :three: Viewing the app
66 |
67 | After the successful completion of CodePipeline, the `deploy.yaml` cloudFormation stack is deployed. Get the CloudFront URL from the `Output` of the stack named `deploy`. Paste it in the browser to view the Hellow World app.
68 |
69 | ## Steps to Customize Web App CICD deployment
70 |
71 | ### Step :one: Replace Web App Content
72 |
73 | In order to customize the web app change the content of `app.py` file.
74 |
75 | > [!CAUTION]
76 | > 1. Do not rename the `app.py` file
77 | > 2. Make sure to declare all packages in requirements.txt
78 |
79 | ### Step :two: Zip the Repository
80 |
81 | First commit all changes
82 |
83 | ```
84 | git add .
85 | git commit -m "All Changes"
86 | ```
87 |
88 | Then zip the current repository using the following command:
89 | ```
90 | git archive --format=zip --output=app.zip HEAD
91 | ```
92 |
93 | This will create an `app.zip` file.
94 |
95 | ### Step :three: Upload app.zip
96 |
97 | Upload the zip file into `CodeS3Bucket` either using S3 management console or AWS CLI.
98 |
99 | ```
100 | aws s3 cp app.zip s3://
101 | ```
102 |
103 | > [!IMPORTANT]
104 | > You have access to CodeS3Bucket name from `Outputs` of `codepipeline.yaml` cloudFormation stack
105 |
106 | ### Step :four: Deploy Further Revisions of the Web App
107 |
108 | Repeat `Step 2` and `Step 3` with modified content.
109 |
110 | ## Streamlit Secrets Management (CICD deployment)
111 |
112 | > [!IMPORTANT]
113 | > It is crucial to .gitignore files containing confidential information
114 |
115 | ### Step :one: Create Parameter in SSM Parameter store
116 |
117 | > [!TIP]
118 | > 1. For the purpose of simplicity we are using [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html). However, when dealing with sensitive secrets such as Database credentials, the best practice is to use [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/).
119 | > 2. If you decide to use AWS Secrets Manager for storing credentials make sure to follow stops [here](#invoking-aws-services-from-web-app) to give Fargate appropriate permissions.
120 |
121 | To get more information about creating secure parameters using SSM Parameter store visit [link](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-create-console.html).
122 |
123 | > [!CAUTION]
124 | > 1. Start the parameter path with /streamlitapp
125 | > 2. Use SecureString parameter type while creating the parameters to encrypt the parameters
126 |
127 | ### Step :two: Use boto3 for accessing Secrets within your streamlit app
128 |
129 | ```
130 | from boto3.session import Session
131 | ssm = Session().client("ssm")
132 |
133 | USERNAME = ssm.get_parameter(Name='/streamlitapp/USERNAME',WithDecryption=True)["Parameter"]["Value"]
134 | ```
135 |
136 | ## Invoking AWS Services from Web App (CICD deployment)
137 |
138 | Inorder to give permission to the web app to invoke AWS services add appropriate policies to `StreamlitECSTaskRole*` role.
139 |
140 | For instance, if you want to invoke Anthropic Claude V2 Bedrock model from the Streamlit app add the following policy to `StreamlitECSTaskRole*` role:
141 |
142 | ```
143 | {
144 | "Version": "2012-10-17",
145 | "Statement": [
146 | {
147 | "Sid": "Statement1",
148 | "Effect": "Allow",
149 | "Action": [
150 | "bedrock:InvokeModel"
151 | ],
152 | "Resource": [
153 | "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2"
154 | ]
155 | }
156 | ]
157 | }
158 | ```
159 |
160 | For invoking Bedrock Agents from Streamlit app add the following policy to `StreamlitECSTaskRole*` role:
161 |
162 | ```
163 | {
164 | "Version": "2012-10-17",
165 | "Statement": [
166 | {
167 | "Sid": "Statement1",
168 | "Effect": "Allow",
169 | "Action": [
170 | "bedrock:InvokeAgent"
171 | ],
172 | "Resource": [
173 | "arn:aws:bedrock:{Region}:{Account}:agent-alias/{AgentId}/{AgentAliasId}"
174 | ]
175 | }
176 | ]
177 | }
178 | ```
179 |
180 | > [!CAUTION]
181 | > Replace {Region}, {Account}, {AgentId}, and {AgentAliasId} with valid values in the above policy
182 |
183 | ## Clean up CICD deployment
184 | - Open the CloudFormation console.
185 | - Select the stack `codepipeline.yaml` you created then click **Delete**. Wait for the stack to be deleted.
186 | - Delete the nested stack `-Infrastructure-*` created by `codepipeline.yaml`. Please ensure that you refrain from deleting this stack if there are any additional web deployments utilizing this repository within the specified region of your current work environment.
187 | - Delete the role `-StreamlitCloudformationExecutionRole-*` manually.
188 |
189 |
190 | ## Architecture Simple Deployment
191 | 
192 | ## Steps to Deploy Hello World App Development deployment
193 |
194 | > [!NOTE]
195 | > Optionally, you can deploy the Virtual Private Cloud (VPC) infrastructure using the provided [infrastructure.yaml](/cfn_stack/development/infrastructure.yaml) file, or utilize the default VPC. The required infrastructure components, including Amazon CloudFront, an Application Load Balancer, and Amazon Elastic Container Service (ECS) on AWS Fargate instances, will be deployed within the chosen VPC environment.
196 |
197 | | Region | infrastructure.yaml |
198 | | ---------- | ----------------- |
199 | | us-east-1 | [](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=StreamlitDeployInfra&templateURL=https://ws-assets-prod-iad-r-iad-ed304a55c2ca1aee.s3.us-east-1.amazonaws.com/0a9f7588-a2c4-4484-b051-6658ce32605c/streamlit-deploy/development/infrastructure.yaml)|
200 | | us-west-2 | [](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=StreamlitDeployInfra&templateURL=https://ws-assets-prod-iad-r-pdx-f3b3f9f1a7d6a3d0.s3.us-west-2.amazonaws.com/0a9f7588-a2c4-4484-b051-6658ce32605c/streamlit-deploy/development/infrastructure.yaml)|
201 |
202 |
203 | ### Step :one: Deploy [development.yaml](/cfn_stack/development/development.yaml).
204 |
205 | | Region | development.yaml |
206 | | ---------- | ----------------- |
207 | | us-east-1 | [](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=StreamlitDeploy&templateURL=https://ws-assets-prod-iad-r-iad-ed304a55c2ca1aee.s3.us-east-1.amazonaws.com/0a9f7588-a2c4-4484-b051-6658ce32605c/streamlit-deploy/development/development.yaml)|
208 | | us-west-2 | [](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=StreamlitDeploy&templateURL=https://ws-assets-prod-iad-r-pdx-f3b3f9f1a7d6a3d0.s3.us-west-2.amazonaws.com/0a9f7588-a2c4-4484-b051-6658ce32605c/streamlit-deploy/development/development.yaml)|
209 |
210 | ### Step :two: Viewing the app
211 |
212 | After the successful completion of `development.yaml`. Get the CloudFront URL from the `Output` of the stack. Paste it in the browser to view the web application.
213 |
214 | ## Clean up simple deployment
215 | - Open the CloudFormation console.
216 | - Select the stack `infrastructure.yaml` you created then click **Delete**. Wait for the stack to be deleted.
217 | - Select the stack `development.yaml` you created then click **Delete**. Wait for the stack to be deleted.
218 |
219 | ## Security
220 |
221 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
222 |
223 | ## License
224 |
225 | This library is licensed under the MIT-0 License. See the LICENSE file.
226 |
227 |
--------------------------------------------------------------------------------
/cfn_stack/pipeline/deploy.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: 2010-09-09
2 | Description:
3 | This CloudFormation template provisions
4 | 1. ECS Cluster, Task Definition, and Service for hosting the Streamlit application on AWS Fargate.
5 | 2. Application Load Balancer, Target Group, Security Groups, and Listener Rules for load balancing and routing traffic.
6 | 3. AutoScaling configuration, including target and scaling policy, for automatically scaling ECS tasks based on CPU utilization.
7 | 4. CloudFront Distribution with caching and content delivery settings, using the ALB as the origin.
8 |
9 | Metadata:
10 | 'AWS::CloudFormation::Interface':
11 | ParameterGroups:
12 | - Label:
13 | default: 'Container Configuration'
14 | Parameters:
15 | - Cpu
16 | - Memory
17 | - StreamLitImageURI
18 | - ContainerPort
19 | - Label:
20 | default: 'Autoscaling'
21 | Parameters:
22 | - Task
23 | - Min
24 | - Max
25 | - AutoScalingTargetValue
26 | - Label:
27 | default: 'Infrastructure'
28 | Parameters:
29 | - StreamlitCluster
30 | - StreamlitPublicSubnetA
31 | - StreamlitPublicSubnetB
32 | - StreamlitPrivateSubnetA
33 | - StreamlitPrivateSubnetB
34 | - LoggingBucketName
35 | - Label:
36 | default: 'Environment Configuration'
37 | Parameters:
38 | - UniqueId
39 |
40 | Parameters:
41 |
42 | UniqueId:
43 | Description: A unique identifier for resources in this stack
44 | Type: String
45 | Default: streamlit-example
46 |
47 | StreamlitCluster:
48 | Description: StreamlitCluster
49 | Type: String
50 |
51 | StreamLitImageURI:
52 | Description: Image URI
53 | Type: String
54 | Default: .dkr.ecr..amazonaws.com/:
55 |
56 | Cpu:
57 | Description: "CPU of Fargate Task. Make sure you put valid Memory and CPU pair, refer: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-cpu:~:text=requires%3A%20Replacement-,Cpu,-The%20number%20of"
58 | Type: Number
59 | Default: 512
60 | AllowedValues:
61 | - 256
62 | - 512
63 | - 1024
64 | - 2048
65 | - 4096
66 |
67 | Memory:
68 | Description: "Memory of Fargate Task. Make sure you put valid Memory and CPU pair, refer: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-cpu:~:text=requires%3A%20Replacement-,Cpu,-The%20number%20of"
69 | Type: Number
70 | Default: 1024
71 | AllowedValues:
72 | - 512
73 | - 1024
74 | - 2048
75 | - 3072
76 | - 4096
77 | - 5120
78 | - 6144
79 | - 7168
80 | - 8192
81 | - 16384
82 | - 30720
83 |
84 | Task:
85 | Description: Desired Docker task count
86 | Type: Number
87 | Default: 2
88 |
89 | Min:
90 | Description: Minimum containers for Autoscaling. Should be less than or equal to DesiredTaskCount
91 | Type: Number
92 | Default: 2
93 |
94 | Max:
95 | Description: Maximum containers for Autoscaling. Should be greater than or equal to DesiredTaskCount
96 | Type: Number
97 | Default: 2
98 |
99 | AutoScalingTargetValue:
100 | Description: CPU Utilization Target
101 | Type: Number
102 | Default: 80
103 |
104 | StreamlitPublicSubnetA:
105 | Description: Task private subnet A
106 | Type: String
107 |
108 | StreamlitPublicSubnetB:
109 | Description: Task private subnet A
110 | Type: String
111 |
112 | StreamlitPrivateSubnetA:
113 | Description: Task private subnet A
114 | Type: String
115 |
116 | StreamlitPrivateSubnetB:
117 | Description: Task private subnet B
118 | Type: String
119 |
120 | LoggingBucketName:
121 | Description: Name of Logging Bucket
122 | Type: String
123 |
124 | StreamlitVPC:
125 | Description: Id of VPC created
126 | Type: String
127 |
128 | ContainerPort:
129 | Description: Port for Docker host and container
130 | Type: Number
131 | Default: 80
132 |
133 | Mappings:
134 | # Cloudfront Mappings
135 | CFRegionMap:
136 | 'us-east-1':
137 | PrefixListCloudFront: 'pl-3b927c52'
138 | 'us-west-2':
139 | PrefixListCloudFront: 'pl-82a045eb'
140 |
141 | Resources:
142 |
143 | ############################
144 | ##### Security Groups #####
145 | ##########################
146 |
147 | StreamlitALBSecurityGroup:
148 | Type: AWS::EC2::SecurityGroup
149 | Properties:
150 | GroupDescription: !Sub Allow ${ContainerPort} port from Cloudfront
151 | VpcId: !Ref StreamlitVPC
152 | Tags:
153 | - Key: Name
154 | Value: !Sub "StreamlitALBSecurityGroup${AWS::StackName}"
155 |
156 | ALBSGOutBoundRule:
157 | Type: AWS::EC2::SecurityGroupEgress
158 | Properties:
159 | GroupId: !GetAtt StreamlitALBSecurityGroup.GroupId
160 | IpProtocol: tcp
161 | FromPort: !Ref ContainerPort
162 | ToPort: !Ref ContainerPort
163 | CidrIp: 0.0.0.0/0
164 | Description: !Sub Allow outbound ${ContainerPort} port
165 |
166 | ALBSGInBoundRule:
167 | Type: AWS::EC2::SecurityGroupIngress
168 | Properties:
169 | GroupId: !GetAtt StreamlitALBSecurityGroup.GroupId
170 | IpProtocol: tcp
171 | FromPort: !Ref ContainerPort
172 | ToPort: !Ref ContainerPort
173 | SourcePrefixListId: !FindInMap
174 | - CFRegionMap
175 | - !Ref AWS::Region
176 | - PrefixListCloudFront
177 | Description: !Sub Allow ${ContainerPort} port from Cloudfront
178 |
179 | StreamlitContainerSecurityGroup:
180 | Type: AWS::EC2::SecurityGroup
181 | Properties:
182 | GroupDescription: Allow container traffic from ALB
183 | VpcId: !Ref StreamlitVPC
184 | Tags:
185 | - Key: Name
186 | Value: !Sub "StreamlitContainerSecurityGroup${AWS::StackName}"
187 |
188 | ContainerSGOutBoundRule:
189 | Type: AWS::EC2::SecurityGroupEgress
190 | Properties:
191 | GroupId: !GetAtt StreamlitContainerSecurityGroup.GroupId
192 | IpProtocol: -1
193 | FromPort: !Ref ContainerPort
194 | ToPort: !Ref ContainerPort
195 | CidrIp: 0.0.0.0/0
196 | Description: !Sub Allow ${ContainerPort} port outbound for all traffic
197 |
198 | ContainerSGInBoundRule:
199 | Type: AWS::EC2::SecurityGroupIngress
200 | Properties:
201 | GroupId: !GetAtt StreamlitContainerSecurityGroup.GroupId
202 | IpProtocol: tcp
203 | FromPort: !Ref ContainerPort
204 | ToPort: !Ref ContainerPort
205 | SourceSecurityGroupId: !Ref StreamlitALBSecurityGroup
206 | Description: !Sub Allow ${ContainerPort} port from ALB SG
207 |
208 | #################################
209 | ##### ECS Task and Service #####
210 | ###############################
211 |
212 | StreamlitExecutionRole:
213 | Type: AWS::IAM::Role
214 | Properties:
215 | AssumeRolePolicyDocument:
216 | Statement:
217 | - Effect: Allow
218 | Principal:
219 | Service: ecs-tasks.amazonaws.com
220 | Action: 'sts:AssumeRole'
221 | ManagedPolicyArns:
222 | - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'
223 |
224 | StreamlitECSTaskRole:
225 | Type: AWS::IAM::Role
226 | Properties:
227 | AssumeRolePolicyDocument:
228 | Statement:
229 | - Effect: Allow
230 | Principal:
231 | Service: ecs-tasks.amazonaws.com
232 | Action: 'sts:AssumeRole'
233 | Policies:
234 | - PolicyName: 'TaskSSMPolicy'
235 | PolicyDocument:
236 | Version: '2012-10-17'
237 | Statement:
238 | - Effect: 'Allow'
239 | Action:
240 | - "ssm:GetParameter"
241 | Resource:
242 | - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/streamlitapp/*"
243 | - Effect: 'Allow'
244 | Action:
245 | - "kms:Decrypt"
246 | Resource:
247 | - !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/alias/aws/ssm"
248 | - Effect: 'Allow'
249 | Action:
250 | - "bedrock:InvokeModelWithResponseStream"
251 | Resource:
252 | - "*"
253 |
254 | StreamlitLogGroup:
255 | DeletionPolicy: Retain
256 | UpdateReplacePolicy: Retain
257 | Type: AWS::Logs::LogGroup
258 | Properties:
259 | RetentionInDays: 7
260 |
261 | StreamlitTaskDefinition:
262 | Type: AWS::ECS::TaskDefinition
263 | Properties:
264 | Memory: !Ref Memory
265 | Cpu: !Ref Cpu
266 | NetworkMode: awsvpc
267 | RequiresCompatibilities:
268 | - 'FARGATE'
269 | RuntimePlatform:
270 | OperatingSystemFamily: LINUX
271 | TaskRoleArn: !GetAtt StreamlitECSTaskRole.Arn
272 | ExecutionRoleArn: !GetAtt StreamlitExecutionRole.Arn
273 | ContainerDefinitions:
274 | - Name: !Join ['-', ['ContainerDefinition', !Sub "${AWS::StackName}"]]
275 | LogConfiguration:
276 | LogDriver: "awslogs"
277 | Options:
278 | awslogs-group: !Ref StreamlitLogGroup
279 | awslogs-region: !Ref AWS::Region
280 | awslogs-stream-prefix: "ecs"
281 | Image: !Ref StreamLitImageURI
282 | PortMappings:
283 | - AppProtocol: "http"
284 | ContainerPort: !Ref ContainerPort
285 | HostPort: !Ref ContainerPort
286 | Name: !Join ['-', ['streamlit', !Ref ContainerPort, 'tcp']]
287 | Protocol: "tcp"
288 |
289 | StreamlitECSService:
290 | DependsOn:
291 | - StreamlitApplicationLoadBalancer
292 | - StreamlitALBListenerRule
293 | Type: AWS::ECS::Service
294 | Properties:
295 | Cluster: !Ref StreamlitCluster
296 | TaskDefinition: !Ref StreamlitTaskDefinition
297 | DesiredCount: !Ref Task
298 | HealthCheckGracePeriodSeconds: 120
299 | LaunchType: FARGATE
300 | NetworkConfiguration:
301 | AwsvpcConfiguration:
302 | Subnets:
303 | - !Ref StreamlitPrivateSubnetA
304 | - !Ref StreamlitPrivateSubnetB
305 | SecurityGroups:
306 | - !Ref StreamlitContainerSecurityGroup
307 | LoadBalancers:
308 | - ContainerName: !Join ['-', ['ContainerDefinition', !Sub "${AWS::StackName}"]]
309 | ContainerPort: !Ref ContainerPort
310 | TargetGroupArn: !Ref StreamlitContainerTargetGroup
311 |
312 | ########################
313 | ##### AutoScaling #####
314 | ######################
315 |
316 | StreamlitAutoScalingRole:
317 | Type: AWS::IAM::Role
318 | Properties:
319 | AssumeRolePolicyDocument:
320 | Statement:
321 | - Effect: Allow
322 | Principal:
323 | Service: ecs-tasks.amazonaws.com
324 | Action: 'sts:AssumeRole'
325 | ManagedPolicyArns:
326 | - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole'
327 |
328 | StreamlitAutoScalingTarget:
329 | Type: AWS::ApplicationAutoScaling::ScalableTarget
330 | Properties:
331 | MinCapacity: !Ref Min
332 | MaxCapacity: !Ref Max
333 | ResourceId: !Join ['/', [service, !Ref StreamlitCluster, !GetAtt StreamlitECSService.Name]]
334 | ScalableDimension: ecs:service:DesiredCount
335 | ServiceNamespace: ecs
336 | # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that allows Application Auto Scaling to modify your scalable target."
337 | RoleARN: !GetAtt StreamlitAutoScalingRole.Arn
338 |
339 | StreamlitAutoScalingPolicy:
340 | Type: AWS::ApplicationAutoScaling::ScalingPolicy
341 | Properties:
342 | PolicyName: !Join ['', [AutoScalingPolicy, !Sub "${AWS::StackName}"]]
343 | PolicyType: TargetTrackingScaling
344 | ScalingTargetId: !Ref StreamlitAutoScalingTarget
345 | TargetTrackingScalingPolicyConfiguration:
346 | PredefinedMetricSpecification:
347 | PredefinedMetricType: ECSServiceAverageCPUUtilization
348 | ScaleInCooldown: 60
349 | ScaleOutCooldown: 60
350 | # Keep things at or lower than 50% CPU utilization, for example
351 | TargetValue: !Ref AutoScalingTargetValue
352 |
353 |
354 | ######################################
355 | ##### Application Load Balancer #####
356 | ####################################
357 |
358 | StreamlitContainerTargetGroup:
359 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
360 | Properties:
361 | Name: !Ref UniqueId
362 | Port: !Ref ContainerPort
363 | Protocol: "HTTP"
364 | TargetType: ip
365 | VpcId: !Ref StreamlitVPC
366 | TargetGroupAttributes:
367 | - Key: stickiness.enabled
368 | Value: true
369 | - Key: stickiness.type
370 | Value: lb_cookie
371 |
372 | StreamlitApplicationLoadBalancer:
373 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
374 | Properties:
375 | Name: !Ref UniqueId
376 | LoadBalancerAttributes:
377 | - Key: access_logs.s3.enabled
378 | Value: true
379 | - Key: access_logs.s3.bucket
380 | Value: !Ref LoggingBucketName
381 | - Key: access_logs.s3.prefix
382 | Value: alb/logs
383 | - Key: load_balancing.cross_zone.enabled
384 | Value: true
385 | Scheme: internet-facing
386 | Type: application
387 | Subnets:
388 | - !Ref StreamlitPublicSubnetA
389 | - !Ref StreamlitPublicSubnetB
390 | SecurityGroups:
391 | - !Ref StreamlitALBSecurityGroup
392 | IpAddressType: ipv4
393 |
394 | StreamlitHTTPListener:
395 | Type: "AWS::ElasticLoadBalancingV2::Listener"
396 | Properties:
397 | LoadBalancerArn: !Ref StreamlitApplicationLoadBalancer
398 | Port: !Ref ContainerPort
399 | Protocol: HTTP
400 | DefaultActions:
401 | - FixedResponseConfig:
402 | ContentType: text/plain
403 | MessageBody: Access denied
404 | StatusCode: 403
405 | Type: fixed-response
406 |
407 | StreamlitALBListenerRule:
408 | Type: AWS::ElasticLoadBalancingV2::ListenerRule
409 | Properties:
410 | Actions:
411 | - Type: forward
412 | TargetGroupArn: !Ref StreamlitContainerTargetGroup
413 | Conditions:
414 | - Field: http-header
415 | HttpHeaderConfig:
416 | HttpHeaderName: X-Custom-Header
417 | Values:
418 | - !Join ['-', [!Sub "${AWS::StackName}", !Sub "${AWS::AccountId}"]]
419 | ListenerArn: !Ref StreamlitHTTPListener
420 | Priority: 1
421 |
422 | #########################
423 | ##### Distribution #####
424 | #######################
425 |
426 | Distribution:
427 | Type: "AWS::CloudFront::Distribution"
428 | Properties:
429 | DistributionConfig:
430 | Origins:
431 | - ConnectionAttempts: 3
432 | ConnectionTimeout: 10
433 | DomainName: !GetAtt StreamlitApplicationLoadBalancer.DNSName
434 | Id: !Ref StreamlitApplicationLoadBalancer
435 | OriginCustomHeaders:
436 | - HeaderName: X-Custom-Header
437 | HeaderValue: !Join ['-', [!Sub "${AWS::StackName}", !Sub "${AWS::AccountId}"]]
438 | CustomOriginConfig:
439 | HTTPPort: !Ref ContainerPort
440 | OriginProtocolPolicy: 'http-only'
441 | DefaultCacheBehavior:
442 | ForwardedValues:
443 | Cookies:
444 | Forward: whitelist
445 | WhitelistedNames: [token]
446 | QueryString: true
447 | QueryStringCacheKeys: [code]
448 | Compress: true
449 | ViewerProtocolPolicy: 'https-only'
450 | AllowedMethods:
451 | - "HEAD"
452 | - "DELETE"
453 | - "POST"
454 | - "GET"
455 | - "OPTIONS"
456 | - "PUT"
457 | - "PATCH"
458 | CachedMethods:
459 | - "HEAD"
460 | - "GET"
461 | CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
462 | OriginRequestPolicyId: "216adef6-5c7f-47e4-b989-5492eafa07d3"
463 | TargetOriginId: !Ref StreamlitApplicationLoadBalancer
464 | PriceClass: "PriceClass_All"
465 | Enabled: true
466 | HttpVersion: "http2"
467 | IPV6Enabled: true
468 | Logging:
469 | Bucket: !Sub '${LoggingBucketName}.s3.amazonaws.com'
470 | IncludeCookies: true
471 | Prefix: !Sub distribution-${AWS::StackName}-logs/
472 | ViewerCertificate:
473 | CloudFrontDefaultCertificate: true
474 | MinimumProtocolVersion: TLSv1.2_2021
475 | Tags:
476 | - Key: CloudfrontStreamlitApp
477 | Value: !Sub ${AWS::StackName}-Cloudfront
478 |
479 | Outputs:
480 | CloudfrontURL:
481 | Description: "CloudFront URL"
482 | Value: !GetAtt Distribution.DomainName
483 |
484 | CloudfrontID:
485 | Description: "CloudFront ID"
486 | Value: !Ref Distribution
487 |
--------------------------------------------------------------------------------
/cfn_stack/development/development.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Description: CloudFormation template deploys a containerized Streamlit app for Architectrue to CloudFormation project.
3 | This templates provisions the following resources
4 | - ECS cluster, task definition, service to run Streamlit containers
5 | - CodeBuild project to build Streamlit Docker image and push to ECR repo
6 | - Application Load Balancer, listener, target group for Streamlit service
7 | - Auto Scaling for dynamic scaling of Streamlit task count
8 | - CloudFront distribution for global access & caching
9 | - Logging to S3 & CloudWatch
10 |
11 | Metadata:
12 | 'AWS::CloudFormation::Interface':
13 | ParameterGroups:
14 | - Label:
15 | default: 'Environment Configuration'
16 | Parameters:
17 | - GitURL
18 | - Label:
19 | default: 'Autoscaling'
20 | Parameters:
21 | - Task
22 | - Min
23 | - Max
24 | - TargetCpu
25 | - Label:
26 | default: 'Container Configurations'
27 | Parameters:
28 | - Cpu
29 | - Memory
30 | - ContainerPort
31 | - Label:
32 | default: 'VPC Configurations'
33 | Parameters:
34 | - VPCId
35 | - PublicSubnetAId
36 | - PublicSubnetBId
37 | - PrivateSubnetAId
38 | - PrivateSubnetBId
39 |
40 | Parameters:
41 | PublicSubnetAId:
42 | Type: AWS::EC2::Subnet::Id
43 | Description: Public Subnet A Id
44 |
45 | PublicSubnetBId:
46 | Type: AWS::EC2::Subnet::Id
47 | Description: Public Subnet B Id
48 |
49 | PrivateSubnetAId:
50 | Type: AWS::EC2::Subnet::Id
51 | Description: Private Subnet A Id
52 |
53 | PrivateSubnetBId:
54 | Type: AWS::EC2::Subnet::Id
55 | Description: Private Subnet B Id
56 |
57 | VPCId:
58 | Type: AWS::EC2::VPC::Id
59 | Description: VPC Id
60 |
61 | GitURL:
62 | Type: String
63 | Description: Initial repository for web application
64 | Default: https://github.com/aws-samples/streamlit-deploy.git
65 |
66 | Cpu:
67 | Description: "CPU of Fargate Task. Make sure you put valid Memory and CPU pair, refer: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-cpu:~:text=requires%3A%20Replacement-,Cpu,-The%20number%20of"
68 | Type: Number
69 | Default: 512
70 | AllowedValues:
71 | - 256
72 | - 512
73 | - 1024
74 | - 2048
75 | - 4096
76 |
77 | Memory:
78 | Description: "Memory of Fargate Task. Make sure you put valid Memory and CPU pair, refer: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-cpu:~:text=requires%3A%20Replacement-,Cpu,-The%20number%20of"
79 | Type: Number
80 | Default: 1024
81 | AllowedValues:
82 | - 512
83 | - 1024
84 | - 2048
85 | - 3072
86 | - 4096
87 | - 5120
88 | - 6144
89 | - 7168
90 | - 8192
91 | - 16384
92 | - 30720
93 |
94 | Task:
95 | Description: Desired Docker task count
96 | Type: Number
97 | Default: 2
98 |
99 | Min:
100 | Description: Minimum containers for Autoscaling. Should be less than or equal to DesiredTaskCount
101 | Type: Number
102 | Default: 2
103 |
104 | Max:
105 | Description: Maximum containers for Autoscaling. Should be greater than or equal to DesiredTaskCount
106 | Type: Number
107 | Default: 5
108 |
109 | TargetCpu:
110 | Description: CPU Utilization Target
111 | Type: Number
112 | Default: 80
113 |
114 | ContainerPort:
115 | Description: Port for Docker host and container
116 | Type: Number
117 | Default: 80
118 |
119 | Mappings:
120 | # Cloudfront Mappings
121 | CFRegionMap:
122 | 'us-east-1':
123 | PrefixListCloudFront: 'pl-3b927c52'
124 | 'us-west-2':
125 | PrefixListCloudFront: 'pl-82a045eb'
126 |
127 | # Cloudfront Mappings
128 | ELBRegionMap:
129 | 'us-east-1':
130 | ELBAccountId: '127311923021'
131 | 'us-west-2':
132 | ELBAccountId: '797873946194'
133 |
134 | Resources:
135 |
136 | ####################
137 | ##### Logging #####
138 | ##################
139 |
140 | LogsPolicy:
141 | Type: "AWS::IAM::ManagedPolicy"
142 | Properties:
143 | PolicyDocument:
144 | Version: '2012-10-17'
145 | Statement:
146 | - Effect: 'Allow'
147 | Action:
148 | - 'logs:CreateLogGroup'
149 | - 'logs:CreateLogStream'
150 | - 'logs:PutLogEvents'
151 | - 'logs:PutRetentionPolicy'
152 | Resource: '*'
153 |
154 | LoggingBucket:
155 | Type: "AWS::S3::Bucket"
156 | DeletionPolicy: Retain
157 | UpdateReplacePolicy: Retain
158 | Properties:
159 | OwnershipControls:
160 | Rules:
161 | - ObjectOwnership: BucketOwnerPreferred
162 | PublicAccessBlockConfiguration:
163 | BlockPublicAcls: true
164 | BlockPublicPolicy: true
165 | IgnorePublicAcls: true
166 | RestrictPublicBuckets: true
167 | VersioningConfiguration:
168 | Status: Enabled
169 | BucketEncryption:
170 | ServerSideEncryptionConfiguration:
171 | - ServerSideEncryptionByDefault:
172 | SSEAlgorithm: AES256
173 |
174 | LoggingBucketPolicy:
175 | Type: 'AWS::S3::BucketPolicy'
176 | DeletionPolicy: Retain
177 | UpdateReplacePolicy: Retain
178 | Properties:
179 | Bucket: !Ref LoggingBucket
180 | PolicyDocument:
181 | Version: '2012-10-17'
182 | Statement:
183 | - Action:
184 | - 's3:PutObject'
185 | Effect: Allow
186 | Principal:
187 | Service: logging.s3.amazonaws.com
188 | Resource:
189 | - !Sub arn:aws:s3:::${LoggingBucket}/*
190 | - Action:
191 | - 's3:PutObject'
192 | Effect: Allow
193 | Principal:
194 | AWS: !Sub
195 | - arn:aws:iam::${ElbAccount}:root
196 | - {ElbAccount: !FindInMap [ELBRegionMap, !Ref 'AWS::Region', ELBAccountId]}
197 | Resource:
198 | - !Sub arn:aws:s3:::${LoggingBucket}/alb/logs/AWSLogs/${AWS::AccountId}/*
199 | - Action:
200 | - 's3:*'
201 | Effect: Deny
202 | Resource:
203 | - !Sub arn:aws:s3:::${LoggingBucket}/*
204 | - !Sub arn:aws:s3:::${LoggingBucket}
205 | Principal: "*"
206 | Condition:
207 | Bool:
208 | 'aws:SecureTransport': 'false'
209 |
210 | ##############################
211 | ##### Streamlit Cluster #####
212 | ############################
213 |
214 | StreamlitCluster:
215 | Type: AWS::ECS::Cluster
216 | Properties:
217 | ClusterSettings:
218 | - Name: containerInsights
219 | Value: enabled
220 |
221 | StreamlitImageRepo:
222 | Type: AWS::ECR::Repository
223 | Properties:
224 | EmptyOnDelete: true
225 | ImageScanningConfiguration:
226 | ScanOnPush: true
227 |
228 | ############################
229 | ##### Security Groups #####
230 | ##########################
231 |
232 | StreamlitALBSecurityGroup:
233 | Type: AWS::EC2::SecurityGroup
234 | Properties:
235 | GroupDescription: !Sub Allow ${ContainerPort} port from Cloudfront
236 | VpcId: !Ref VPCId
237 | Tags:
238 | - Key: Name
239 | Value: !Sub "StreamlitALBSecurityGroup${AWS::StackName}"
240 |
241 | ALBSGOutBoundRule:
242 | Type: AWS::EC2::SecurityGroupEgress
243 | Properties:
244 | GroupId: !GetAtt StreamlitALBSecurityGroup.GroupId
245 | IpProtocol: tcp
246 | FromPort: !Ref ContainerPort
247 | ToPort: !Ref ContainerPort
248 | CidrIp: 0.0.0.0/0
249 | Description: !Sub Allow outbound ${ContainerPort} port
250 |
251 | ALBSGInBoundRule:
252 | Type: AWS::EC2::SecurityGroupIngress
253 | Properties:
254 | GroupId: !GetAtt StreamlitALBSecurityGroup.GroupId
255 | IpProtocol: tcp
256 | FromPort: !Ref ContainerPort
257 | ToPort: !Ref ContainerPort
258 | SourcePrefixListId: !FindInMap
259 | - CFRegionMap
260 | - !Ref AWS::Region
261 | - PrefixListCloudFront
262 | Description: !Sub Allow ${ContainerPort} port from Cloudfront
263 |
264 | StreamlitContainerSecurityGroup:
265 | Type: AWS::EC2::SecurityGroup
266 | Properties:
267 | GroupDescription: Allow container traffic from ALB
268 | VpcId: !Ref VPCId
269 | Tags:
270 | - Key: Name
271 | Value: !Sub "StreamlitContainerSecurityGroup${AWS::StackName}"
272 |
273 | ContainerSGOutBoundRule:
274 | Type: AWS::EC2::SecurityGroupEgress
275 | Properties:
276 | GroupId: !GetAtt StreamlitContainerSecurityGroup.GroupId
277 | IpProtocol: -1
278 | FromPort: !Ref ContainerPort
279 | ToPort: !Ref ContainerPort
280 | CidrIp: 0.0.0.0/0
281 | Description: !Sub Allow ${ContainerPort} port outbound for all traffic
282 |
283 | ContainerSGInBoundRule:
284 | Type: AWS::EC2::SecurityGroupIngress
285 | Properties:
286 | GroupId: !GetAtt StreamlitContainerSecurityGroup.GroupId
287 | IpProtocol: tcp
288 | FromPort: !Ref ContainerPort
289 | ToPort: !Ref ContainerPort
290 | SourceSecurityGroupId: !Ref StreamlitALBSecurityGroup
291 | Description: !Sub Allow ${ContainerPort} port from ALB SG
292 |
293 | #################################
294 | ##### ECS Task and Service #####
295 | ###############################
296 |
297 | StreamlitExecutionRole:
298 | Type: AWS::IAM::Role
299 | Properties:
300 | AssumeRolePolicyDocument:
301 | Statement:
302 | - Effect: Allow
303 | Principal:
304 | Service: ecs-tasks.amazonaws.com
305 | Action: 'sts:AssumeRole'
306 | ManagedPolicyArns:
307 | - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'
308 |
309 | StreamlitECSTaskRole:
310 | Type: AWS::IAM::Role
311 | Properties:
312 | AssumeRolePolicyDocument:
313 | Statement:
314 | - Effect: Allow
315 | Principal:
316 | Service: ecs-tasks.amazonaws.com
317 | Action: 'sts:AssumeRole'
318 | Policies:
319 | - PolicyName: 'TaskSSMPolicy'
320 | PolicyDocument:
321 | Version: '2012-10-17'
322 | Statement:
323 | - Effect: 'Allow'
324 | Action:
325 | - "ssm:GetParameter"
326 | Resource:
327 | - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/streamlitapp/*"
328 | - Effect: 'Allow'
329 | Action:
330 | - "kms:Decrypt"
331 | Resource:
332 | - !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/alias/aws/ssm"
333 | - Effect: 'Allow'
334 | Action:
335 | - "bedrock:InvokeModelWithResponseStream"
336 | Resource:
337 | - "*"
338 |
339 | StreamlitLogGroup:
340 | DeletionPolicy: Retain
341 | UpdateReplacePolicy: Retain
342 | Type: AWS::Logs::LogGroup
343 | Properties:
344 | RetentionInDays: 7
345 |
346 | StreamlitTaskDefinition:
347 | Type: AWS::ECS::TaskDefinition
348 | Properties:
349 | Memory: !Ref Memory
350 | Cpu: !Ref Cpu
351 | NetworkMode: awsvpc
352 | RequiresCompatibilities:
353 | - 'FARGATE'
354 | RuntimePlatform:
355 | OperatingSystemFamily: LINUX
356 | TaskRoleArn: !GetAtt StreamlitECSTaskRole.Arn
357 | ExecutionRoleArn: !GetAtt StreamlitExecutionRole.Arn
358 | ContainerDefinitions:
359 | - Name: !Join ['-', ['ContainerDefinition', !Sub "${AWS::StackName}"]]
360 | LogConfiguration:
361 | LogDriver: "awslogs"
362 | Options:
363 | awslogs-group: !Ref StreamlitLogGroup
364 | awslogs-region: !Ref AWS::Region
365 | awslogs-stream-prefix: "ecs"
366 | Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${StreamlitImageRepo}:latest
367 | PortMappings:
368 | - AppProtocol: "http"
369 | ContainerPort: !Ref ContainerPort
370 | HostPort: !Ref ContainerPort
371 | Name: !Join ['-', ['streamlit', !Ref ContainerPort, 'tcp']]
372 | Protocol: "tcp"
373 |
374 | StreamlitECSService:
375 | DependsOn:
376 | - StreamlitApplicationLoadBalancer
377 | - StreamlitALBListenerRule
378 | Type: AWS::ECS::Service
379 | Properties:
380 | Cluster: !Ref StreamlitCluster
381 | TaskDefinition: !Ref StreamlitTaskDefinition
382 | DesiredCount: !Ref Task
383 | HealthCheckGracePeriodSeconds: 120
384 | LaunchType: FARGATE
385 | NetworkConfiguration:
386 | AwsvpcConfiguration:
387 | Subnets:
388 | - !Ref PrivateSubnetAId
389 | - !Ref PrivateSubnetBId
390 | SecurityGroups:
391 | - !Ref StreamlitContainerSecurityGroup
392 | LoadBalancers:
393 | - ContainerName: !Join ['-', ['ContainerDefinition', !Sub "${AWS::StackName}"]]
394 | ContainerPort: !Ref ContainerPort
395 | TargetGroupArn: !Ref StreamlitContainerTargetGroup
396 |
397 | ########################
398 | ##### AutoScaling #####
399 | ######################
400 |
401 | StreamlitAutoScalingRole:
402 | Type: AWS::IAM::Role
403 | Properties:
404 | AssumeRolePolicyDocument:
405 | Statement:
406 | - Effect: Allow
407 | Principal:
408 | Service: ecs-tasks.amazonaws.com
409 | Action: 'sts:AssumeRole'
410 | ManagedPolicyArns:
411 | - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole'
412 |
413 | StreamlitAutoScalingTarget:
414 | Type: AWS::ApplicationAutoScaling::ScalableTarget
415 | Properties:
416 | MinCapacity: !Ref Min
417 | MaxCapacity: !Ref Max
418 | ResourceId: !Join ['/', [service, !Ref StreamlitCluster, !GetAtt StreamlitECSService.Name]]
419 | ScalableDimension: ecs:service:DesiredCount
420 | ServiceNamespace: ecs
421 | # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that allows Application Auto Scaling to modify your scalable target."
422 | RoleARN: !GetAtt StreamlitAutoScalingRole.Arn
423 |
424 | StreamlitAutoScalingPolicy:
425 | Type: AWS::ApplicationAutoScaling::ScalingPolicy
426 | Properties:
427 | PolicyName: !Join ['', [AutoScalingPolicy, !Sub "${AWS::StackName}"]]
428 | PolicyType: TargetTrackingScaling
429 | ScalingTargetId: !Ref StreamlitAutoScalingTarget
430 | TargetTrackingScalingPolicyConfiguration:
431 | PredefinedMetricSpecification:
432 | PredefinedMetricType: ECSServiceAverageCPUUtilization
433 | ScaleInCooldown: 60
434 | ScaleOutCooldown: 60
435 | # Keep things at or lower than 50% CPU utilization, for example
436 | TargetValue: !Ref TargetCpu
437 |
438 | ######################################
439 | ##### Application Load Balancer #####
440 | ####################################
441 |
442 | StreamlitContainerTargetGroup:
443 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
444 | Properties:
445 | Port: !Ref ContainerPort
446 | Protocol: "HTTP"
447 | TargetType: ip
448 | VpcId: !Ref VPCId
449 | TargetGroupAttributes:
450 | - Key: stickiness.enabled
451 | Value: true
452 | - Key: stickiness.type
453 | Value: lb_cookie
454 |
455 | StreamlitApplicationLoadBalancer:
456 | DependsOn:
457 | - LoggingBucketPolicy
458 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
459 | Properties:
460 | LoadBalancerAttributes:
461 | - Key: access_logs.s3.enabled
462 | Value: true
463 | - Key: access_logs.s3.bucket
464 | Value: !Ref LoggingBucket
465 | - Key: access_logs.s3.prefix
466 | Value: alb/logs
467 | - Key: load_balancing.cross_zone.enabled
468 | Value: true
469 | Scheme: internet-facing
470 | Type: application
471 | Subnets:
472 | - !Ref PublicSubnetAId
473 | - !Ref PublicSubnetBId
474 | SecurityGroups:
475 | - !Ref StreamlitALBSecurityGroup
476 | IpAddressType: ipv4
477 |
478 | StreamlitHTTPListener:
479 | Type: "AWS::ElasticLoadBalancingV2::Listener"
480 | Properties:
481 | LoadBalancerArn: !Ref StreamlitApplicationLoadBalancer
482 | Port: !Ref ContainerPort
483 | Protocol: HTTP
484 | DefaultActions:
485 | - FixedResponseConfig:
486 | ContentType: text/plain
487 | MessageBody: Access denied
488 | StatusCode: 403
489 | Type: fixed-response
490 |
491 | StreamlitALBListenerRule:
492 | Type: AWS::ElasticLoadBalancingV2::ListenerRule
493 | Properties:
494 | Actions:
495 | - Type: forward
496 | TargetGroupArn: !Ref StreamlitContainerTargetGroup
497 | Conditions:
498 | - Field: http-header
499 | HttpHeaderConfig:
500 | HttpHeaderName: X-Custom-Header
501 | Values:
502 | - !Join ['-', [!Sub "${AWS::StackName}", !Sub "${AWS::AccountId}"]]
503 | ListenerArn: !Ref StreamlitHTTPListener
504 | Priority: 1
505 |
506 | #########################
507 | ##### Distribution #####
508 | #######################
509 |
510 | Distribution:
511 | Type: "AWS::CloudFront::Distribution"
512 | Properties:
513 | DistributionConfig:
514 | Origins:
515 | - ConnectionAttempts: 3
516 | ConnectionTimeout: 10
517 | DomainName: !GetAtt StreamlitApplicationLoadBalancer.DNSName
518 | Id: !Ref StreamlitApplicationLoadBalancer
519 | OriginCustomHeaders:
520 | - HeaderName: X-Custom-Header
521 | HeaderValue: !Join ['-', [!Sub "${AWS::StackName}", !Sub "${AWS::AccountId}"]]
522 | CustomOriginConfig:
523 | HTTPPort: !Ref ContainerPort
524 | OriginProtocolPolicy: 'http-only'
525 | DefaultCacheBehavior:
526 | ForwardedValues:
527 | Cookies:
528 | Forward: whitelist
529 | WhitelistedNames: [token]
530 | QueryString: true
531 | QueryStringCacheKeys: [code]
532 | Compress: true
533 | ViewerProtocolPolicy: 'https-only'
534 | AllowedMethods:
535 | - "HEAD"
536 | - "DELETE"
537 | - "POST"
538 | - "GET"
539 | - "OPTIONS"
540 | - "PUT"
541 | - "PATCH"
542 | CachedMethods:
543 | - "HEAD"
544 | - "GET"
545 | CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
546 | OriginRequestPolicyId: "216adef6-5c7f-47e4-b989-5492eafa07d3"
547 | TargetOriginId: !Ref StreamlitApplicationLoadBalancer
548 | PriceClass: "PriceClass_All"
549 | Enabled: true
550 | HttpVersion: "http2"
551 | IPV6Enabled: true
552 | Logging:
553 | Bucket: !Sub '${LoggingBucket}.s3.amazonaws.com'
554 | IncludeCookies: true
555 | Prefix: !Sub distribution-${AWS::StackName}-logs/
556 | ViewerCertificate:
557 | CloudFrontDefaultCertificate: true
558 | MinimumProtocolVersion: TLSv1.2_2021
559 | Tags:
560 | - Key: CloudfrontStreamlitApp
561 | Value: !Sub ${AWS::StackName}-Cloudfront
562 |
563 | #############################
564 | ##### Docker CodeBuild #####
565 | ###########################
566 |
567 | StreamlitCodeBuildExecutionRole:
568 | Type: 'AWS::IAM::Role'
569 | Properties:
570 | AssumeRolePolicyDocument:
571 | Version: '2012-10-17'
572 | Statement:
573 | - Effect: 'Allow'
574 | Principal:
575 | Service:
576 | - 'codebuild.amazonaws.com'
577 | Action:
578 | - 'sts:AssumeRole'
579 | ManagedPolicyArns:
580 | - !Ref LogsPolicy
581 | Policies:
582 | - PolicyName: 'CodeBuildPolicy'
583 | PolicyDocument:
584 | Version: '2012-10-17'
585 | Statement:
586 | - Effect: 'Allow'
587 | Action:
588 | - 'ecr:GetAuthorizationToken'
589 | Resource:
590 | - '*'
591 | - Effect: 'Allow'
592 | Action:
593 | - 'ecr:UploadLayerPart'
594 | - 'ecr:PutImage'
595 | - 'ecr:InitiateLayerUpload'
596 | - 'ecr:CompleteLayerUpload'
597 | - 'ecr:BatchCheckLayerAvailability'
598 | Resource:
599 | - !GetAtt StreamlitImageRepo.Arn
600 |
601 | StreamlitCodeBuild:
602 | Type: AWS::CodeBuild::Project
603 | Properties:
604 | Description: CodeBuild for building image
605 | Cache:
606 | Location: LOCAL
607 | Modes:
608 | - LOCAL_SOURCE_CACHE
609 | - LOCAL_DOCKER_LAYER_CACHE
610 | Type: LOCAL
611 | Source:
612 | Type: GITHUB
613 | Location: !Ref GitURL
614 | BuildSpec:
615 | !Sub
616 | - |
617 | version: 0.2
618 | phases:
619 | pre_build:
620 | commands:
621 | - pip3 install awscli
622 | - aws ecr get-login-password --region ${AWS::Region} | docker login --username AWS --password-stdin ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com
623 | build:
624 | commands:
625 | - echo Build started on `date`
626 | - docker build -t ${StreamlitImageRepo} .
627 | - docker tag ${StreamlitImageRepo}:latest ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${StreamlitImageRepo}:latest
628 | post_build:
629 | commands:
630 | - echo Build completed on `date`
631 | - docker push ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${StreamlitImageRepo}:latest
632 | - {
633 | StreamlitImageRepo: !Ref StreamlitImageRepo
634 | }
635 | Environment:
636 | Type: LINUX_CONTAINER
637 | Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
638 | ComputeType: BUILD_GENERAL1_SMALL
639 | ServiceRole: !GetAtt StreamlitCodeBuildExecutionRole.Arn
640 | TimeoutInMinutes: 10
641 | Artifacts:
642 | Type: NO_ARTIFACTS
643 |
644 | StreamlitCodeBuildLogGroup:
645 | Type: AWS::Logs::LogGroup
646 | Properties:
647 | RetentionInDays: 7
648 |
649 | ###################################
650 | ##### Start Docker CodeBuild #####
651 | #################################
652 |
653 | StreamlitBuildCustomResourceRole:
654 | Type: AWS::IAM::Role
655 | Properties:
656 | AssumeRolePolicyDocument:
657 | Version: '2012-10-17'
658 | Statement:
659 | - Effect: 'Allow'
660 | Principal:
661 | Service:
662 | - lambda.amazonaws.com
663 | Action:
664 | - 'sts:AssumeRole'
665 | Path: "/"
666 | Policies:
667 | - PolicyName: LambdaCustomPolicy
668 | PolicyDocument:
669 | Version: '2012-10-17'
670 | Statement:
671 | - Effect: Allow
672 | Action:
673 | - codebuild:StartBuild
674 | - codebuild:BatchGetBuilds
675 | Resource:
676 | - !GetAtt StreamlitCodeBuild.Arn
677 | - Effect: 'Allow'
678 | Action:
679 | - 'logs:CreateLogGroup'
680 | - 'logs:CreateLogStream'
681 | - 'logs:PutLogEvents'
682 | - 'logs:PutRetentionPolicy'
683 | Resource: '*'
684 |
685 | StreamlitBuildCustomResourceFunction:
686 | Type: "AWS::Lambda::Function"
687 | Properties:
688 | Handler: index.handler
689 | Role: !GetAtt StreamlitBuildCustomResourceRole.Arn
690 | Timeout: 300
691 | Runtime: python3.12
692 | Code:
693 | ZipFile: !Sub |
694 | import boto3
695 | from time import sleep
696 | import cfnresponse
697 |
698 | codebuild = boto3.client("codebuild")
699 |
700 | def handler(event, context):
701 | try:
702 | request_type = event['RequestType']
703 | if request_type == 'Create':
704 | status = 'STARTING'
705 |
706 | build_id = codebuild.start_build(projectName=event['ResourceProperties']['PROJECT'])['build']['id']
707 | while status not in ['SUCCEEDED', 'FAILED', 'STOPPED', 'FAULT', 'TIMED_OUT']:
708 | status = codebuild.batch_get_builds(ids=[build_id])['builds'][0]['buildStatus']
709 | sleep(15)
710 | if status in ['FAILED', 'STOPPED', 'FAULT', 'TIMED_OUT']:
711 | print("Initial CodeBuild failed")
712 | cfnresponse.send(event, context, cfnresponse.FAILED, {})
713 | return
714 | except Exception as ex:
715 | print(ex)
716 | cfnresponse.send(event, context, cfnresponse.FAILED, {})
717 | else:
718 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
719 |
720 | StreamlitBuildCustomResource:
721 | DependsOn: StreamlitECSRoleCustomResource
722 | Type: Custom::BuildCode
723 | Properties:
724 | ServiceToken: !GetAtt StreamlitBuildCustomResourceFunction.Arn
725 | PROJECT: !Ref StreamlitCodeBuild
726 |
727 | ################################
728 | ##### ECS Custom Resource #####
729 | ##############################
730 |
731 | StreamlitECSRoleCustomResourceRole:
732 | Type: AWS::IAM::Role
733 | Properties:
734 | AssumeRolePolicyDocument:
735 | Version: '2012-10-17'
736 | Statement:
737 | - Effect: 'Allow'
738 | Principal:
739 | Service:
740 | - lambda.amazonaws.com
741 | Action:
742 | - 'sts:AssumeRole'
743 | Path: "/"
744 | ManagedPolicyArns:
745 | - !Ref LogsPolicy
746 | Policies:
747 | - PolicyName: IAMPolicy
748 | PolicyDocument:
749 | Version: '2012-10-17'
750 | Statement:
751 | - Effect: Allow
752 | Action:
753 | - iam:ListRoles
754 | Resource:
755 | - "*"
756 | - Effect: Allow
757 | Action:
758 | - iam:GetRole
759 | - iam:CreateServiceLinkedRole
760 | - iam:AttachRolePolicy
761 | Resource:
762 | - "*"
763 |
764 | StreamlitECSRoleCustomResourceFunction:
765 | Type: "AWS::Lambda::Function"
766 | Properties:
767 | Handler: index.handler
768 | Role: !GetAtt StreamlitECSRoleCustomResourceRole.Arn
769 | Timeout: 300
770 | Runtime: python3.12
771 | Code:
772 | ZipFile: !Sub |
773 | import boto3
774 | from botocore.exceptions import ClientError
775 | import cfnresponse
776 | iam_client = boto3.client('iam')
777 |
778 | def handler(event, context):
779 |
780 | try:
781 | request_type = event['RequestType']
782 | print(request_type)
783 |
784 | if request_type == 'Create':
785 | desired_ecs_role_name = "AWSServiceRoleForECS"
786 | desired_ecs_scaling_role_name = "AWSServiceRoleForApplicationAutoScaling_ECSService"
787 |
788 | try:
789 | iam_client.get_role(RoleName=desired_ecs_role_name)
790 | ecs_role_exists = True
791 | except ClientError as e:
792 | if e.response['Error']['Code'] == 'NoSuchEntity':
793 | ecs_role_exists = False
794 | else:
795 | ecs_role_exists = True
796 |
797 | try:
798 | iam_client.get_role(RoleName=desired_ecs_scaling_role_name)
799 | ecs_scaling_role_exists = True
800 | except ClientError as e:
801 | if e.response['Error']['Code'] == 'NoSuchEntity':
802 | ecs_scaling_role_exists = False
803 | else:
804 | ecs_scaling_role_exists = True
805 |
806 | print(f"ECS service role exist? {ecs_role_exists}")
807 | if not ecs_role_exists:
808 | iam_client.create_service_linked_role(AWSServiceName="ecs.amazonaws.com")
809 |
810 | print(f"ECS scaling service role exist? {ecs_scaling_role_exists}")
811 | if not ecs_scaling_role_exists:
812 | iam_client.create_service_linked_role(AWSServiceName="ecs.application-autoscaling.amazonaws.com")
813 |
814 | except Exception as ex:
815 | print(ex)
816 | cfnresponse.send(event, context, cfnresponse.FAILED, {})
817 | else:
818 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
819 |
820 | StreamlitECSRoleCustomResource:
821 | Type: Custom::ECSRole
822 | Properties:
823 | ServiceToken: !GetAtt StreamlitECSRoleCustomResourceFunction.Arn
824 |
825 | Outputs:
826 | CloudfrontURL:
827 | Description: "CloudFront URL"
828 | Value: !GetAtt Distribution.DomainName
--------------------------------------------------------------------------------
/cfn_stack/pipeline/codepipeline.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Description: This AWS CloudFormation template sets up the following resources
3 | 1. Amazon Elastic Container Registry (ECR) for storing the Docker image, S3 buckets for artifacts, code, and CloudTrail logs, and logging-related resources.
4 | 2. IAM roles and policies for CodeBuild, CodePipeline, CloudFormation execution, and other components.
5 | 3. CodeBuild project for building and pushing the Docker image, and a Lambda function for invalidating the CloudFront cache.
6 | 4. CodePipeline for automating the deployment process, and a CloudWatch event rule for triggering the pipeline on code changes.
7 | 5. Custom resources for initializing the code S3 bucket, creating ECS service-linked roles, and cleaning up resources during stack deletion.
8 |
9 | Metadata:
10 | 'AWS::CloudFormation::Interface':
11 | ParameterGroups:
12 | - Label:
13 | default: 'Container Configuration'
14 | Parameters:
15 | - Cpu
16 | - Memory
17 | - Label:
18 | default: 'Environment Configuration'
19 | Parameters:
20 | - GitURL
21 | - DeployVPCInfrastructure
22 | - Label:
23 | default: 'Autoscaling'
24 | Parameters:
25 | - DesiredTaskCount
26 | - MinContainers
27 | - MaxContainers
28 | - AutoScalingTargetValue
29 |
30 | Parameters:
31 | Cpu:
32 | Description: "CPU of Fargate Task. Make sure you put valid Memory and CPU pair, refer: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-cpu:~:text=requires%3A%20Replacement-,Cpu,-The%20number%20of"
33 | Type: Number
34 | Default: 512
35 | AllowedValues:
36 | - 256
37 | - 512
38 | - 1024
39 | - 2048
40 | - 4096
41 |
42 | Memory:
43 | Description: "Memory of Fargate Task. Make sure you put valid Memory and CPU pair, refer: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-cpu:~:text=requires%3A%20Replacement-,Cpu,-The%20number%20of"
44 | Type: Number
45 | Default: 1024
46 | AllowedValues:
47 | - 512
48 | - 1024
49 | - 2048
50 | - 3072
51 | - 4096
52 | - 5120
53 | - 6144
54 | - 7168
55 | - 8192
56 | - 16384
57 | - 30720
58 |
59 | GitURL:
60 | Type: String
61 | Description: Initial repository for basic Hello World application
62 | Default: https://github.com/aws-samples/streamlit-deploy.git
63 |
64 | DeployVPCInfrastructure:
65 | Description: Select false if you already have infrastructure.yaml nested stack deployed in this region
66 | Type: String
67 | Default: 'true'
68 | AllowedValues:
69 | - 'true'
70 | - 'false'
71 |
72 | DesiredTaskCount:
73 | Description: Desired Docker task count
74 | Type: Number
75 | Default: 2
76 |
77 | MinContainers:
78 | Description: Minimum containers for Autoscaling. Should be less than or equal to DesiredTaskCount
79 | Type: Number
80 | Default: 2
81 |
82 | MaxContainers:
83 | Description: Maximum containers for Autoscaling. Should be greater than or equal to DesiredTaskCount
84 | Type: Number
85 | Default: 5
86 |
87 | AutoScalingTargetValue:
88 | Description: CPU Utilization Target
89 | Type: Number
90 | Default: 80
91 |
92 | Conditions:
93 | IsDeployVPCInfrastructure: !Equals
94 | - !Ref DeployVPCInfrastructure
95 | - 'true'
96 |
97 | # NotDeployVPCInfrastructure: !Equals
98 | # - !Ref DeployVPCInfrastructure
99 | # - 'false'
100 |
101 | Mappings:
102 |
103 | # Cloudfront Mappings
104 | ELBRegionMap:
105 | 'us-east-1':
106 | ELBAccountId: '127311923021'
107 | 'us-west-2':
108 | ELBAccountId: '797873946194'
109 | Resources:
110 |
111 | ##############################
112 | ##### Docker Image Repo #####
113 | ############################
114 |
115 | StreamlitImageRepo:
116 | Type: AWS::ECR::Repository
117 | Properties:
118 | EmptyOnDelete: true
119 | ImageScanningConfiguration:
120 | ScanOnPush: true
121 |
122 | #############################
123 | ##### Nested VPC Stack #####
124 | ###########################
125 |
126 | Infrastructure:
127 | Condition: IsDeployVPCInfrastructure
128 | DeletionPolicy: Retain
129 | UpdateReplacePolicy: Retain
130 | DependsOn: StreamlitBuildCustomResource
131 | Type: AWS::CloudFormation::Stack
132 | Properties:
133 | TemplateURL: !Sub https://s3.amazonaws.com/${StreamlitCodeS3Bucket}/infrastructure.yaml
134 |
135 |
136 | ####################
137 | ##### Logging #####
138 | ##################
139 |
140 | LogsPolicy:
141 | Type: "AWS::IAM::ManagedPolicy"
142 | Properties:
143 | PolicyDocument:
144 | Version: '2012-10-17'
145 | Statement:
146 | - Effect: 'Allow'
147 | Action:
148 | - 'logs:CreateLogGroup'
149 | - 'logs:CreateLogStream'
150 | - 'logs:PutLogEvents'
151 | - 'logs:PutRetentionPolicy'
152 | Resource: '*'
153 |
154 | LoggingBucket:
155 | Type: "AWS::S3::Bucket"
156 | DeletionPolicy: Retain
157 | UpdateReplacePolicy: Retain
158 | Properties:
159 | OwnershipControls:
160 | Rules:
161 | - ObjectOwnership: BucketOwnerPreferred
162 | PublicAccessBlockConfiguration:
163 | BlockPublicAcls: true
164 | BlockPublicPolicy: true
165 | IgnorePublicAcls: true
166 | RestrictPublicBuckets: true
167 | VersioningConfiguration:
168 | Status: Enabled
169 | BucketEncryption:
170 | ServerSideEncryptionConfiguration:
171 | - ServerSideEncryptionByDefault:
172 | SSEAlgorithm: AES256
173 |
174 | LoggingBucketPolicy:
175 | Type: 'AWS::S3::BucketPolicy'
176 | DeletionPolicy: Retain
177 | UpdateReplacePolicy: Retain
178 | Properties:
179 | Bucket: !Ref LoggingBucket
180 | PolicyDocument:
181 | Version: '2012-10-17'
182 | Statement:
183 | - Action:
184 | - 's3:PutObject'
185 | Effect: Allow
186 | Principal:
187 | Service: logging.s3.amazonaws.com
188 | Resource:
189 | - !Sub arn:aws:s3:::${LoggingBucket}/*
190 | - Action:
191 | - 's3:PutObject'
192 | Effect: Allow
193 | Principal:
194 | AWS: !Sub
195 | - arn:aws:iam::${ElbAccount}:root
196 | - {ElbAccount: !FindInMap [ELBRegionMap, !Ref 'AWS::Region', ELBAccountId]}
197 | Resource:
198 | - !Sub arn:aws:s3:::${LoggingBucket}/alb/logs/AWSLogs/${AWS::AccountId}/*
199 | - Action:
200 | - 's3:*'
201 | Effect: Deny
202 | Resource:
203 | - !Sub arn:aws:s3:::${LoggingBucket}/*
204 | - !Sub arn:aws:s3:::${LoggingBucket}
205 | Principal: "*"
206 | Condition:
207 | Bool:
208 | 'aws:SecureTransport': 'false'
209 |
210 | #######################
211 | ##### S3 Buckets #####
212 | #####################
213 |
214 | # Artifact Bucket
215 | StreamlitArtifactStore:
216 | Type: AWS::S3::Bucket
217 | Properties:
218 | LoggingConfiguration:
219 | DestinationBucketName: !Ref LoggingBucket
220 | LogFilePrefix: !Sub artifact-${AWS::StackName}-logs
221 | PublicAccessBlockConfiguration:
222 | BlockPublicAcls: true
223 | BlockPublicPolicy: true
224 | IgnorePublicAcls: true
225 | RestrictPublicBuckets: true
226 | VersioningConfiguration:
227 | Status: Enabled
228 | BucketEncryption:
229 | ServerSideEncryptionConfiguration:
230 | - ServerSideEncryptionByDefault:
231 | SSEAlgorithm: AES256
232 |
233 | StreamlitArtifactStorePolicy:
234 | Type: AWS::S3::BucketPolicy
235 | Properties:
236 | Bucket: !Ref StreamlitArtifactStore
237 | PolicyDocument:
238 | Version: 2012-10-17
239 | Statement:
240 | -
241 | Action:
242 | - s3:*
243 | Effect: Deny
244 | Resource:
245 | - !GetAtt StreamlitArtifactStore.Arn
246 | - !GetAtt StreamlitArtifactStore.Arn
247 | Principal: '*'
248 | Condition:
249 | Bool:
250 | aws:SecureTransport: 'false'
251 |
252 | # CodeBucket
253 | StreamlitCodeS3Bucket:
254 | Type: AWS::S3::Bucket
255 | Properties:
256 | LoggingConfiguration:
257 | DestinationBucketName: !Ref LoggingBucket
258 | LogFilePrefix: !Sub artifact-${AWS::StackName}-logs
259 | VersioningConfiguration:
260 | Status: Enabled
261 | PublicAccessBlockConfiguration:
262 | BlockPublicAcls: true
263 | BlockPublicPolicy: true
264 | IgnorePublicAcls: true
265 | RestrictPublicBuckets: true
266 | BucketEncryption:
267 | ServerSideEncryptionConfiguration:
268 | - ServerSideEncryptionByDefault:
269 | SSEAlgorithm: AES256
270 |
271 | StreamlitCodeS3BucketPolicy:
272 | Type: 'AWS::S3::BucketPolicy'
273 | Properties:
274 | Bucket: !Ref StreamlitCodeS3Bucket
275 | PolicyDocument:
276 | Version: '2012-10-17'
277 | Statement:
278 | - Action:
279 | - 's3:*'
280 | Effect: Deny
281 | Resource:
282 | - !Sub arn:aws:s3:::${StreamlitCodeS3Bucket}/*
283 | - !Sub arn:aws:s3:::${StreamlitCodeS3Bucket}
284 | Principal: "*"
285 | Condition:
286 | Bool:
287 | 'aws:SecureTransport': 'false'
288 |
289 | # CloudTrail Bucket
290 | StreamlitCloudTrailBucket:
291 | Type: AWS::S3::Bucket
292 | DeletionPolicy: Retain
293 | Properties:
294 | VersioningConfiguration:
295 | Status: Enabled
296 | PublicAccessBlockConfiguration:
297 | BlockPublicAcls: true
298 | BlockPublicPolicy: true
299 | IgnorePublicAcls: true
300 | RestrictPublicBuckets: true
301 | BucketEncryption:
302 | ServerSideEncryptionConfiguration:
303 | - ServerSideEncryptionByDefault:
304 | SSEAlgorithm: AES256
305 |
306 | StreamlitCloudTrailBucketPolicy:
307 | Type: AWS::S3::BucketPolicy
308 | Properties:
309 | Bucket: !Ref StreamlitCloudTrailBucket
310 | PolicyDocument:
311 | Version: 2012-10-17
312 | Statement:
313 | -
314 | Sid: AWSCloudTrailAclCheck
315 | Effect: Allow
316 | Principal:
317 | Service:
318 | - cloudtrail.amazonaws.com
319 | Action: s3:GetBucketAcl
320 | Resource: !GetAtt StreamlitCloudTrailBucket.Arn
321 | -
322 | Sid: AWSCloudTrailWrite
323 | Effect: Allow
324 | Principal:
325 | Service:
326 | - cloudtrail.amazonaws.com
327 | Action: s3:PutObject
328 | Resource: !Join [ '', [ !GetAtt StreamlitCloudTrailBucket.Arn, '/AWSLogs/', !Ref 'AWS::AccountId', '/*' ] ]
329 | Condition:
330 | StringEquals:
331 | s3:x-amz-acl: bucket-owner-full-control
332 | -
333 | Action:
334 | - 's3:*'
335 | Effect: Deny
336 | Resource:
337 | - !Sub arn:aws:s3:::${StreamlitCloudTrailBucket}/*
338 | - !Sub arn:aws:s3:::${StreamlitCloudTrailBucket}
339 | Principal: "*"
340 | Condition:
341 | Bool:
342 | 'aws:SecureTransport': 'false'
343 |
344 | StreamlitCloudTrail:
345 | DependsOn:
346 | - StreamlitCloudTrailBucketPolicy
347 | - StreamlitBuildCustomResource
348 | Type: AWS::CloudTrail::Trail
349 | Properties:
350 | S3BucketName: !Ref StreamlitCloudTrailBucket
351 | EventSelectors:
352 | -
353 | DataResources:
354 | -
355 | Type: AWS::S3::Object
356 | Values:
357 | - !Join [ '', [ !GetAtt StreamlitCodeS3Bucket.Arn, '/', "app.zip" ] ]
358 | ReadWriteType: WriteOnly
359 | IncludeManagementEvents: false
360 | IncludeGlobalServiceEvents: true
361 | IsLogging: true
362 | IsMultiRegionTrail: true
363 |
364 | ###################
365 | ##### Roles ######
366 | #################
367 |
368 | StreamlitCodeBuildExecutionRole:
369 | Type: 'AWS::IAM::Role'
370 | Properties:
371 | AssumeRolePolicyDocument:
372 | Version: '2012-10-17'
373 | Statement:
374 | - Effect: 'Allow'
375 | Principal:
376 | Service:
377 | - 'codebuild.amazonaws.com'
378 | Action:
379 | - 'sts:AssumeRole'
380 | ManagedPolicyArns:
381 | - !Ref LogsPolicy
382 | Policies:
383 | - PolicyName: 'CodeBuildPolicy'
384 | PolicyDocument:
385 | Version: '2012-10-17'
386 | Statement:
387 | - Effect: 'Allow'
388 | Action:
389 | - 'ecr:GetAuthorizationToken'
390 | Resource:
391 | - '*'
392 | - Effect: 'Allow'
393 | Action:
394 | - 'ecr:UploadLayerPart'
395 | - 'ecr:PutImage'
396 | - 'ecr:InitiateLayerUpload'
397 | - 'ecr:CompleteLayerUpload'
398 | - 'ecr:BatchCheckLayerAvailability'
399 | Resource:
400 | - !GetAtt StreamlitImageRepo.Arn
401 | - Effect: 'Allow'
402 | Action:
403 | - "s3:GetObject"
404 | - "s3:PutObject"
405 | Resource:
406 | - !Sub "arn:aws:s3:::${StreamlitArtifactStore}/*"
407 |
408 | StreamlitCodePipelineServiceRole:
409 | Type: AWS::IAM::Role
410 | Properties:
411 | AssumeRolePolicyDocument:
412 | Version: 2012-10-17
413 | Statement:
414 | -
415 | Effect: Allow
416 | Principal:
417 | Service:
418 | - codepipeline.amazonaws.com
419 | Action: sts:AssumeRole
420 | Path: /
421 | Policies:
422 | -
423 | PolicyName: AWS-CodePipeline-Service-3
424 | PolicyDocument:
425 | Version: 2012-10-17
426 | Statement:
427 | -
428 | Effect: Allow
429 | Action:
430 | - codebuild:BatchGetBuilds
431 | - codebuild:StartBuild
432 | Resource: !GetAtt StreamlitCodeBuild.Arn
433 | -
434 | Effect: Allow
435 | Action:
436 | - lambda:InvokeFunction
437 | - lambda:ListFunctions
438 | Resource: !GetAtt InvalidateCacheFunction.Arn
439 | -
440 | Effect: Allow
441 | Action:
442 | - iam:PassRole
443 | Resource: !GetAtt StreamlitCloudformationExecutionRole.Arn
444 | -
445 | Effect: Allow
446 | Action:
447 | - cloudformation:UpdateStack
448 | - cloudformation:DescribeStacks
449 | - cloudformation:CreateStack
450 | Resource: !Sub
451 | - |-
452 | arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${StackName}/*
453 | - {
454 | StackName: !Join ['', [!Sub '${AWS::StackName}', 'deploy']]
455 | }
456 | -
457 | Effect: Allow
458 | Action:
459 | - s3:*
460 | Resource:
461 | - !Sub arn:aws:s3:::${StreamlitCodeS3Bucket}/*
462 | - !Sub arn:aws:s3:::${StreamlitCodeS3Bucket}
463 | - !Sub arn:aws:s3:::${StreamlitArtifactStore}/*
464 | - !Sub arn:aws:s3:::${StreamlitArtifactStore}
465 |
466 | StreamlitCloudformationExecutionRole:
467 | DeletionPolicy: Retain
468 | UpdateReplacePolicy: Retain
469 | Type: AWS::IAM::Role
470 | Properties:
471 | AssumeRolePolicyDocument:
472 | Statement:
473 | - Effect: Allow
474 | Principal:
475 | Service: cloudformation.amazonaws.com
476 | Action: 'sts:AssumeRole'
477 | ManagedPolicyArns:
478 | - !Ref LogsPolicy
479 | Policies:
480 | - PolicyName: 'CloudFormationPolicy'
481 | PolicyDocument:
482 | Version: '2012-10-17'
483 | Statement:
484 | - Effect: 'Allow'
485 | Action:
486 | - 'iam:ListRolePolicies'
487 | - 'iam:ListAttachedRolePolicies'
488 | - 'iam:CreateServiceLinkedRole'
489 | - 'iam:CreateRole'
490 | - 'iam:GetRolePolicy'
491 | - 'iam:GetRole'
492 | - 'iam:AttachRolePolicy'
493 | - 'iam:PutRolePolicy'
494 | - 'iam:DetachRolePolicy'
495 | - 'iam:DeleteRole'
496 | - 'iam:DeleteRolePolicy'
497 | - 'iam:PassRole'
498 | - 'sts:AssumeRole'
499 | Resource:
500 | - !Sub arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}deploy-StreamlitExecutionRole*
501 | - !Sub arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}deploy-StreamlitECSTaskRole*
502 | - !Sub arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}deploy-ECSCustomRole*
503 | - !Sub arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}deploy-StreamlitAutoScalingRole*
504 | - !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/elasticloadbalancing.amazonaws.com/AWSServiceRoleForElasticLoadBalancing
505 | - !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService
506 | - Effect: 'Allow'
507 | Action:
508 | - 's3:GetBucketAcl'
509 | - 's3:PutBucketAcl'
510 | Resource:
511 | - !Sub
512 | - 'arn:aws:s3:::${LoggingBucket}'
513 | - LoggingBucket: !Ref LoggingBucket
514 | - !Sub
515 | - 'arn:aws:s3:::${LoggingBucket}/*'
516 | - LoggingBucket: !Ref LoggingBucket
517 | - Effect: 'Allow'
518 | Action:
519 | - 'ecs:DeregisterTaskDefinition'
520 | - 'ecs:RegisterTaskDefinition'
521 | Resource:
522 | - '*'
523 | - Effect: 'Allow'
524 | Action:
525 | - 'ecs:DescribeClusters'
526 | - 'ecs:DescribeServices'
527 | - 'ecs:CreateService'
528 | - 'ecs:UpdateService'
529 | - 'ecs:DeleteService'
530 | Resource:
531 | - !Sub
532 | - arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${StreamlitClusterName}
533 | - StreamlitClusterName: !If [IsDeployVPCInfrastructure, !GetAtt Infrastructure.Outputs.StreamlitCluster, !ImportValue StreamlitCluster]
534 | - !Sub
535 | - arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:service/${StreamlitClusterName}/${AWS::StackName}deploy-StreamlitECSService*
536 | - StreamlitClusterName: !If [IsDeployVPCInfrastructure, !GetAtt Infrastructure.Outputs.StreamlitCluster, !ImportValue StreamlitCluster]
537 | # - Effect: 'Allow'
538 | # Action:
539 | # - 'lambda:GetRuntimeManagementConfig'
540 | # - 'lambda:GetFunctionCodeSigningConfig'
541 | # - 'lambda:GetFunction'
542 | # - 'lambda:CreateFunction'
543 | # - 'lambda:DeleteFunction'
544 | # - 'lambda:InvokeFunction'
545 | # Resource:
546 | # - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:ECSCustomF-${EnvironmentName}
547 | - Effect: 'Allow'
548 | Action:
549 | - 'cloudfront:ListDistributions'
550 | Resource:
551 | - '*'
552 | - Effect: 'Allow'
553 | Action:
554 | - 'cloudfront:CreateDistribution'
555 | - 'cloudfront:GetDistribution'
556 | - 'cloudfront:DeleteDistribution'
557 | - 'cloudfront:UpdateDistribution'
558 | - 'cloudfront:TagResource'
559 | Resource:
560 | - '*'
561 | Condition:
562 | StringEquals:
563 | 'aws:ResourceTag/CloudfrontStreamlitApp': !Sub '${AWS::StackName}deploy-Cloudfront'
564 | - Effect: 'Allow'
565 | Action:
566 | - 'application-autoscaling:DescribeScalableTargets'
567 | - 'application-autoscaling:DescribeScalingPolicies'
568 | - 'application-autoscaling:RegisterScalableTarget'
569 | - 'application-autoscaling:DeregisterScalableTarget'
570 | Resource:
571 | - !Sub arn:aws:application-autoscaling:${AWS::Region}:${AWS::AccountId}:scalable-target/*
572 | - Effect: 'Allow'
573 | Action:
574 | - 'application-autoscaling:PutScalingPolicy'
575 | - 'application-autoscaling:DeleteScalingPolicy'
576 | Resource:
577 | - !Sub arn:aws:application-autoscaling:${AWS::Region}:${AWS::AccountId}:scalable-target/*
578 | - Effect: 'Allow'
579 | Action:
580 | - 'autoscaling:PutScalingPolicy'
581 | - 'autoscaling:DescribeScheduledActions'
582 | Resource:
583 | - '*'
584 | - Effect: 'Allow'
585 | Action:
586 | - 'wafv2:CreateWebACL'
587 | Resource:
588 | - '*'
589 | # - Effect: 'Allow'
590 | # Action:
591 | # - 'wafv2:GetWebACL'
592 | # - 'wafv2:DeleteWebACL'
593 | # - 'wafv2:ListTagsForResource'
594 | # Resource:
595 | # - !Sub arn:aws:wafv2:${AWS::Region}:${AWS::AccountId}:*/webacl/CloudFrontWebACL${EnvironmentName}/*
596 | - Effect: 'Allow'
597 | Action:
598 | - 'ec2:CreateSecurityGroup'
599 | - 'ec2:DescribeSecurityGroups'
600 | - 'ec2:CreateTags'
601 | - 'ec2:DescribeVpcs'
602 | - 'ec2:DescribeInternetGateways'
603 | - 'ec2:DescribeAccountAttributes'
604 | - 'ec2:DescribeSubnets'
605 | Resource:
606 | - "*"
607 | - Effect: 'Allow'
608 | Action:
609 | - 'ec2:DeleteSecurityGroup'
610 | - 'ec2:RevokeSecurityGroupIngress'
611 | - 'ec2:RevokeSecurityGroupEgress'
612 | - 'ec2:AuthorizeSecurityGroupIngress'
613 | - 'ec2:AuthorizeSecurityGroupEgress'
614 | Resource:
615 | - "*"
616 | Condition:
617 | StringEquals:
618 | 'aws:ResourceTag/Name': !Join ['', ['StreamlitALBSecurityGroup', !Sub "${AWS::StackName}deploy"]]
619 | - Effect: 'Allow'
620 | Action:
621 | - 'ec2:DeleteSecurityGroup'
622 | - 'ec2:RevokeSecurityGroupIngress'
623 | - 'ec2:RevokeSecurityGroupEgress'
624 | - 'ec2:AuthorizeSecurityGroupIngress'
625 | - 'ec2:AuthorizeSecurityGroupEgress'
626 | Resource:
627 | - "*"
628 | Condition:
629 | StringEquals:
630 | 'aws:ResourceTag/Name': !Join ['', ['StreamlitContainerSecurityGroup', !Sub "${AWS::StackName}deploy"]]
631 | - Effect: 'Allow'
632 | Action:
633 | - 'elasticloadbalancing:DeleteLoadBalancer'
634 | - 'elasticloadbalancing:DeleteListener'
635 | - 'elasticloadbalancing:DeleteRule'
636 | - 'elasticloadbalancing:DeleteTargetGroup'
637 | Resource:
638 | - !Sub
639 | -
640 | arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:targetgroup/${UniqueId}/*
641 | - {
642 | UniqueId: !Select [0, !Split ["-", !Select [2, !Split ["/", !Ref AWS::StackId]]]]
643 | }
644 | - !Sub
645 | -
646 | arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:loadbalancer/app/${UniqueId}/*
647 | - {
648 | UniqueId: !Select [0, !Split ["-", !Select [2, !Split ["/", !Ref AWS::StackId]]]]
649 | }
650 | - !Sub
651 | -
652 | arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:listener/app/${UniqueId}/*/*
653 | - {
654 | UniqueId: !Select [0, !Split ["-", !Select [2, !Split ["/", !Ref AWS::StackId]]]]
655 | }
656 | - !Sub
657 | -
658 | arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:listener-rule/app/${UniqueId}/*/*/*
659 | - {
660 | UniqueId: !Select [0, !Split ["-", !Select [2, !Split ["/", !Ref AWS::StackId]]]]
661 | }
662 | - Effect: 'Allow'
663 | Action:
664 | - 'elasticloadbalancing:CreateLoadBalancer'
665 | - 'elasticloadbalancing:CreateListener'
666 | - 'elasticloadbalancing:CreateRule'
667 | - 'elasticloadbalancing:CreateTargetGroup'
668 | - 'elasticloadbalancing:DescribeTargetGroups'
669 | - 'elasticloadbalancing:DescribeListeners'
670 | - 'elasticloadbalancing:DescribeLoadBalancers'
671 | - 'elasticloadbalancing:DescribeRules'
672 | - 'elasticloadbalancing:ModifyLoadBalancerAttributes'
673 | - 'elasticloadbalancing:ModifyTargetGroup'
674 | - 'elasticloadbalancing:ModifyTargetGroupAttributes'
675 | Resource:
676 | - "*"
677 | - Effect: 'Allow'
678 | Action:
679 | - 'iam:CreateServiceLinkedRole'
680 | - 'iam:AttachRolePolicy'
681 | - 'iam:PutRolePolicy'
682 | - 'sts:AssumeRole'
683 | Resource:
684 | - !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService
685 |
686 | ##############################
687 | ##### Docker CodeBuild ######
688 | ############################
689 |
690 | StreamlitCodeBuild:
691 | Type: AWS::CodeBuild::Project
692 | Properties:
693 | Description: CodeBuild for Code Pipeline
694 | Cache:
695 | Location: LOCAL
696 | Modes:
697 | - LOCAL_SOURCE_CACHE
698 | - LOCAL_DOCKER_LAYER_CACHE
699 | Type: LOCAL
700 | Artifacts:
701 | Type: CODEPIPELINE
702 | Source:
703 | Type: CODEPIPELINE
704 | BuildSpec:
705 | !Sub
706 | - |
707 | version: 0.2
708 | phases:
709 | pre_build:
710 | commands:
711 | - pip3 install awscli
712 | - aws ecr get-login-password --region ${AWS::Region} | docker login --username AWS --password-stdin ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com
713 | - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
714 | - COMMIT_HASH=${!COMMIT_HASH//./a}
715 | - IMAGE_TAG=${!COMMIT_HASH:=latest}
716 | build:
717 | commands:
718 | - echo Build started on `date`
719 | - docker build -t ${StreamlitImageRepo} .
720 | - docker tag ${StreamlitImageRepo}:latest ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${StreamlitImageRepo}:$IMAGE_TAG
721 | post_build:
722 | commands:
723 | - echo Build completed on `date`
724 | - docker push ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${StreamlitImageRepo}:$IMAGE_TAG
725 | - printf '{"StreamLitImageURI":"%s"}' ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${StreamlitImageRepo}:$IMAGE_TAG > imageDetail.json
726 | artifacts:
727 | files:
728 | - imageDetail.json
729 | - {
730 | StreamlitImageRepo: !Ref StreamlitImageRepo
731 | }
732 | Environment:
733 | Type: LINUX_CONTAINER
734 | Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
735 | ComputeType: BUILD_GENERAL1_SMALL
736 | ServiceRole: !GetAtt StreamlitCodeBuildExecutionRole.Arn
737 | TimeoutInMinutes: 10
738 |
739 | StreamlitCodeBuildLogGroup:
740 | Type: AWS::Logs::LogGroup
741 | UpdateReplacePolicy: Retain
742 | DeletionPolicy: Retain
743 | Properties:
744 | RetentionInDays: 7
745 |
746 | #######################################
747 | ##### Invalidate cache function ######
748 | #####################################
749 |
750 | InvalidateCacheFunctionRole:
751 | Type: AWS::IAM::Role
752 | Properties:
753 | AssumeRolePolicyDocument:
754 | Version: '2012-10-17'
755 | Statement:
756 | - Effect: 'Allow'
757 | Principal:
758 | Service:
759 | - lambda.amazonaws.com
760 | Action:
761 | - 'sts:AssumeRole'
762 | Path: "/"
763 | ManagedPolicyArns:
764 | - !Ref LogsPolicy
765 | Policies:
766 | - PolicyName: LambdaCustomPolicy
767 | PolicyDocument:
768 | Version: '2012-10-17'
769 | Statement:
770 | - Effect: Allow
771 | Action:
772 | - codepipeline:PutJobFailureResult
773 | - codepipeline:PutJobSuccessResult
774 | - cloudfront:CreateInvalidation
775 | Resource:
776 | - '*'
777 | - Effect: Allow
778 | Action:
779 | - s3:GetObject
780 | - s3:GetObjectAcl
781 | - s3:ListBucket
782 | Resource:
783 | - !Sub "arn:aws:s3:::${StreamlitArtifactStore}/*"
784 |
785 | InvalidateCacheFunction:
786 | Type: "AWS::Lambda::Function"
787 | Properties:
788 | Handler: index.handler
789 | Role: !GetAtt InvalidateCacheFunctionRole.Arn
790 | Timeout: 300
791 | Runtime: python3.12
792 | Code:
793 | ZipFile: !Sub |
794 | import json
795 | import boto3
796 | import zipfile
797 | import os
798 |
799 | code_pipeline = boto3.client("codepipeline")
800 | cloud_front = boto3.client("cloudfront")
801 | s3 = boto3.client('s3')
802 |
803 | def get_input_artifacts(inputArtifacts):
804 | bucketName = inputArtifacts["location"]["s3Location"]["bucketName"]
805 | objectKey = inputArtifacts["location"]["s3Location"]["objectKey"]
806 |
807 | s3.download_file(bucketName, objectKey, "/tmp/file.zip")
808 |
809 | with zipfile.ZipFile("/tmp/file.zip", 'r') as zip_ref:
810 | zip_ref.extractall("/tmp/extracted")
811 |
812 | json_file_path = os.path.join("/tmp/extracted", 'CreateStackOutput.json')
813 | with open(json_file_path, 'r') as json_file:
814 | json_data = json.loads(json_file.read())
815 | # You can now use json_data as needed
816 | return json_data["CloudfrontID"]
817 |
818 |
819 | def handler(event, context):
820 | job_id = event["CodePipeline.job"]["id"]
821 | try:
822 | CloudfrontID = get_input_artifacts(event["CodePipeline.job"]["data"]["inputArtifacts"][0])
823 |
824 | cloud_front.create_invalidation(
825 | DistributionId=CloudfrontID,
826 | InvalidationBatch={
827 | "Paths": {
828 | "Quantity": 1,
829 | "Items": ["/*"],
830 | },
831 | "CallerReference": event["CodePipeline.job"]["id"],
832 | },
833 | )
834 | except Exception as e:
835 | code_pipeline.put_job_failure_result(
836 | jobId=job_id,
837 | failureDetails={
838 | "type": "JobFailed",
839 | "message": str(e),
840 | },
841 | )
842 | else:
843 | code_pipeline.put_job_success_result(
844 | jobId=job_id,
845 | )
846 |
847 | #########################################
848 | ##### Infrastructure CodePipeline ######
849 | #######################################
850 |
851 | StreamlitCodePipeLine:
852 | Type: AWS::CodePipeline::Pipeline
853 | DependsOn: StreamlitBuildCustomResource
854 | Properties:
855 | ArtifactStore:
856 | Location: !Ref StreamlitArtifactStore
857 | Type: S3
858 | RestartExecutionOnUpdate: False
859 | RoleArn: !GetAtt StreamlitCodePipelineServiceRole.Arn
860 | Stages:
861 | - Name: Source
862 | Actions:
863 | - Name: SourceAction
864 | ActionTypeId:
865 | Category: Source
866 | Owner: AWS
867 | Provider: S3
868 | Version: 1
869 | Configuration:
870 | S3Bucket: !Ref StreamlitCodeS3Bucket
871 | S3ObjectKey: app.zip
872 | PollForSourceChanges: false
873 | RunOrder: 1
874 | OutputArtifacts:
875 | - Name: source-output-artifacts
876 | # Build the project using the BuildProject and Output build artifacts to build-output-artifacts path in S3 Bucket
877 | - Name: Build
878 | Actions:
879 | - Name: Build
880 | ActionTypeId:
881 | Category: Build
882 | Owner: AWS
883 | Version: 1
884 | Provider: CodeBuild
885 | OutputArtifacts:
886 | - Name: build-output-artifacts
887 | InputArtifacts:
888 | - Name: source-output-artifacts
889 | Configuration:
890 | ProjectName: !Ref StreamlitCodeBuild
891 | RunOrder: 1
892 |
893 | # Deploy the project by executing Fargate-Cluster.yml file in the Source code with Cloudformation.
894 | - Name: InfrastructureDeploy
895 | Actions:
896 | - Name: Deploy
897 | ActionTypeId:
898 | Category: Deploy
899 | Owner: AWS
900 | Version: 1
901 | Provider: CloudFormation
902 | InputArtifacts:
903 | - Name: source-output-artifacts
904 | - Name: build-output-artifacts
905 | OutputArtifacts:
906 | - Name: cfn-output-artifacts
907 | Configuration:
908 | OutputFileName: CreateStackOutput.json
909 | ActionMode: CREATE_UPDATE
910 | Capabilities: CAPABILITY_NAMED_IAM
911 | ParameterOverrides: !Sub
912 | - |
913 | {"StreamLitImageURI" : { "Fn::GetParam" : ["build-output-artifacts", "imageDetail.json", "StreamLitImageURI"] },"StreamlitCluster": "${Cluster}", "Cpu": "${Cpu}", "Memory":"${Memory}","Task":"${DesiredTaskCount}","Min":"${MinContainers}","Max":"${MaxContainers}","AutoScalingTargetValue":"${AutoScalingTargetValue}","StreamlitPublicSubnetA": "${PubSubnetA}","StreamlitPublicSubnetB": "${PubSubnetB}","StreamlitPrivateSubnetA": "${PvtSubnetA}","StreamlitPrivateSubnetB": "${PvtSubnetB}","UniqueId": "${UniqueId}", "LoggingBucketName": "${LoggingBucketName}","StreamlitVPC": "${VPC}"},
914 | - {
915 | Cluster: !If [IsDeployVPCInfrastructure, !GetAtt Infrastructure.Outputs.StreamlitCluster, !ImportValue StreamlitCluster],
916 | PubSubnetA: !If [IsDeployVPCInfrastructure, !GetAtt Infrastructure.Outputs.PublicSubnetA, !ImportValue Basic-PublicSubnetA],
917 | PubSubnetB: !If [IsDeployVPCInfrastructure, !GetAtt Infrastructure.Outputs.PublicSubnetB, !ImportValue Basic-PublicSubnetB],
918 | PvtSubnetA: !If [IsDeployVPCInfrastructure, !GetAtt Infrastructure.Outputs.PrivateSubnetA, !ImportValue Basic-PrivateSubnetA],
919 | PvtSubnetB: !If [IsDeployVPCInfrastructure, !GetAtt Infrastructure.Outputs.PrivateSubnetB, !ImportValue Basic-PrivateSubnetB],
920 | LoggingBucketName: !Ref LoggingBucket,
921 | UniqueId: !Select [0, !Split ["-", !Select [2, !Split ["/", !Ref AWS::StackId]]]],
922 | VPC: !If [IsDeployVPCInfrastructure, !GetAtt Infrastructure.Outputs.VPC, !ImportValue Basic-VPC]
923 | }
924 | RoleArn:
925 | !GetAtt StreamlitCloudformationExecutionRole.Arn
926 | StackName: !Join ['', [!Sub '${AWS::StackName}', 'deploy']]
927 | TemplatePath: source-output-artifacts::cfn_stack/pipeline/deploy.yaml
928 | RunOrder: 1
929 | - Name: InvalidateCache
930 | Actions:
931 | - Name: Invalidate
932 | ActionTypeId:
933 | Category: Invoke
934 | Owner: AWS
935 | Version: 1
936 | Provider: Lambda
937 | InputArtifacts:
938 | - Name: cfn-output-artifacts
939 | Configuration:
940 | FunctionName: !Ref InvalidateCacheFunction
941 | RunOrder: 1
942 |
943 |
944 | ########################
945 | ##### CloudWatch ######
946 | ######################
947 |
948 | StreamlitCloudWatchEventRole:
949 | Type: AWS::IAM::Role
950 | Properties:
951 | AssumeRolePolicyDocument:
952 | Version: 2012-10-17
953 | Statement:
954 | -
955 | Effect: Allow
956 | Principal:
957 | Service:
958 | - events.amazonaws.com
959 | Action: sts:AssumeRole
960 | Path: /
961 | Policies:
962 | -
963 | PolicyName: cwe-pipeline-execution
964 | PolicyDocument:
965 | Version: 2012-10-17
966 | Statement:
967 | -
968 | Effect: Allow
969 | Action: codepipeline:StartPipelineExecution
970 | Resource: !Join [ '', [ 'arn:aws:codepipeline:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref StreamlitCodePipeLine] ]
971 |
972 | AmazonCloudWatchEventRule:
973 | Type: AWS::Events::Rule
974 | Properties:
975 | EventPattern:
976 | source:
977 | - aws.s3
978 | detail-type:
979 | - 'AWS API Call via CloudTrail'
980 | detail:
981 | eventSource:
982 | - s3.amazonaws.com
983 | eventName:
984 | - PutObject
985 | - CompleteMultipartUpload
986 | resources:
987 | ARN:
988 | - !Join [ '', [ !GetAtt StreamlitCodeS3Bucket.Arn, '/', "app.zip" ] ]
989 | Targets:
990 | -
991 | Arn:
992 | !Join [ '', [ 'arn:aws:codepipeline:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref StreamlitCodePipeLine]]
993 | RoleArn: !GetAtt StreamlitCloudWatchEventRole.Arn
994 | Id: codepipeline-AppPipeline
995 |
996 | ####################################################################
997 | ##### Initialize CodeS3Bucket with infrastructure and app.zip #####
998 | ##################################################################
999 |
1000 | StreamlitInitBuildRole:
1001 | Type: 'AWS::IAM::Role'
1002 | Properties:
1003 | AssumeRolePolicyDocument:
1004 | Version: '2012-10-17'
1005 | Statement:
1006 | - Effect: 'Allow'
1007 | Principal:
1008 | Service:
1009 | - 'codebuild.amazonaws.com'
1010 | Action:
1011 | - 'sts:AssumeRole'
1012 | Policies:
1013 | - PolicyName: 'S3PutObject'
1014 | PolicyDocument:
1015 | Version: '2012-10-17'
1016 | Statement:
1017 | - Effect: 'Allow'
1018 | Action:
1019 | - "s3:PutObject"
1020 | - "s3:PutObjectAcl"
1021 | Resource:
1022 | - !Sub "arn:aws:s3:::${StreamlitCodeS3Bucket}/*"
1023 | - Effect: 'Allow'
1024 | Action:
1025 | - 'logs:CreateLogGroup'
1026 | - 'logs:CreateLogStream'
1027 | - 'logs:PutLogEvents'
1028 | - 'logs:PutRetentionPolicy'
1029 | Resource: '*'
1030 |
1031 | StreamlitInitCodebuild:
1032 | Type: AWS::CodeBuild::Project
1033 | Properties:
1034 | Source:
1035 | Type: GITHUB
1036 | Location: !Ref GitURL
1037 | BuildSpec:
1038 | !Sub
1039 | - |
1040 | version: 0.2
1041 | phases:
1042 | pre_build:
1043 | commands:
1044 | - pip3 install awscli --upgrade --user
1045 | build:
1046 | commands:
1047 | - echo Build started on `date`
1048 | - aws s3 cp cfn_stack/pipeline/infrastructure.yaml s3://${StreamlitCodeS3Bucket}
1049 | - zip -r app.zip .
1050 | post_build:
1051 | commands:
1052 | - echo Build completed on `date`
1053 | - aws s3 cp app.zip s3://${StreamlitCodeS3Bucket}
1054 | - {
1055 | StreamlitCodeS3Bucket: !Ref StreamlitCodeS3Bucket
1056 | }
1057 | # SourceVersion: branch
1058 | Environment:
1059 | Type: LINUX_CONTAINER
1060 | Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
1061 | ComputeType: BUILD_GENERAL1_SMALL
1062 | ServiceRole: !GetAtt StreamlitInitBuildRole.Arn
1063 | Artifacts:
1064 | Type: NO_ARTIFACTS
1065 |
1066 |
1067 | ###################################
1068 | ##### Start Docker CodeBuild #####
1069 | #################################
1070 |
1071 | StreamlitBuildCustomResourceRole:
1072 | Type: AWS::IAM::Role
1073 | Properties:
1074 | AssumeRolePolicyDocument:
1075 | Version: '2012-10-17'
1076 | Statement:
1077 | - Effect: 'Allow'
1078 | Principal:
1079 | Service:
1080 | - lambda.amazonaws.com
1081 | Action:
1082 | - 'sts:AssumeRole'
1083 | Path: "/"
1084 | Policies:
1085 | - PolicyName: LambdaCustomPolicy
1086 | PolicyDocument:
1087 | Version: '2012-10-17'
1088 | Statement:
1089 | - Effect: Allow
1090 | Action:
1091 | - codebuild:StartBuild
1092 | - codebuild:BatchGetBuilds
1093 | Resource:
1094 | - !GetAtt StreamlitInitCodebuild.Arn
1095 | - Effect: 'Allow'
1096 | Action:
1097 | - 'logs:CreateLogGroup'
1098 | - 'logs:CreateLogStream'
1099 | - 'logs:PutLogEvents'
1100 | - 'logs:PutRetentionPolicy'
1101 | Resource: '*'
1102 | - Effect: Allow
1103 | Action:
1104 | - s3:ListBucket
1105 | - s3:DeleteObject
1106 | - s3:DeleteObjectVersion
1107 | - s3:ListBucketVersions
1108 | Resource:
1109 | - !Sub arn:aws:s3:::${StreamlitCodeS3Bucket}/*
1110 | - !Sub arn:aws:s3:::${StreamlitCodeS3Bucket}
1111 |
1112 | StreamlitBuildCustomResourceFunction:
1113 | Type: "AWS::Lambda::Function"
1114 | Properties:
1115 | Handler: index.handler
1116 | Role: !GetAtt StreamlitBuildCustomResourceRole.Arn
1117 | Timeout: 300
1118 | Runtime: python3.12
1119 | Code:
1120 | ZipFile: !Sub |
1121 | import boto3
1122 | from time import sleep
1123 | import cfnresponse
1124 |
1125 | codebuild = boto3.client("codebuild")
1126 |
1127 | def handler(event, context):
1128 | try:
1129 | request_type = event['RequestType']
1130 | if request_type == 'Create':
1131 | status = 'STARTING'
1132 |
1133 | build_id = codebuild.start_build(projectName=event['ResourceProperties']['PROJECT'])['build']['id']
1134 | while status not in ['SUCCEEDED', 'FAILED', 'STOPPED', 'FAULT', 'TIMED_OUT']:
1135 | status = codebuild.batch_get_builds(ids=[build_id])['builds'][0]['buildStatus']
1136 | sleep(15)
1137 | if status in ['FAILED', 'STOPPED', 'FAULT', 'TIMED_OUT']:
1138 | print("Initial CodeBuild failed")
1139 | cfnresponse.send(event, context, cfnresponse.FAILED, {})
1140 | return
1141 | elif request_type == 'Delete':
1142 | bucket = boto3.resource("s3").Bucket(event['ResourceProperties']['CODEBUCKET'])
1143 | bucket.object_versions.delete()
1144 | bucket.objects.all().delete()
1145 | except Exception as ex:
1146 | print(ex)
1147 | bucket = boto3.resource("s3").Bucket(event['ResourceProperties']['CODEBUCKET'])
1148 | bucket.object_versions.delete()
1149 | bucket.objects.all().delete()
1150 | cfnresponse.send(event, context, cfnresponse.FAILED, {})
1151 | else:
1152 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
1153 |
1154 | StreamlitBuildCustomResource:
1155 | Type: Custom::BuildCode
1156 | DependsOn: StreamlitECSRoleCustomResource
1157 | Properties:
1158 | ServiceToken: !GetAtt StreamlitBuildCustomResourceFunction.Arn
1159 | PROJECT: !Ref StreamlitInitCodebuild
1160 | CODEBUCKET: !Ref StreamlitCodeS3Bucket
1161 |
1162 | ################################
1163 | ##### ECS Custom Resource #####
1164 | ##############################
1165 |
1166 | StreamlitECSRoleCustomResourceRole:
1167 | Type: AWS::IAM::Role
1168 | Properties:
1169 | AssumeRolePolicyDocument:
1170 | Version: '2012-10-17'
1171 | Statement:
1172 | - Effect: 'Allow'
1173 | Principal:
1174 | Service:
1175 | - lambda.amazonaws.com
1176 | Action:
1177 | - 'sts:AssumeRole'
1178 | Path: "/"
1179 | ManagedPolicyArns:
1180 | - !Ref LogsPolicy
1181 | Policies:
1182 | - PolicyName: IAMPolicy
1183 | PolicyDocument:
1184 | Version: '2012-10-17'
1185 | Statement:
1186 | - Effect: Allow
1187 | Action:
1188 | - iam:ListRoles
1189 | Resource:
1190 | - "*"
1191 | - Effect: Allow
1192 | Action:
1193 | - iam:GetRole
1194 | - iam:CreateServiceLinkedRole
1195 | - iam:AttachRolePolicy
1196 | Resource:
1197 | - "*"
1198 |
1199 | StreamlitECSRoleCustomResourceFunction:
1200 | Type: "AWS::Lambda::Function"
1201 | Properties:
1202 | Handler: index.handler
1203 | Role: !GetAtt StreamlitECSRoleCustomResourceRole.Arn
1204 | Timeout: 300
1205 | Runtime: python3.12
1206 | Code:
1207 | ZipFile: !Sub |
1208 | import boto3
1209 | from botocore.exceptions import ClientError
1210 | import cfnresponse
1211 | iam_client = boto3.client('iam')
1212 |
1213 | def handler(event, context):
1214 |
1215 | try:
1216 | request_type = event['RequestType']
1217 | print(request_type)
1218 |
1219 | if request_type == 'Create':
1220 | desired_ecs_role_name = "AWSServiceRoleForECS"
1221 | desired_ecs_scaling_role_name = "AWSServiceRoleForApplicationAutoScaling_ECSService"
1222 |
1223 | try:
1224 | iam_client.get_role(RoleName=desired_ecs_role_name)
1225 | ecs_role_exists = True
1226 | except ClientError as e:
1227 | if e.response['Error']['Code'] == 'NoSuchEntity':
1228 | ecs_role_exists = False
1229 | else:
1230 | ecs_role_exists = True
1231 |
1232 | try:
1233 | iam_client.get_role(RoleName=desired_ecs_scaling_role_name)
1234 | ecs_scaling_role_exists = True
1235 | except ClientError as e:
1236 | if e.response['Error']['Code'] == 'NoSuchEntity':
1237 | ecs_scaling_role_exists = False
1238 | else:
1239 | ecs_scaling_role_exists = True
1240 |
1241 | print(f"ECS service role exist? {ecs_role_exists}")
1242 | if not ecs_role_exists:
1243 | iam_client.create_service_linked_role(AWSServiceName="ecs.amazonaws.com")
1244 |
1245 | print(f"ECS scaling service role exist? {ecs_scaling_role_exists}")
1246 | if not ecs_scaling_role_exists:
1247 | iam_client.create_service_linked_role(AWSServiceName="ecs.application-autoscaling.amazonaws.com")
1248 |
1249 | except Exception as ex:
1250 | print(ex)
1251 | cfnresponse.send(event, context, cfnresponse.FAILED, {})
1252 | else:
1253 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
1254 |
1255 | StreamlitECSRoleCustomResource:
1256 | Type: Custom::ECSRole
1257 | Properties:
1258 | ServiceToken: !GetAtt StreamlitECSRoleCustomResourceFunction.Arn
1259 |
1260 | ####################
1261 | ##### CleanUp #####
1262 | ##################
1263 |
1264 | StreamlitCleanCustomResourceRole:
1265 | Type: AWS::IAM::Role
1266 | Properties:
1267 | AssumeRolePolicyDocument:
1268 | Version: '2012-10-17'
1269 | Statement:
1270 | - Effect: 'Allow'
1271 | Principal:
1272 | Service:
1273 | - lambda.amazonaws.com
1274 | Action:
1275 | - 'sts:AssumeRole'
1276 | Path: "/"
1277 | ManagedPolicyArns:
1278 | - !Ref LogsPolicy
1279 | Policies:
1280 | - PolicyName: LambdaCustomPolicy
1281 | PolicyDocument:
1282 | Version: '2012-10-17'
1283 | Statement:
1284 | - Effect: Allow
1285 | Action:
1286 | - s3:ListBucket
1287 | - s3:DeleteObject
1288 | - s3:DeleteObjectVersion
1289 | - s3:ListBucketVersions
1290 | Resource:
1291 | - !Sub arn:aws:s3:::${StreamlitCodeS3Bucket}/*
1292 | - !Sub arn:aws:s3:::${StreamlitCodeS3Bucket}
1293 | - !Sub arn:aws:s3:::${StreamlitArtifactStore}/*
1294 | - !Sub arn:aws:s3:::${StreamlitArtifactStore}
1295 | - !Sub arn:aws:s3:::${StreamlitCloudTrailBucket}/*
1296 | - !Sub arn:aws:s3:::${StreamlitCloudTrailBucket}
1297 | - Effect: Allow
1298 | Action:
1299 | - cloudformation:DeleteStack
1300 | Resource:
1301 | - !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}deploy/*"
1302 |
1303 | StreamlitCleanCustomResourceFunction:
1304 | Type: "AWS::Lambda::Function"
1305 | Properties:
1306 | Handler: index.handler
1307 | Role: !GetAtt StreamlitCleanCustomResourceRole.Arn
1308 | Timeout: 300
1309 | Runtime: python3.12
1310 | Code:
1311 | ZipFile: !Sub
1312 | - |
1313 | import boto3
1314 | from time import sleep
1315 | import cfnresponse
1316 | from botocore.exceptions import ClientError
1317 |
1318 | cfn = boto3.client("cloudformation")
1319 |
1320 | def handler(event, context):
1321 | try:
1322 | request_type = event['RequestType']
1323 | if request_type == 'Delete':
1324 | bucket = boto3.resource("s3").Bucket(event['ResourceProperties']['CODEBUCKET'])
1325 | bucket.object_versions.delete()
1326 | bucket.objects.all().delete()
1327 |
1328 | bucket = boto3.resource("s3").Bucket(event['ResourceProperties']['ARTIFACTBUCKET'])
1329 | bucket.object_versions.delete()
1330 | bucket.objects.all().delete()
1331 |
1332 | bucket = boto3.resource("s3").Bucket(event['ResourceProperties']['TRAILBUCKET'])
1333 | bucket.object_versions.delete()
1334 | bucket.objects.all().delete()
1335 | stack_name="${StackName}"
1336 | try:
1337 | data = cfn.delete_stack(StackName=stack_name)
1338 | print(f"Deleting stack {stack_name}")
1339 | except ClientError as e:
1340 | if e.response['Error']['Code'] == 'ValidationError' and 'does not exist' in e.response['Error']['Message']:
1341 | print(f"Stack doesn't exist. No action taken.")
1342 | else:
1343 | raise
1344 | except Exception as ex:
1345 | print(ex)
1346 | cfnresponse.send(event, context, cfnresponse.FAILED, {})
1347 | else:
1348 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
1349 | - {
1350 | StackName: !Join ['', [!Sub '${AWS::StackName}', 'deploy']]
1351 | }
1352 |
1353 | StreamlitCleanCustomResource:
1354 | Type: Custom::BuildCode
1355 | Properties:
1356 | ServiceToken: !GetAtt StreamlitCleanCustomResourceFunction.Arn
1357 | CODEBUCKET: !Ref StreamlitCodeS3Bucket
1358 | ARTIFACTBUCKET: !Ref StreamlitArtifactStore
1359 | TRAILBUCKET: !Ref StreamlitCloudTrailBucket
1360 |
1361 | Outputs:
1362 | StreamlitCodeS3Bucket:
1363 | Value: !Ref StreamlitCodeS3Bucket
1364 | Description: Name of code S3 bucket
1365 |
--------------------------------------------------------------------------------