Deploying Flutter Web to S3 with CodeCommit, Codepipeline, Codebuild, and CodeDeploy
Tags: flutter, cicd, codepipeline, s3
Published: 2024-06-19 17:56:27

For this exercise, we are going to perform a step-by-step process of developing a web application with Flutter, starting with creating a Flutter application, setting up the environment variables, setting up the git repository, and configuring the CI/CD pipeline for Flutter Web S3 deployment.

## Step 1: Create a Flutter App
First create your flutter application by running this command:
```terminal
flutter create yourapplication
```
Navigate to your application then run `flutter run`.

In this application, we are going to use environment variables so that we will be able to input dynamic data during our CI/CD build. In this case, we are using the envied package. To install, run the following commands:
```terminal
flutter pub add envied
flutter pub add --dev envied_generator
flutter pub add --dev build_runner
```
After installation our pubspec.yml should look like this.
```yml
name: flutter_app_s3
description: "A new Flutter project."
publish_to: "none"
version: 1.0.0+1
environment:
  sdk: ">=3.3.3 <4.0.0"
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.6
  envied: ^0.5.4+1
dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.9
  flutter_lints: ^3.0.0
  envied_generator: ^0.5.4+1
flutter:
  uses-material-design: true
```
### Why use environment variable?
Our Flutter application may have different environments, like development, testing, and production environments, depending on where it is deployed. Different environments may have their own API urls and other variables that are unique to each environment.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s2bh0mjl6mkniklopf3v.png)

Our application might consume an API that has a URL for the development, test, and production environments. The API URL on our application **must not be hardcoded**; it must be stored in an environment variable so that we will be able to change its value depending on the environment where we want to deploy it.
Example:
* Dev environment: `APIBASEURL="https://dev.yourapiurl.com/"`
* Test environment: `APIBASEURL="https://test.yourapiurl.com/"`
* Prod environment: `APIBASEURL="https://api.yourapiurl.com/"`

Inside the root of our application, create a file `.env` then set the value to:
```
APIBASEURL=https://dev.yourapiurl.com/
```
Inside *lib/env/* create a file `env.dart` then set the value to:
```dart
import 'package:envied/envied.dart';

part 'env.g.dart';

@Envied(path: '.env')
abstract class Env {
  @EnviedField(varName: 'APIBASEURL', obfuscate: true)
  static String apiBaseUrl = _Env.apiBaseUrl ;
}
```
Now run:
```terminal
dart run build_runner build
```
This will generate a file `env.g.dart` alongside `env.dart` we previously created.

Now we are ready to use the environment variable to our flutter application.
Inside our `main.dart` Statefull widget, import the `env.dart`:
```dart
import 'package:flutter_app_s3/env/env.dart';
```
To get the value from our environment variable we can use this:
```dart
String apiBaseUrl = Env.apiBaseUrl ;
```
To display the value: (add this to your UI)
```dart
Text('Environment Variable: $apiBaseUrl '),
```

## Step 2: Create a CodeCommit
This exercise uses CodeCommit as a git repository, but you can use your own, like Github, BitBucket, etc.
* Go to *CodeCommit* then click Create repository
* Set the name ex: *flutter-app-s3*
* Open your repository click Clone URL (this will copy the url)
* On your local machine run `git clone pastetheurlhere`
* Enter your CodeCommit credentials (Check on documentation if you don't have one)
* On the root folder of your flutter project, run
```terminal
git add .
git commit -m "first commit"
git push
```

Create 3 branches that will be used for our environments (dev, test, prod), run the following commands:
```terminal
// Create a dev branch
git checkout -b "dev"
git push

// Create a test branch
git checkout master // go back to master branch
git checkout -b "test"
git push

// Create a production branch
git checkout master
git checkout -b "prod"
git push
git checkout master
```
These branches will be used in our CodePipeline for different types of environments. Every time there is a change detected in any of those branches, a deployment process will be triggered.

## Step 3: Create an S3 bucket
this is where we are going to host our application.
* Go to S3 then click *Create a bucket*
* Add your S3 bucket name ex: *flutter-app-s3-dev*
* Uncheck *Block all public access*
* Disable *Bucket Versioning*
* Click *Create Bucket*
* Open your bucket name created
* Go to *properties*
* Go to *Static website hosting* then click Edit then set these settings. (You can change these settings if you want to add attach a domain name)
	- **Static website**: Enabled
	- **Hosting type**: Host a static website
	- **Index document**: index.html
	- **Redirection rules:** (leave empty for now)
	- **Save changes**
* Go to *Permissions* then edit *Bucket policy*, this will allow our application to be accessed publicly. Note: replace yourbucketname with the bucket name you created ex: *flutter-app-s3-dev*
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::flutter-app-s3-dev/*"
        }
    ]
}
```

## Step 4: Create a CodeBuild
This will perform the build command for our application. (This will perform the build command we can do locally.) To make sure that build doesn't fail on the build process, run "flutter build web" on your local machine to make sure that the build command will also run successfully on the CI/CD pipeline.

**# Create project:**
* Go to CodeBuild then click **Create project**
* Set your project name ex: *flutter-app-s3-codebuild-dev*

**# Select your git repository and specific branch for this CodeBuild:**
* On source set to *AWS CodeCommit*
* Select repository we previously created
* Select specific branch where this code build pulls its source code. (make sure that codecommit repository have 4 primary branched **master, dev, test, prod**)
* if you are building a CodeBuild for development environment, use dev branch. (Note: You have to create one CodeBuild for each environment)

**# Environment settings:**
* **Provisioning model:** On Demand
* **Environment image:** Managed image
* **Compute:** EC2
* **Operating System:** Ubuntu
* **Runtime:** Standard
* **Image:** aws/codebuild/standard:7.0
* **Image version:** Always use the latest image for this runtime version
* **Service role:** (you can create new or use existing 
one(manually created))
* **Additional Configuration:** (expand this)
Set your environment variables (this must correspond to the environment variables we used on our flutter application)

The environment variable created here will be injected into our application during the build stage. Note: Make sure that you input the correct environment variables and their values. In our case, we only created one environment variable, which we can set like this:.

```
Name: APIBASEURL
Value: https://dev.yourbaseurl.com/
Type: Plaintext
```
Note: you can add as many environment variables as you want

**# Set the buildspec will be used for this CodeBuild:**
* Buildspec set to **Use a buildspec file**
	* Buildspec name: buildspec.yml
* Artifacts set to **No Artifact**
* Leave everything default then click **Create build project**

## Step 5: Create a buildspec
Inside the root folder of our flutter application create a *buildspec.yml*. Copy and paste the following script:
```yml
version: 0.2
phases:
  pre_build:
    commands:
      - echo Pre Build started on `date`
      - git clone https://github.com/flutter/flutter.git -b stable
      - export PATH="$PATH:`pwd`/flutter/bin"
      - flutter packages pub get
      - flutter config --enable-web
  build:
    commands:
      - echo Build started on `date`
      - dart run build_runner build
      - flutter build web --release
  post_build:
    commands:
      - echo Build completed on `date`
artifacts:
  files:
    - "**/*"
  discard-paths: no
  base-directory: "build/web"
```
Codebuild performs all the processes we normally do on a manual basis, like installing Flutter SDK, installing packages, running commands like build, etc.

## Step 6: Create a CodePipeline with CodeDeploy
CodePipeline is responsible for the orchestration of the deployment process; it observes any changes to our repository and then creates a deployment process if so. After a change is detected on the repository, the CodePipeline triggers the CodeBuild to perform all the scripts defined in buildspec.yml, then uploads the artifacts to an S3 bucket for code versioning (note that this bucket is not the S3 bucket we created earlier). Once the build is done, CodePipeline will trigger CodeDeploy to get the specific artifact previously created by the Codebuild and then deploy it to a specific environment like S3, EC2, ECS, EKS, etc.

**# Create a CodePipeline**
* Go to CodePipeline then click **Create new pipeline**

**# Pipeline Settings**
* Set your pipeline name ex: flutter-app-s3-pipeline-dev
* Execution mode: **Queued (Pipeline type V2 required)**
* Create new or use existing service role
* Leave other to default then click **Next**

**# Add Source Stage**
* Select **AWS CodeCommit** (you can use your own, ex Github)
* Select repository name (select the previously created repository)
* Select branch name (select specific branch you want this pipeline to observe)
* Leave everything default then click **Next**

**# Add Build Stage**
* Select **AWS CodeBuild**
* Select Region (Select the correct region your working on)
* Select Project Name (select the codebuild we previously created)
* Build type: **Single build**
* Click **Next**

**# Add Deploy Stage**
* Select **Amazon S3**
* Select Region (Select the correct region your working on)
* Select the S3 bucket we previously created
* Check **Extract file before deploy**
* Leave deployment path empty
* Click **Next**
* Review then click **Create Pipeline**

## Step 7: Testing the app
* Push your latest code into your git repository.
```terminal
git checkout master
git add .
git commit -m "your updates"
git push
```
* Go to CodeCommit then open your repository then
* Create a pull request from **master->dev**
* After successful pull request go to CodePipeline
* Open the CodePipeline you created for the development environment (you will notice a CI/CD process is being performed automatically)
* After successful deployment go to S3 (You will notice a new s3 bucket is created. Do not touch that bucket as it keeps all the version of your application)
* Open the S3 bucket you **manually created** before for the development environment
* Go to properties then scroll down to **Static website hosting**
* Click the link (you will be redirected to your app)

## Summary
In this exercise, we performed the process of building an application and creating the deployment process. But we only created a single environment; in the real world, we have to create three or more branches for our application.

### Here's my environment settings
```terminal
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.19.5, on Microsoft Windows [Version 10.0.19045.4529], locale en-PH)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.9.0)
[√] Android Studio (version 2023.2)
[√] VS Code (version 1.90.1)      
[√] Connected device (3 available)
[√] Network resources
```