Comprehensive Guide to CI/CD for Flutter Apps on AWS EC2 with Jenkins

Devesh kharadeDevesh kharade
10 min read

Comprehensive Guide to CI/CD for Flutter Apps on AWS EC2 with Jenkins

This documentation provides a detailed, step-by-step guide for setting up a Continuous Integration and Continuous Deployment (CI/CD) pipeline for a Flutter application using Jenkins on an AWS EC2 instance. This pipeline will automate the process of building a signed Android App Bundle (AAB) and deploying it to the Google Play Console.

Disclaimer:  The commands and configurations provided here are based on a typical Linux environment (e.g., Ubuntu/Debian). Specific commands and file paths may vary depending on your chosen operating system, version, and other system configurations. This guide assumes a basic understanding of Linux, AWS, Jenkins, and Flutter.

For a smooth and efficient build process, especially for Flutter applications, a robust EC2 instance is highly recommended.

  • Instance Type: t2.medium or larger. A t2.small might work but could be slow. t2.medium provides a good balance of cost and performance.

  • Operating System: Ubuntu 24 LTS or a similar Linux distribution.

  • Storage (EBS): At least 30 GB of storage. This is essential to accommodate the operating system, Jenkins, all SDKs (Flutter, Android, Java), and your project's build artifacts.

Step 1: Create an EC2 Instance

  1. Log in to your AWS Management Console.

  2. Navigate to the EC2 service.

  3. Click "Launch Instance."

  4. Choose an AMI: Select "Ubuntu Server 22.04 LTS (HVM)."

  5. Choose an Instance Type: Select t2.medium.

  6. Configure Instance Details, Add Storage, and Add Tags as needed.

  7. Configure Security Group:

  8. Add an inbound rule for SSH (Port 22) from your IP address.

  9. Add an inbound rule for HTTP (Port 8080) for Jenkins from your IP address or anywhere.

  10. Review and Launch the instance.

  11. Download the key pair and save it securely. You will need it to connect to your instance via SSH.

Step 2: Install and Setup Required Components

After launching and connecting to your EC2 instance via SSH, execute the following commands to install all the necessary tools.

Bash


# Update package list and install unzip and wget
sudo apt-get update -y
sudo apt-get install -y unzip wget

# Install Java Development Kit (JDK) 11, required by Android SDK
sudo apt-get install -y openjdk-11-jdk

# Install Jenkins
# Add Jenkins GPG key and repository
curl -fsSL https://pkg.jenkins.io/debian/jenkins.io-2023.key | sudo tee \
  /usr/share/keyrings/jenkins-keyring.asc > /dev/null
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
  https://pkg.jenkins.io/debian binary/ | sudo tee \
  /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt-get update -y
sudo apt-get install -y jenkins

# Start and enable Jenkins service
sudo systemctl start jenkins
sudo systemctl enable jenkins

# Install Flutter SDK
cd /opt
sudo git clone https://github.com/flutter/flutter.git
sudo chown -R jenkins:jenkins /opt/flutter
sudo chmod -R 755 /opt/flutter
export PATH="$PATH:/opt/flutter/bin"
flutter precache
flutter doctor

# Install Android SDK Command-line Tools
# Create a directory for the SDK and set permissions
sudo mkdir -p /opt/android-sdk
sudo chown -R jenkins:jenkins /opt/android-sdk
sudo chmod -R 755 /opt/android-sdk
cd /opt/android-sdk

# Download the latest command-line tools
wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
sudo unzip -q commandlinetools-linux-*.zip -d .
sudo rm commandlinetools-linux-*.zip

# Install necessary SDK components
yes | /opt/android-sdk/cmdline-tools/latest/bin/sdkmanager --sdk_root=/opt/android-sdk "platforms;android-34" "build-tools;34.0.0"

# Install GCloud CLI
sudo apt-get install -y apt-transport-https ca-certificates gnupg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
sudo apt-get update -y
sudo apt-get install -y google-cloud-cli

# Set permissions for the gcloud installation
sudo chown -R jenkins:jenkins /usr/local/google-cloud-sdk
sudo chmod -R 755 /usr/local/google-cloud-sdk

# Configure environment variables for all users
echo 'export JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"' | sudo tee -a /etc/profile.d/jenkins-env.sh
echo 'export ANDROID_HOME="/opt/android-sdk"' | sudo tee -a /etc/profile.d/jenkins-env.sh
echo 'export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:/opt/flutter/bin:/usr/local/google-cloud-sdk/bin"' | sudo tee -a /etc/profile.d/jenkins-env.sh
source /etc/profile.d/jenkins-env.sh

# Verify installations
java --version
flutter doctor
gcloud --version

Step 3: Setup Jenkins

  1. Access Jenkins: Open a web browser and navigate to http://<YOUR_EC2_PUBLIC_IP>:8080.

  2. Unlock Jenkins: Follow the on-screen instructions to retrieve the initial admin password from /var/lib/jenkins/secrets/initialAdminPassword on your EC2 instance.

  3. Install Plugins: Install the following plugins via the "Manage Jenkins" -> "Manage Plugins" page:

  4. Google Android Publisher Plugin: Essential for deploying to the Google Play Console.

  5. Credentials Plugin: For securely managing credentials.

  6. Git Plugin: To integrate with your Git repository.

  7. Pipeline: To create Jenkins pipelines.

  8. Configure Tools: Go to "Manage Jenkins" -> "Global Tool Configuration."

  9. JDK: Add a new JDK installation. Uncheck "Install automatically" and set JAVA_HOME to /usr/lib/jvm/java-11-openjdk-amd64.

  10. Git: Configure Git if not automatically detected.

  11. Add Jenkins Credentials: Navigate to "Manage Jenkins" -> "Manage Credentials" -> "(global)" -> "Add Credentials."

  12. ID: playstore-json, Kind: Secret file, Description: Service Account key in json formate. Upload your Google Cloud service account JSON file.

  13. ID: keystore-file, Kind: Secret file, Description: Bundle signing key key.jks. Upload your .jks keystore file.

  14. ID: store-password, Kind: Secret text, Description: Keystore password. Enter the password.

  15. ID: keypassword, Kind: Secret text, Description: Key password. Enter the password.

  16. ID: key-alias, Kind: Secret text, Description: Key alias. Enter your key alias.

  17. Setup Environment Variables: Go to "Manage Jenkins" -> "Configure System" and find the "Global properties" section. Check "Environment variables" and add the following:

    | Key | Value |

    | ANDROID_HOME | /opt/android-sdk |

    | PATH+ANDROIDSDK | /opt/android-sdk/platform-tools:/opt/android-sdk/cmdline-tools/latest/bin |

    | PATH+FLUTTER | /opt/flutter/bin |

    | PATH+GCLOUD | /usr/local/google-cloud-sdk/bin |

Step 4: Google Cloud Console Setup

  1. Create a Service Account:

  2. Go to the Google Cloud Console.

  3. Navigate to "IAM & Admin" -> "Service Accounts."

  4. Click "Create Service Account."

  5. Provide a name and description.

  6. Grant the Owner role to this service account. This is a powerful role and should be used with caution, but it simplifies the process for this guide.

  7. Create a JSON Key:

  8. Click on your newly created service account.

  9. Go to the "Keys" tab and click "Add Key" -> "Create new key."

  10. Select "JSON" and click "Create." The key file will be downloaded to your computer. This is the playstore-json file to be added to Jenkins.

  11. Configure OAuth2 and Enable API:

  12. Go to "APIs & Services" -> "OAuth consent screen."

  13. Configure the screen and add scopes.

  14. Navigate to "APIs & Services" -> "Library."

  15. Search for and enable the Google Play Android Developer API.

Step 5: Google Play Console Setup

  1. Add Service Account as a User:

  2. Go to the Google Play Console.

  3. Navigate to "Setup" -> "API access."

  4. Click "Create new service account" and follow the instructions to link your Google Cloud project.

  5. Once the service account appears, click on "Manage Play Console permissions" for that account.

  6. Grant the service account Admin privileges.

  7. Create App:

  8. Go to the "All apps" page and click "Create app."

  9. Fill in the app details and complete the initial setup.

Step 6: Add Jenkins Pipeline Script

This pipeline script automates the build and deployment process. It performs the following steps:

  1. Checkout: Clones the Flutter project from a Git repository.

  2. Clean & Get: Runs flutter clean and flutter pub get to prepare the environment.

  3. Generate Token: Uses the GCloud CLI and the service account JSON key to generate an OAuth 2.0 access token.

  4. Build AAB: Uses Jenkins credentials to dynamically create a key.properties file and then builds a signed Android App Bundle (.aab).

  5. Deploy to Play Store:

  6. Initiates an "edit" in the Play Developer API.

  7. Uploads the generated AAB.

  8. Assigns the uploaded AAB to the specified track (e.g., alpha, beta, or production).

  9. Commits the changes to finalize the deployment.

Pipeline Script:

def accessToken
pipeline {
  agent any

  environment {
    PACKAGE_NAME = "com.example.jenkins"
    AAB_PATH = "build/app/outputs/bundle/release/app-release.aab"
    //Testing tracks such as "alpha" and "beta"
    //The internal testing track: "qa"
    //The production track: "production"
    TRACK = "alpha"
    CREDENTIALS_FILE = "gplay_service_account.json"
  }

  stages {

    stage('Checkout') {
      steps {
        git branch: 'main', url: 'https://github.com/username/jenkins-test.git'
      }
    }

    stage('Flutter Clean & Get') {
      steps {
        sh '''
        which gcloud
          flutter clean
          flutter pub get
        '''
      }
    }

    stage('Generate Token using gcloud CLI') {
            steps {
                // Use Jenkins's built-in credentials management for security.
                // The 'service-account-key' ID must match a Secret File credential in Jenkins.
                withCredentials([file(credentialsId: 'playstore-json', variable: 'GOOGLE_APPLICATION_CREDENTIALS_FILE')]) {
                    script {
                        // Activate the service account using the JSON key file.
                        // The gcloud command handles reading the key and authenticating.
                        sh """
                            gcloud auth activate-service-account --key-file="${GOOGLE_APPLICATION_CREDENTIALS_FILE}"
                        """

                     // Get the access token for the authenticated service account,
                        // explicitly requesting the 'androidpublisher' scope.
                         accessToken = sh(
                            returnStdout: true,
                            script: "gcloud auth print-access-token --scopes=https://www.googleapis.com/auth/androidpublisher"
                        ).trim()

                        echo "Access Token generated successfully using gcloud CLI."
                        echo "Access Token: ${accessToken}"
                    }
                }
            }
        }

    stage('Build AAB') {
      steps {
       script {
                    // Create a dummy `key.properties` file on the fly using credentials.
                    withCredentials([
                        // Replace these with your actual Jenkins credential IDs for the keystore file and passwords.
                        file(credentialsId: 'keystore-file', variable: 'KEYSTORE_FILE'),
                        string(credentialsId: 'store-password', variable: 'KEYSTORE_PASSWORD'),
                        string(credentialsId: 'key-alias', variable: 'KEY_ALIAS'),
                        string(credentialsId: 'keypassword', variable: 'KEY_PASSWORD'),

                    ]) {
                      try{


                        // Create the `key.properties` file dynamically.
                        sh """
                        echo 'storeFile=${KEYSTORE_FILE}' > android/key.properties
                        echo 'storePassword=${KEYSTORE_PASSWORD}' >> android/key.properties
                        echo 'keyAlias=${KEY_ALIAS}' >> android/key.properties
                        echo 'keyPassword=${KEY_PASSWORD}' >> android/key.properties

                        """
                      }catch(Exception e){
                        error "Failed to create key.properties : ${e}"
                      }
                        // Execute the Flutter build command to create a signed AAB.
                        sh 'flutter build appbundle --release -v'
                    }
                }
      }
    }




       // Stage 4: Deploy the App Bundle to the Google Play Store using the API.
        stage('Deploy to Play Store') {
            steps {
                script {
                    // Use a temporary directory for the service account key.
                    withCredentials([file(credentialsId: 'playstore-json', variable: 'SERVICE_ACCOUNT_JSON_FILE')]) {
                        // Obtain an OAuth 2.0 access token using the service account key.
                        // This token is required to authenticate with the Play Developer API.
                    //   def accessToken = "ya29.a0AS3H6Nws0iCkmOD6znhl21KoSkHohcUI6QZIY2g8arHHW3vRkVAeMNsGFBS1tRIBOUnp3NkA-oslBixZhKjsochhL7J_ZYiP0_pv4ceyB3ZGyXmmxLPQYBPQk5eM7I5AL_dIZafJGfxrQmeumk25o-K_NWYKpFME2OOpTOKTaCgYKAfsSARMSFQHGX2MiD4cbiModIwuRhi94NbRbew0175"



                        // Make sure to configure API access in your Google Play Console and link it
                        // to the Google Cloud project where your service account lives.
                        // This step fetches the API URL for a new edit, which is required to publish the app.
                        def api_url_base = "https://www.googleapis.com/androidpublisher/v3/applications/${env.PACKAGE_NAME}"
                        def edits_url = "${api_url_base}/edits"

                       def editIdResponse = sh(
                    script: """
                        curl -s -X POST "${edits_url}" \\
                        -H "Authorization: Bearer ${accessToken}" \\
                        -H "Content-Type: application/json" \\
                        -d "{}"
                    """,
                    returnStdout: true
                ).trim()
                // Now this will print the full JSON response, including the edit ID.
                echo "Raw API Response for Edit ID: ${editIdResponse}"
                      // Parse the edit ID from the JSON response and store it as a simple string.
                        def editId = new groovy.json.JsonSlurper().parseText(editIdResponse).id.toString()

                      // Upload the new App Bundle (.aab) to the Play Store.
                 def upload_url = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications/${env.PACKAGE_NAME}/edits/${editId}/bundles?ackBundleInstallationWarning=true"
                def uploadResponse = sh(
                    script: """
                        curl -s -X POST \\
                        -H "Authorization: Bearer ${accessToken}" \\
                        -H "Content-Type: application/octet-stream" \\
                        --data-binary "@${env.AAB_PATH}" \\
                        "${upload_url}"
                    """,
                    returnStdout: true
                ).trim()

                echo "Raw API Response for Upload: ${uploadResponse}"

                // Parse the version code from the JSON response and store it as a simple integer.
                def versionCode
                try {
                    versionCode = new groovy.json.JsonSlurper().parseText(uploadResponse).versionCode.toInteger()
                    echo "Successfully uploaded AAB with version code: ${versionCode}"
                } catch (Exception e) {
                    error "Failed to parse version code from upload response. Response: ${uploadResponse}"
                }

                        // Assign the uploaded bundle to the specified track (e.g., 'internal').
                        def track_url = "${edits_url}/${editId}/tracks/${env.TRACK}"
                        def trackBody = "{\"releases\": [{\"versionCodes\": [\"${versionCode}\"], \"status\": \"completed\"}]}"

                        sh "curl -X PUT -H 'Authorization: Bearer ${accessToken}' -H 'Content-Type: application/json' --data '${trackBody}' '${track_url}'"

                        // Commit the changes to the Play Store to finalize the release.
                        def commit_url = "${edits_url}/${editId}:commit"
                        sh "curl -X POST -H 'Authorization: Bearer ${accessToken}' -H 'Content-Type: application/json' '${commit_url}'"
                    }
                }
            }
            }

  }
}

Running the Job

  1. In Jenkins, go to the dashboard and click "New Item."

  2. Enter an item name (e.g., flutter-ci-cd-pipeline).

  3. Select "Pipeline" and click "OK."

  4. In the configuration page, scroll down to the "Pipeline" section.

  5. Select "Pipeline script" from the "Definition" dropdown.

  6. Copy and paste the pipeline script provided above into the text area.

  7. Click "Save."

  8. Click "Build Now" to run the job and watch the pipeline execute.

Automatic Job Trigger with GitHub

To trigger the Jenkins job automatically on a new commit to your GitHub repository, follow these steps:

  1. Install GitHub Integration Plugin: In Jenkins, go to "Manage Jenkins" -> "Manage Plugins" and install the "GitHub Integration Plugin."

  2. Configure GitHub Webhook:

  3. In your Jenkins job configuration, go to "Build Triggers."

  4. Check "GitHub hook trigger for GITScm polling."

  5. Add Webhook in GitHub:

  6. Go to your GitHub repository on the web.

  7. Navigate to "Settings" -> "Webhooks."

  8. Click "Add webhook."

  9. Payload URL: http://<YOUR_EC2_PUBLIC_IP>:8080/github-webhook/

  10. Content type: application/json

  11. Leave the secret blank for now.

  12. Select "Just the push event."

  13. Click "Add webhook."

Now, every time you push a new commit to the main branch, Jenkins will automatically trigger the pipeline job.

Reference Documentation

1. https://developers.google.com/android-publisher/edits

0
Subscribe to my newsletter

Read articles from Devesh kharade directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Devesh kharade
Devesh kharade