Tích hợp liên tục với Jenkins và Java

toannm

Tối ưu hoá DevOps với hướng dẫn thiết lập và sử dụng Jenkins để tích hợp liên tục (CI) trên Java app. Hướng dẫn tại bài viết này gồm các nội dung sau:
  • Cấu hình pipeline bằng Jenkinsfile.
  • Quản lý credentials bên thứ ba.
  • Jenkins kiểm tra báo cáo tích hợp (integration report).
  • Poll và hook build các trigger.
  • Build các pull request.
Trước khi bắt đầu phần hướng dẫn, cần hiểu về CI. CI là một development pratice phổ biến, hoạt động khi người dùng xác nhận tính năng bảo đảm phần mềm đảm bảo chất lượng cao và có thể deploy, ngay khi người dùng kiểm tra các thay đổi đối với SCM.
  • Để áp dụng CI, cần một số thành phần quan trọng sau:
  • Hệ thống SCM như Git và repository được chia sẻ
  • Máy chủ CI (như Jenkins)
  • Kiểm tra tự động
  • Practive CI làm việc theo nhóm cho phép giữ build time ngắn, khắc phục các bản dựng bị hỏng ngay lập tức, thực hiện các cam kết thường xuyên và giữ các thay đổi nhỏ.
Yêu cầu công cụ:

Running Jenkins

1588101051208.png

Jenkins là một server nguồn mở tự dộng hoá mà developer có thể sử dụng để Continuous Integration (Tích hợp liên tục), Continuous Delivery (Phân phối liên tục) và Continuous Deployment (Triển khai liên tục). Đây là một nhánh của Hudson, một server CI được viết bằng Java tại Sun microsystems năm 2004.

Jenkins Pipeline là một bộ plugin có thể được sử dụng để tự động hóa các bản dựng, test và deploy. Người dùng có thể xác định pipeline với cú pháp cụ thể trong Jenkinsfile, những pipline này có thể được cam kết với một repo và phiên bản của project, trong mô hình Pipline-as-code.

Khởi động nhanh bằng cách pull Jenkins image từ Docker HYub:

Mã:
Shell
docker pull jenkins/jenkins:lts
Khởi động một Jenkins container:

Mã:
Shell
docker run \
  -p 8081:8080 \
  -p 50000:50000 \
  --name my-jenkins \
  -v jenkins_data:/var/jenkins_home
  jenkins/jenkins:lts
Ở lệnh trên, ta đã map port 8080 của Jenkins sang port 8080 của host và port 50000 của Jenkins tới port 50000 của host. Đồng thời, một volume cho Jenkins home đã được xác định trong folder jenkins_data.

Khi container khởi động, cài đặt ban đầu sẽ chạy và Jenkins sẽ lưu admin password.

Plain Text

Mã:
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
b518968d266d41d3beb0abef50834fa7
This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
Copy password và vào http://localhost:8081 để bắt đầu setup.

1588101196967.png

Paste amind password và tiếp tục. Quá trình process đã mở cơ hội tuỳ chỉnh các plugin người dùng muốn thêm vào. Chọn Install Suggested Plugins và chờ phần cài đặt hoàn thiện.

1588101659940.png


Đặt admin user data. Vì đây như một bản test, để lại phần URL Jenkins (http://localhost:8081/) mặc định và hoàn thành nó.

1588101262994.png


Đến đây, hệ thống đã sẵn sàng để tạo Jenkins Pipeline đầu tiên.

Ứng dụng đơn giản với Xác thực Okta OIDC

Tại đây, Jenkins sẽ được sử dụng để tự động bản dụng của một Java app với Xác thực Okta OIDC (Oka OIDC authentication). Vì vậy, đầu tiên hãy tạo app với Spring Intializr:

Mã:
curl https://start.spring.io/starter.zip -d dependencies=web,okta \
-d language=java \
-d type=maven-project \
-d groupId=com.okta.developer \
-d artifactId=simpleapp  \
-d name="Simple Application" \
-d description="Demo project for Jenkins CI test" \
-d packageName=com.okta.developer.simpleapp \
-o simple-app.zip
Giải nén file:

Mã:
unzip simple-app.zip -d simple-app
cd simple-app
Nếu chưa có tài khoản Okta developer, hãy thực hiện Plugin Okta Maven để tạo một tài khoản miễn phí và định cấu hình xác thực trong app:

Mã:
Plain Text
./mvnw com.okta:okta-maven-plugin:setup
Output trả về như sau:
Plain Text

Mã:
First name: Jimena
Last name: Garbarino
Email address: ***
Company: ***
Creating new Okta Organization, this may take a minute:
OrgUrl: ***
Check your email address to verify your account.
Writing Okta SDK config to: /home/indiepopart/.okta/okta.yaml
Configuring a new OIDC, almost done:
Created OIDC application, client-id: ***
Kiểm tra email và làm theo hướng dẫn để trigger tài khoản Okta.

Plugin Maven sẽ tạo client ID OIDC (ID khách hàng), khóa bí mật và URL của nhà phát hành src/main/resources/application.properties.

Vì kho lưu trữ GitHub công khai cho CI test sẽ được sử dụng, cần sao chép chứng chỉ xác thực credentials ở nơi khác và xóa chúng khỏi file thuộc tính.

Nếu đã có tài khoản Okta developer, hãy đăng nhập và tạo một app mới:
- Từ trang Application page, chọn Add Application.
- Trên trang Create New Application, chọn Web.
- Đặt cho app một cái tên dễ nhớ và thêm http://localhost:8080/login/oauth2/code/okta
làm URI chuyển hướng đăng nhập.

Sao chép issuer ( có thể tìm thấy nó trong API>Authorization Servers), client ID và client secret để sử dụng sau.

Thêm một REST Controller

Tạo một GreetingController class để gửi lời chào user tại phần đăng nhập.

Java
Mã:
package com.okta.developer.simpleapp;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
    @GetMapping("/greeting")
    public String greet(@AuthenticationPrincipal OidcUser user){
        return "Hello " + user.getEmail();
    }
}
Test app với Maven Spring Boot plugin:

Java
Mã:
OKTA_OAUTH2_CLIENT_ID={youtOktaClientId} \
OKTA_OAUTH2_CLIENT_SECRET={yourOktaClientSecret} \
OKTA_OAUTH2_ISSUER={yourOktaDomain}/oauth2/default \
mvn spring-boot:run
Vào đường link http://localhost:8080/greeting. App được tạo sẽ chuyển hướng đến Okta để đăng nhập:

1588101447681.png


Sau khi đăng nhập, app sẽ hiển thị màn hình chào:
Plain Text
Mã:
Hello [email protected]***.com
Tạo một repo GitHub public cho simple-app và làm theo hướng dẫn để push các code hiện có.

Shell
Mã:
git init
git add .
git commit -m "initial commit"
git remote add origin https://github.com/<your-username>/simple-app.git
git push -u origin master
Jenkins Pipeline và Jenkinsfile

Trong Jenkins dashboard, chọn Create New Jobs, đặt simple-app làm tên thư mục, chọn Pipline làm kiểu của project.

1588101559541.png


Trong màn hình tiếp theo, chọn tab Advanced Project Options. Từ menu pull xuống bên phải, chọn GitHub + Maven để lấy template Jenkinsfile sẽ được tùy chỉnh.
Sao chép tập lệnh pipeline vào một file trong thư mục gốc của project simple-app có tên Jenkinsfile. Cập nhật URL GitHub repository và thiết lập thông tin đăng nhập Okta cho bản dựng, đồng thời thay đổi lệnh Maven để sử dụng Maven gói sẵn trong project.

Groovy
Mã:
pipeline {
   agent any
   environment {
       // use your actual issuer URL here and NOT the placeholder {yourOktaDomain}
       OKTA_OAUTH2_ISSUER           = '{yourOktaDomain}/oauth2/default'
       OKTA_OAUTH2_CLIENT_ID        = credentials('OKTA_OAUTH2_CLIENT_ID')
       OKTA_OAUTH2_CLIENT_SECRET    = credentials('OKTA_OAUTH2_CLIENT_SECRET')
   }
   stages {
      stage('Build') {
         steps {
            // Get some code from a GitHub repository
            git 'https://github.com/<your-username>/simple-app.git'
            // Run Maven on a Unix agent.
            sh "./mvnw -Dmaven.test.failure.ignore=true clean package"
            // To run Maven on a Windows agent, use
            // bat "mvn -Dmaven.test.failure.ignore=true clean package"
         }
         post {
            // If Maven was able to run the tests, even if some of the test
            // failed, record the test results and archive the jar file.
            success {
               junit '**/target/surefire-reports/TEST-*.xml'
               archiveArtifacts 'target/*.jar'
            }
         }
      }
   }
}
environment directive của pipeline đang sử dụng để xác định các biến OKTA_* mà bản dựng yêu cầu. Lệnh hỗ trợ credentials() hỗ trợ lấy các giá trị từ môi trường Jenkins.

Tiếp theo, trước khi yêu cầu build project, cần thiết lập credential được quản lý bởi Okta trong Jenkins.

Push Jenkinsfile vào public repo.

Trong Advanced Project Options, đối với Pipeline Definition, chọn Pipeline script from SCM và hoàn thành thông tin repo:

Chọn Save để tạo project.

Quản lý Chứng chỉ xác thực


Jenkins cho phép lưu trữ thông tin đăng nhập cho các app của bên thứ ba một cách an toàn, cho phép các project Pipeline sử dụng chúng cho các tương tác với các dịch vụ của bên thứ ba này. Hãy thêm vào các thông tin xác thực cho Okta.

Trong Jenkins dashboard, đi tới Credentials trên menu bên trái, sau đó chọn global.

Tạo chứng chỉ xác thực credentials "Secret text" cho OKTA_OAUTH2_CLIENT_ID,, click vào Add Credentials và chọn các tùy chọn sau:
  • Kind: Secret text
  • Scope: global
  • Secret: {yourOktaClientID}
  • ID: OKTA_OAUTH2_CLIENT_ID

Lưu ý: Thay thế {yourOktaClientID} bằng client ID.

Làm tương tự cho OKTA_OAUTH2_CLIENT_SECRET.

1588101806341.png


Lưu ý: Không nên sử dụng các secret trong Jenkins và building pull-requests từ repo rẽ nhánh. Building pull-requests từ bên ngoài cũng giống như thực thi code tùy ý và cần được thực hiện hết sức cẩn thận và nằm ngoài phạm vi của bài viết này.

Đến đây, hệ thống đã sẵn sàng build project. Tới simple-app và chọn Build Now. Chuyển đến Build History và chọn build #1 (bản dựng số 1). Sau đó chọn tùy chọn Console Output để theo dõi tác vụ.

1588101822463.png


Thêm một Controller Test

Template Jenkinsfile cho GitHub và Maven đã tích hợp các test report (báo cáo thử nghiệm) và cho chúng khả năng truy cập được từ build summary (bản tóm tắt bản dựng). Hãy thêm vào một bài test controller trong ứng dụng để xác minh tính năng này.

Thêm phụ thuộc spring-security-test vào pom.xml:

Mã:
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <scope>test</scope>
</dependency>
Tạo một class mới:
src/test/java/com/okta/developer/

simpleapp/GreetingControllerTest.java:

Java
Mã:
package com.okta.developer.simpleapp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@AutoConfigureMockMvc
@WebMvcTest
@ContextConfiguration(classes={GreetingController.class})
public class GreetingControllerTest {
    private final static String ID_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" +
            ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsIm" +
            "p0aSI6ImQzNWRmMTRkLTA5ZjYtNDhmZi04YTkzLTdjNmYwMzM5MzE1OSIsImlhdCI6MTU0M" +
            "Tk3MTU4MywiZXhwIjoxNTQxOTc1MTgzfQ.QaQOarmV8xEUYV7yvWzX3cUE_4W1luMcWCwpr" +
            "oqqUrg";
    @Autowired
    private MockMvc mvc;
    @Test
    void testGreet() throws Exception {
        OidcIdToken idToken = createOidcToken();
        this.mvc.perform(get("/greeting")
                .with(authentication(createMockOAuth2AuthenticationToken(idToken))))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(content().string("Hello [email protected]"));
    }
    private OAuth2AuthenticationToken createMockOAuth2AuthenticationToken(OidcIdToken idToken) {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        OidcUser user = new DefaultOidcUser(authorities, idToken);
        return new OAuth2AuthenticationToken(user, authorities, "oidc");
    }
    private OidcIdToken createOidcToken(){
        Map<String, Object> claims = new HashMap<>();
        claims.put("groups", "ROLE_USER");
        claims.put("email", "[email protected]");
        claims.put("sub", 123);
        OidcIdToken idToken = new OidcIdToken(ID_TOKEN, Instant.now(),
                Instant.now().plusSeconds(60), claims);
        return idToken;
    }
}
1588101963069.png


Bỏ phiếu cho những thay đổi

Pipeline hỗ trợ một số loại trigger để lên lịch build. Một trong số chúng định kỳ thăm dò Hệ thống SCM (GitHub) để thay đổi. Nếu một thay đổi mới tồn tại, nó sẽ trigger lại Pipeline. Lệnh triggers trong Jenkinsfile cấu hình các bản dựng triggers:

Groovy
Mã:
pipeline {
   agent any
   triggers { pollSCM('H/15 * * * *
') } // poll every 15 minutes
   environment {
...
Trigger loại pollSCM có biểu thức cron cấu hình pipeline để thăm dò GitHub mỗi 15 phút.

Lưu ý: Để cài đặt trigger trong Jenkins, trước tiên phải lên lịch build thủ công từ Jenkins, sau khi đã push Jenkinsfile cập nhật.

Multibranch Pipelines

Các project Multibranch Pipeline tự động phát hiện Pipeline cho các branch (chi nhánh) và có thể được sử dụng để xác thực các pull request. Plugin GitHub Branch Source cung cấp chức năng xác thực và Cloudbees lưu trữ tài liệu của nó. Do đã cài đặt các tính năng này với các plugin được đề xuất như phần trên, vì vậy, hãy lướt qua cấu hình.

Trong Jenkins dashboard, đi đến New Item, nhập tên item, sau đó chọn Multibranch Pipeline. Tiếp theo, ở dạng cấu hình, đến Branch Sources và chọn GitHub. Chọn Repository Scan. Trong trường Owner, đặt GitHub user và chọn repo để scan. Để đơn giản hóa test này, public repo đã được tạo, vì vậy có thể bỏ qua thiết lập Chứng chỉ xác nhận GitHub (GitHub Credentials).

1588102140618.png


Chọn tab Scan Multibranch Pipeline Triggers, tick vào ô Periodically if not otherwise run và đặt interval (khoảng thời gian) là 5 minutes (5phút).

Chọn Save và thêm project mới.

Trigger một Build

Tạo một file README.md trong folder gốc (root folder) của project simple-app

Markdown
Mã:
# Simple Application with Okta OIDC Authentication
Clone the project and run the application with Maven:
```shell
git clone https://github.com/<your-username>/simple-app.git
cd simple-api
OKTA_OAUTH2_CLIENT_ID={youtOktaClientId} \
OKTA_OAUTH2_CLIENT_SECRET={yourOktaClientSecret} \
OKTA_OAUTH2_ISSUER={yourOktaDomain}/oauth2/default \
./mvnw spring-boot:run
```
Tạo một branch cho thay đổi và một pull request. Trong lần scan định kỳ tiếp theo, Jenkins sẽ tạo một công việc cho pull request.

Shell
Mã:
git checkout -b add-readme
git add README.md
git commit -m "added readme"
git push origin add-readme
1588102187662.png


Plugin GitHub Branch Source cho phép tạo một project dựa trên cấu trúc repository của một tổ chức GitHub, bằng cách sử dụng loại project "GitHub Organization". Đối với các project như vậy, plugin sẽ scan và nhập tất cả hoặc một subnet của các kho lưu trữ dưới dạng các job theo một tiêu chí được cấu hình.

GitHub Hook Trigger

Jenkins có plugin GitHub để trigger các bản dựng sau khi nhận được thông báo về các push changes và pull request. Thông qua GitHub Webhooks, khi event được trigger, GitHub sẽ gửi một payload HTTP POST đến URL được cấu hình webhook của Jenkins. Khi nhận được POST, Jenkins sẽ chỉ đơn giản chuyển cuộc bỏ phiếu nội bộ (internal polling) sang SCM.
Có thể định cấu hình URL hook của Jenkins tại GitHub theo cách thủ công hoặc chính Jenkins có thể quản lý các hook cho project dựa trên cấu hình. Đối với chế độ được quản lý, cần phải cấu hình xác thực cho GitHub và tại thời điểm viết hướng dẫn này, Jenkins không thể xác thực nếu ngừoi đã bật xác thực hai yếu tố trong GitHub.

Việc sử dụng GitHub Webhooks yêu cầu Jenkins phải truy cập được từ internet. Tài liệu plugin cũng đề cập đến URL hook là duy nhất cho tất cả các repo nhưng không đề cập đến bất kỳ loại xác thực nào được yêu cầu cho phía người gọi. Có những ý nghĩa bảo mật khác được liệt kê trong tài liệu mà nên được đánh giá trước khi sử dụng tính năng này.
 
Top