├── 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 | ![architecture-cicd](/architecture-cicd.png) 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 | [![launch-stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](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 | [![launch-stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](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 | ![architecture-development](/architecture-development.png) 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 | [![launch-stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](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 | [![launch-stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](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 | [![launch-stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](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 | [![launch-stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](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 | --------------------------------------------------------------------------------