ã¯ããã« ããã«ã¡ã¯ãã€ãããŒã·ã§ã³ã»ã³ã¿ãŒã®çŠç°ã§ãã ä»åãéçºç°å¢æ¹åã®åãçµã¿ãšã㊠GitHub Actions ã® self-hosted runners ã AWS äžã«æ§ç¯ããŸããã ãã®æ§ç¯ã§åŸãããç¥èŠã«ã€ããŠå
±æããŸãã æŠèŠ GitHub Actions 㯠GitHub ã§ CI/CD ãæè»œã«å®çŸããæ©èœã§ãã GitHub ãæäŸããŠããç°å¢ãå©çšããŠã CI/CD ã®ãžã§ããå®è¡ã§ããŸã 1 ã äžæ¹ã§ãããŒããŠã§ã¢çãã«ã¹ã¿ãã€ãºã§ããªããããäŸãã°å®¹éã倧ããããéåºŠã®æ©ãã¹ãã¬ãŒãžãå©çšãããå Žåããããå€ãã®ã¡ã¢ãªãå©çšãããå Žåã«å¯Ÿå¿ãã§ããŸããã ããã§ãGitHub Actions ã«ã¯ self-hosted runners ãšããæ©èœããããèªèº«ã®ç°å¢ã§ GitHub Actions ã® CI/CD ãžã§ããèµ°ãããç°å¢ãçšæã§ããŸãã ä»åã¯ããæè»ã«å©çšãããããä¿å®ããããããã« AWS Cloud Development Kit (以äžCDK)ãçšã㊠AWS äžã« self-hosted runners ç°å¢ãæ§ç¯ããã®ã§ããã®æ§ç¯æé çã«ã€ããŠè§£èª¬ããŠãããããšæããŸãã ã¢ãŒããã¯ã㣠ä»åæ§ç¯ããã·ã¹ãã ã®ã¢ãŒããã¯ãã£ã«ã€ããŠè§£èª¬ããŸãã æ§ç¯ããã€ã³ãã©ã®æ§æå³ã¯æ¬¡ã®éãã§ãã å³ã®äžã§ç¹ç·ã«ãã£ãŠã°ã«ãŒãåãããŠããŸãããæ¬ã·ã¹ãã ã¯å€§ããæ¬¡ã®2ã€ã®éšåãããªã£ãŠããŸãã Webhook ã€ãã³ãã®åä¿¡éšå self-hosted runners ç°å¢æäŸéšå GitHub ã§ã¯ GitHub Actions ã® CI/CD ãžã§ããèµ°ãå§ãããšãã« Webhook ã€ãã³ããé£ã°ãããšãã§ãããããå©çšã㊠CI/CD ãžã§ãã®éå§ãšåæã«ãã® CI/CD ãžã§ããèµ°ããã self-hosted runners ãèµ·åãããšããã·ã¹ãã ã«ãªã£ãŠããŸãã Webhook ã€ãã³ãã®åä¿¡ ãŸãã self-hosted runners ã§èµ°ãããã CI/CD ãžã§ããéå§ãããã®ããç¥ãå¿
èŠããããŸãã GitHub ã«ã¯ workflow_job ãšããã€ãã³ãã Webhook ã€ãã³ããšããŠåºãŠããããã®ã€ãã³ãã®ãã€ããŒãã«ä»¥äžã®æ
å ±ãå«ãŸããŠããŸãã ãžã§ãã®å®è¡ç°å¢ã®æå® GitHub-hosted runners ã§ã®å®è¡ãã self-hosted runners ã§ã®å®è¡ããæå®ãããŠããŸãã ãžã§ãã®ã¹ããŒã¿ã¹ ãžã§ãã®éå§ãèŠæ±ãããã®ããçŸåšãžã§ããèµ°ã£ãŠããã®ãããžã§ããçµäºããã®ãã®ã¹ããŒã¿ã¹ãããããŸãã ãã® workflow_job ã€ãã³ãã Webhook ã€ãã³ããšããŠåä¿¡ããããšã§ self-hosted runners ã§èµ°ãããã CI/CD ãžã§ããéå§ãããã®ããç¥ãããšãã§ããŸãã ãããã£ãŠãæåã« workflow_job ã€ãã³ããåä¿¡ãã Webhook ã®åä¿¡å
ã AWS ã«çšæããŸãã AWS ã«ã¯ API Gateway ãšãããµãŒãã¹ãããããã®ãµãŒãã¹ã䜿ã£ãŠå®¹æã« Web API ãäœæã§ããŸãã ãªã®ã§ã workflow_job ã€ãã³ãã®åä¿¡éšåã«é¢ããŠã¯ãã® API Gateway ã䜿ã£ãŠæ§ç¯ããŸãã ãããã workflow_job 㯠GitHub-hosted runners ã§èµ°ãããã CI/CD ãžã§ããéå§ããããšãã«ãçºè¡ãããã€ãã³ãã§ãã ãã®ããã API Gateway ã§äœæãã Web API ã§ãã® workflow_job ã€ãã³ããåä¿¡ã self-hosted runners ãèµ·åãããšããåçŽãªå®è£
ã«ããŠããŸããš self-hosted runners ã§èµ°ãããããžã§ãæ°ããã倧ãã® self-hosted runners ãèµ·åããäºå®ãããå€ãã®èª²éããªãããŠããŸããŸãã ãããé¿ãããããäœæãã Web API ã webhook ã€ãã³ããåä¿¡ããã self-hosted runners ã§èµ°ãããã CI/CD ãžã§ãã®éå§ãèŠæ±ãããã€ãã³ãã ããæœåºãããã以å€ã®ã€ãã³ãã¯ç¡èŠãããããªä»çµã¿ãå°å
¥ããŸãã ä»å䜿çšãã API Gateway 㯠Lambda 颿° ã® HTTP ãšã³ãã€ã³ããæäŸã§ããã®ã§ããã®æ©èœãå©çšããäœæãã Web API ã®ããã¯ãšã³ãã§ Lambda 颿°ã«ããã€ãã³ããã£ã«ã¿ãªã³ã°ã®æ©èœã远å ããŸãã ãããããã® Lambda 颿°ã® HTTP ãšã³ããã€ã³ãã®æ©èœã«ã¯ Lambda 颿°ã®æå€§å®è¡æéã29ç§ãŸã§ã§ãããšããå¶çŽãååšããŸãã ãã®ãŸãŸ self-hosted runners ãåããããšãããšãã®å¶çŽã«ã²ã£ããã£ãŠããŸãã self-hosted runners ãèµ·åã§ããªããªãå¯èœæ§ããããŸãã ãããåé¿ããããã«ã Lambda 颿°ã¯ãã£ã«ã¿ãŒãè¡ã£ãåŸã« self-hosted runners ãèµ·åãããããªããšã¯ãããäžæŠ SQS ã«ãã£ã«ã¿ãŒããã€ãã³ããéä¿¡ããŸãã ããããã° SQS ããããšã®åŠçã¯ç¬ç«ããŠè¡ãããããå
è¿°ããå¶éãåé¿ã§ããŸãã 2 ã 以äžããŸãšãããšã Webhook ã®åä¿¡éšåã¯æ¬¡ã®ãããªã€ã³ãã©ã¹ãã©ã¯ãã£ãŒãæ§ç¯ããããšã«ãªããŸãã self-hosted runners ã®æäŸ ããããã¯åä¿¡ãã Webhook ã€ãã³ããå
ã«ãé©å㪠self-hosted runners ãèµ°ããã VM (以äžã self-hosted runners ããŒã) ãç«ã¡äžããããã®ã€ã³ãã©æ§æã«ã€ããŠè§£èª¬ããŸãã SQS ã«ã¯ Webhook ã€ãã³ããéä¿¡ãããŠããã®ã§ããŸãã¯ãã® SQS ããã¡ãã»ãŒãžãåä¿¡ããéšåãæ§ç¯ããŸãã SQS ãã¡ãã»ãŒãžãåä¿¡ãããšããLambda 颿°ãžã¡ãã»ãŒãžã®åä¿¡ã€ãã³ããéç¥ã§ããŸã 3 ã ãã®æ©èœãå©çšããŠã SQS ãã Webhook ã€ãã³ããå信次第 self-hosted runners ããŒããšãªã VM ã§ãã EC2 ã€ã³ã¹ã¿ã³ã¹ ãèµ·åãã Lambda 颿°ãçšæããŸãã ãã®éã EC2 ã€ã³ã¹ã¿ã³ã¹ã EC2 ã€ã³ã¹ã¿ã³ã¹ã®èµ·åãè¡ã API ãéããŠèµ·åããããšã«ãªãã®ã§ãããããã§ Launch Template ãšãã EC2 ã€ã³ã¹ã¿ã³ã¹ã®èµ·åãã©ã¡ãŒã¿ã®ãã³ãã¬ãŒããå©çšããŠèµ·åããããã«ããŸãã Launch Template 㯠CDK ã§ç°¡åã«çšæããããšãã§ãã ããããã¡ãã®æ©èœãéããŠèµ·åããæ¹ã Lambda 颿°ã« EC2 ã€ã³ã¹ã¿ã³ã¹ã®èµ·åãã©ã¡ãŒã¿ãå
šãŠæž¡ããããªããæ¹ãããæ¥œã«ä¿å®ã»ç®¡çã»éçšã§ããŸã 4 ã ãã®ããã«ã SQS ãã Webhook ã€ãã³ããåä¿¡ããŠãã Lambda 颿°ãèµ·åãããã® Lambda 颿°ã¯ self-hosted runners ããŒããèµ·åããããšã§ self-hosted runners ãå¿
èŠãªåã ãæäŸã§ããŸãã èµ·åãã self-hosted runners ããŒã㯠CI/CD ãžã§ããå®è¡ãããã® CI/CD ãžã§ããçµäºæ¬¡ç¬¬ã·ã£ããããŠã³ããããã«ããŸãã EC2 ã€ã³ã¹ã¿ã³ã¹ã«ã¯ã·ã£ããããŠã³çµäºåŸã«èªåã§ VM ã®åé€ããããã«èšå®ã§ãããã®èšå®ãããŠãããŸãã ããããããšã§ã€ã³ã¹ã¿ã³ã¹ããã®ã¹ãã¬ãŒãžãžã®èª²éé¡ãæãããããšåæã«ãåã®ãžã§ãã®æ
å ±ãã€ã³ã¹ã¿ã³ã¹å
ã«æ®ããªãããã«ã§ããŸãã ã€ãŸããæ¯åã¯ãªãŒã³ãªç°å¢ã§ã€ã³ã¹ã¿ã³ã¹ãèµ·åã§ããåãžã§ãã«äŸåé¢ä¿ãçºçããã®ãé²ãããšãã§ããŸãã æåŸã«ã self-hosted runners ãèµ·åããéã«ã¯ä»¥äžã®2ã€ã®åŠçã self-hosted runners ã®èµ·ååã«è¡ãå¿
èŠãããã®ã§ããããã EC2 ã€ã³ã¹ã¿ã³ã¹ã® èµ·åæã«ã¹ã¯ãªãããèµ°ãããæ©èœ ã䜿ã£ãŠãããªããŸãã ææ°çã® self hosted runners ã®ããŠã³ããŒã self-hosted runners ãç»é² ãã®ãšããã·ã§ã«ã¯ãªããã§ããã«ã¯è€éãããããšããã£ããããããšãããããã±ããã®éãæãããããããã«ãããã€ãã®åŠçã Lambda 颿°ãéããŠè¡ãããã«ããŸãã ãããããŸãšãããšã以äžã®ãããªæ§æå³ãæ§ç¯ããããšã«ãªããŸãã å³äžã«ã¯ S3 ãã±ãããåºçŸããŠããŸããããã¡ã㯠1. ã®ããŠã³ããŒãããŠãã self-hosted runners ããã£ãã·ã¥ããããã«å©çšããŠããŸãã CDK ã«ããã€ã³ãã©æ§ç¯ å
è¿°ããã·ã¹ãã ã Web ã³ã³ãœãŒã«ã CLI ã ãã§æ§ç¯ããé©åã«ä¿å®ã»éçšããŠããã®ã¯é£ãããã CDK ãå©çšããŸãã self-hosted runners ç°å¢ æåã« self-hosted runners ããŒããèµ·åããä»çµã¿ãæ§ç¯ããŸãã self-hosted runners ããŒãèªäœã¯ Public ãªå Žæã«çœ®ããŠããå¿
èŠã¯ãªãã GitHub ãšã®æ¥ç¶ãã§ããã°ãããã Private 㪠ãµãããã ãžãšèšçœ®ããããã«ãã Public ãªãµããããã« NAT ã²ãŒããŠã§ã€ ãèšçœ®ã㊠Private ãµããããã®éä¿¡ããããžæµãããã«ããŸãã ãŸã㯠self-hosted runners ããŒãèµ·åæã«èµ°ãããã¹ã¯ãªããå
ã§åŒã°ããè£å© Lambda 颿°ãçšæããŸãã self-hosted runners ãæäŸããããã°ã©ã ãããŠã³ããŒããã颿° ããŠã³ããŒããããã€ããªãŒã«ããŒãžã§ã³çªå·ãã€ããŠãããããå
ã«ææ°çã®ããŒãžã§ã³çªå·ã GitHub API ããååŸããŠããå¿
èŠããããã¹ã¯ãªããã®è€é床ãå¶åŸ¡é£æåºŠãäžããŠããŸãã®ã§è©ä»£ããããã 颿°ãåŒã³åºãã°ææ°çãåžžã«æã«å
¥ãããã¹ããçšæã«ãªã èµ°ããã self-hosted runners ã GitHub ã«ç»é²ããããã®ããŒã¯ã³ãã€ãã颿° ãã®éšå㯠JWT ãã€ãã£ãããããªã©å€å°è€éãªããžãã¯ãå®è£
ããå¿
èŠãããã bash ã¹ã¯ãªããã§ããããã Lambda 颿°ã«åŠçãç§»è²ããŠããŸã£ãæ¹ãè¯ãããã§ã å®ã¯ 1. ã®å Žå㯠Lambda 颿°ã®å¶éãšã㊠6MB ãŸã§ã®ã¬ã¹ãã³ã¹ããè¿ããŸãã ã self-hosted runners ã¯ãã®å¶éã§ãã 6MB ãè¶ããŠããŸã ã ãã®ãããLambda 颿°ãšããŠã¯ self-hosted runners ããã®ãŸãŸè¿ãããšã¯ããã«ã S3 ãã±ãã ã«äžæŠããŠã³ããŒãããŠãããã®ãé
眮ãããã®é
眮å
ãã¬ã¹ãã³ã¹ã«èŒããŸãã ããŠã³ããŒã㯠VPC ã« S3 ã®ãšã³ããã€ã³ããèšçœ®ããŠã Lambda 颿°ã®ã¬ã¹ãã³ã¹ããé
眮å
ããã¿ãšã£ãŠ S3 ãã±ããããããŠã³ããŒãããŠããŸãã ããããããšã§ã坿¬¡çã« NAT ã²ãŒããŠã§ã€ ãéããã±ããã®éãæžããããšãã§ããã®ã§ã NAT ã²ãŒããŠã§ã€ã®æéãæãã广ããããŸãã ããŠããã®2ã€ã®é¢æ°ã CDK ã«èŒããŠãããŸãã ãªããããã§ã¯çšæãã颿°ã¯ãã¹ãŠ Go èšèª ã§èšèŒããŠããŸãã ãŸãã Go èšèªã® module æ©èœãå©çšããã®ã§ go v1.11 以äžãå¿
èŠã§ãã ããã«ã CDK ã§ã¯ Python ã Java çæ§ã
ãªèšèªã䜿ããŸãããããã§ã¯ TypeScript ãçšã㊠CDK ã³ãŒããèšè¿°ããŠãããŸãã æåã« 1. ã®é¢æ°ãèŒããŸãã ããããããšãšããŠã¯ä»¥äžã®4ã€ã«ãªããŸãã ããŠã³ããŒãããã self-hosted runners ã®ããŒãžã§ã³ãååŸ å
çšååŸããããŒãžã§ã³ãæå®ã㊠self-hosted runners ãããŠã³ããŒã ããŠã³ããŒããããã®ã S3 ãã±ããã«èšçœ® èšçœ®å
ã颿°ã®è¿ãå€ãšããŠè¿ã ãããåããã³ãŒãã¯ä»¥äžã®ããã«ãªããŸãã package main import ( "context" "encoding/json" "fmt" "io/ioutil" "net/http" "os" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" ) type Response struct { Bucket string `json:"bucket"` Key string `json:"key"` } type PackageMetadata struct { TagName string `json:"tag_name"` } func Fetch(url string ) ([] byte , error ) { resp, err := http.Get(url) if err != nil { return nil , err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) } func HandleRequest(ctx context.Context) (Response, error ) { // self-hosted runners ã®ãã£ãã·ã¥å
S3 ãã±ããå㯠// ç°å¢å€æ°ãšããŠèšå®ãã bucket, has := os.LookupEnv( "BUCKET" ) if !has { return Response{}, fmt.Errorf( "No such environement: BUCKET" ) } cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return Response{}, err } client := s3.NewFromConfig(cfg) // 1. ããŠã³ããŒãããã self-hosted runners ã®ããŒãžã§ã³ãååŸ // // ããã§ã¯ææ°çãããŠã³ããŒã b, err := Fetch( "https://api.github.com/repos/actions/runner/releases/latest" ) if err != nil { return Response{}, err } m := &PackageMetadata{} if err := json.Unmarshal(b, m); err != nil { return Response{}, err } // å
é ã« `v` ãããªãã£ãã¯ã¹ãšããŠã€ããŠãã®ã§ã // ããã ãç¡èŠããŠããŒãžã§ã³ãååŸ ver := m.TagName[ 1 :] // 2. å
çšååŸããããŒãžã§ã³ãæå®ã㊠self-hosted runners ãããŠã³ããŒã b, err = Fetch( fmt.Sprintf( "https://github.com/actions/runner/releases/download/v%s/actions-runner-linux-x64-%s.tar.gz" , ver, ver) ) if err != nil { return Response{}, err } // 3. self-hosted runners ã S3 ã«èšçœ®ãã param := &s3.PutObjectInput{ Bucket: aws.String(bucket), // 決ãæã¡ã§ runner.tar.gz ãšããããŒã§ // self-hosted runners ã®ãã€ããªãŒãä¿åããŠãã Key: aws.String( "runner.tar.gz" ), Body: b, } if _, err := client.PutObject(ctx, param); err != nil { return Response{}, err } // 4. èšçœ®å
ã颿°ã®è¿åŽå€ãšããŠè¿ã return Response{Bucket: bucket, Key: "runner.tar.gz" }, nil } func main() { lambda.Start(HandleRequest) } Lambda 㯠Docker ã³ã³ãããåããã ã®ã§ã ãã®ããã® Dockerfile ãæžããŸãã FROM --platform=$TARGETPLATFORM golang:1 as build WORKDIR /usr/src/app ARG TARGETOS ARG TARGETARCH COPY go.mod go.sum ./ RUN go mod download && go mod tidy COPY . . RUN env CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -v -o /main ./... FROM --platform=$TARGETPLATFORM public.ecr.aws/lambda/provided:al2 COPY --from=build /main ${LAMBDA_RUNTIME_DIR}/bootstrap CMD [ "main" ] åãããã« 2. ã®é¢æ°ãçšæããŸãã æ³šæç¹ãšããŠã äºåã« GitHub Apps ãšã㊠self-hosted runners ãç»é²ãã Lambda 颿°ãç»é²ããå¿
èŠããããŸãã GitHub Apps ãšããŠç»é²ããéã«ã¯ä»¥äž2ç¹ãèšå®ããŠãããªããã°ãªããªãããšã«æ³šæããŠãã ããã self-hosted runners ãç»é²ãã Lambda 颿°ã self-hosted runners ãŸããã®æäœã Read/Write ã§ããããæš©éãä»äžããŠãã GitHub Organizations ã®å
šãŠã®ãªããžããªã§åäœããããã«ãããå Žåã Repository Access ã All Repositories ã«èšå®ããŠãã ãã® GitHub Apps ãžã®ç»é²ãã§ããããèµ·åããã self-hosted runners ã GitHub ãžç»é²ãã颿°ãæžããŠãããŸãã package main import ( "context" "fmt" "os" "strconv" "github.com/aws/aws-lambda-go/lambda" "github.com/bradleyfalzon/ghinstallation/v2" "github.com/google/go-github/v45/github" ) func InstallationId(org string ) ( int64 , error ) { itr, err := ghinstallation.NewAppsTransport( https.DefaultTransport, appId, privateKey, ) if err != nil { return 0 , err } client := github.NewCient(&http.Client{Transport: itr}) listOpt := &github.ListOptions{} for { is, resp, err := client.Apps.ListInstallations(ctx, listOpt) if err != nil { return 0 , err } // GitHub Apps ã®å
šãŠã®ã€ã³ã¹ããŒã«å
ããã // æå®ããã Organization ã«ã€ã³ã¹ããŒã«ãããŠãããã®ãèŠã€ã㊠// ãã®ã€ã³ã¹ããŒã«ã«ã€ãããã ID ãååŸããã for _, inst := range insts { if inst.GetAccount().GetLogin() == org { return inst.GetID(), nil } } if resp.NextPage == 0 { break } listOpt.Page = resp.NextPage } return 0 , fmt.Errorf( "cannot find installation id on %s" , org) } func HandleRequest(ctx context.Context) ( map [ string ] string , error ) { org, has := os.LookupEnv( "ORGANIZATION" ) if !has { return "" , fmt.Errorf( "No such environment: ORGANIZATION" ) } appIdStr, has := os.LookupEnv( "GITHUB_APPS_APP_ID" ) if !has { return "" , fmt.Errorf( "No such environment: GITHUB_APPS_APP_ID" ) } appId, err = strconv.ParseInt(appIdStr, 10 , 64 ) if err != nil { return "" , fmt.Errorf( "GITHUB_APPS_APP_ID is not integer: %s" , appIdStr) } privateKey, has := os.LookupEnv( "GITHUB_APPS_APP_PRIVATE_KEY" ) if !has { return "" , fmt.Errorf( "No such environment: GITHUB_APPS_APP_PRIVATE_KEY" ) } instId, err := InstallationId(org) if err != nil { return "" , err } // 2. self-hosted runners ãç»é²ããéã«å¿
èŠãšãªãããŒã¯ã³ã®çºè¡ itr, err := ghinstallation.New(http.DefaultTransport, appId, instId, privateKey) if err != nil { return "" , err } client := github.NewCient(&http.Client{Transport: itr}) resp, _, err := client.Actions.CreateOrganizationRegistrationToken(ctx, org) if err != nil { return "" , err } return resp.GetToken(), nil } func main() { lambda.Start(HandleRequest) } å
è¿°ãã2ã€ã®é¢æ°ã CDK ãšããŠèŒããŸãã import * as cdk from "aws-cdk-lib" ; import * as { Construct } from "constructs" ; import * as path from "path" ; // self-hosted runners ãããŠã³ããŒããã Lambda 颿° export class RunnerDownloader extends cdk.aws_lambda.DockerImageFunction { constructor( scope: Construct , id: string ) { super( scope , id , { code: cdk.aws_lambda.DockerImageCode.fromImageAsset ( // Lambda 颿°ã®ã³ãŒãã®èšçœ®å
ã¯ãã®ãœãŒã¹ãã¡ã€ã«ãšåãéå±€ã«ãã // `handler/runner-downloader` ã« path.join ( __dirname , "handler" , "runner-downloader" ), ), timeout: cdk.Duration.minutes ( 5 ), memorySize: 1024 , } ); const storage = new cdk.aws_s3.Bucket ( this , "Storage" , { lifecycleRules: [ // 7æ¥éã ãã¯ãã£ãã·ã¥ããŠãããã // 7æ¥è¶
ãããææ°ã®ãã®ãååŸ { enabled: true , expiration: cdk.Duration.days ( 7 ), } ] , // ãã£ãã·ã¥ãšããŠããå©çšããªãã®ã§ãã¹ã¿ãã¯ã®å逿ã«ã¯ // ãã®ãã±ããå
ã®ãªããžã§ã¯ããå
šãŠåé€ãã autoDeleteObjercts: true , removalPolicy: cdk.RemovalPolicy.DESTROY , } ); storage.grantRead ( this ); storage.grantPut ( this ); this .addEnvironment ( "BUCKET" , storage.bucketName ); } } export type RegistrationTokenGeneratorProps = { githubAppsPrivateKey: string ; githubAppsAppId: string ; } ; // self-hosted runners ãç»é²ããããã®ããŒã¯ã³ãçæãã Lambda 颿° export class RegistrationTokenGenerator extends cdk.aws_lambda.DockerImageFunction { constructor( scope: Construct , id: string , props: RegistrationTokenGeneratorProps ) { super( scope , id , { code: cdk.aws_lambda.DockerImageCode.fromImageAsset ( // Lambda 颿°ã®ã³ãŒãã®èšçœ®å
ã¯ãã®ãœãŒã¹ãã¡ã€ã«ãšåãéå±€ã«ãã // `handler/registration-token-generator` ã« path.join ( __dirname , "handler" , "registration-token-generator" ) ), timeout: cdk.Duration.minutes ( 5 ), environment: { GITHUB_APPS_APP_ID: props.githubAppsAppId , GITHUB_APPS_PRIVATE_KEY: props.githubAppsPrivateKey , } , memorySize: 1024 , } ); } } çšæã§ããã self-hosted runners ãèµ·åããã¹ã¯ãªãããçµã¿ãŸãã #!/usr/bin/env bash function setup() { local -r \ runner_downloader="$1" \ registration_token_generator="$2" \ organization="$3" \ runner_directory="$4" \ mkdir "$runner_directory" && cd "$runner_directory" || return 1 # self-hosted runners ãååŸãã local -r runner_json=/tmp/runner.json aws lambda invoke \ --function-name "$runner_downloader" \ --payload "{}" \ --cli-binary-format raw-in-base64-out \ "$runner_json" local -r runner_archive=/tmp/runner.tar.gz aws s3api get-object \ --bucket "$(jq -r .bucket "$runner_json")" \ --key "$(jq -r .key "$runner_json")" \ "$runner_archive" tar zxf "$runner_archive" # self-hosted runners ã®ç»é²çšããŒã¯ã³çºè¡ local -r registration_token_json=/tmp/registration-token.json aws lambda invoke \ --function-name "$registration_token_generator" \ --payload "{}" \ --cli-binary-format raw-in-base64-out \ "$registration_token_json" local registration_token registration_token="$(jq -r .token "$registration_token_json")" # runner ã®åæèšå®ãè¡ã ./config.sh \ --url "https://github.com/$organization" \ --disable-update \ --ephemeral \ --unattended } function main() { local -r \ runner_downloader="$1" \ registration_token_generator="$2" \ organization="$3" # self-hosted runners ãèµ°ãããããã®ãŠãŒã¶ãŒãäœæ useradd -m -g users -G sudo -s /bin/bash runner # self-hosted runners ãèµ·åããæºåãè¡ã local runner_directory=~runner/runner su \ - runner \ -c "$(declare -f setup); setup \"$runner_downloader\" \"$registration_token_generator\" \"$organization\" \$runner_directory\"" cd "$runner_directory" && ./bin/installdependencies.sh # self-hosted runners ãèµ·å # self-hosted runners ãã¿ã¹ã¯ãèµ°ãçµããã poweroff ã㊠VM ãçµäºããã su - runner -c "cd '$runner_directory' && ./run.sh" \ && systemctl poweroff } main "$@" ããšã¯ãã®ã¹ã¯ãªãããå©çšã㊠self-hosted runners ããŒããšãªã EC2 ã€ã³ã¹ã¿ã³ã¹ã®ããã® Launch Template ãäœæããŸãã import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; import * as path from "path" ; // å
çšçšæããè£å©é¢æ°ãã€ã³ããŒãããã import { RunnerDownloader } from "./runner-downloader" ; import { RegistrationTokenGenerator } from "./registration-token-generator" ; export type LaunchTemplateProps = { runnerDownloader: RunnerDownloader ; registrationTokenGenerator: RegistrationTokenGenerator ; organization: string ; } ; export class LaunchTemplate extends cdk.Resource { public readonly launchTemplate: cdk.aws_ec2.LaunchTemplate ; constructor( scope: Construct , id: string , props: LaunchTemplateProps ) { super( scope , id ); const role = new cdk.aws_iam.Role ( this , "Role" , { assumedBy: new cdk.aws_iam.ServicePrincipal ( "ec2.amazonaws.com" ), managedPolicies: [ cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName ( "AmazonSSMManagedInstanceCore" ), ] , } ); const asset = new cdk.aws_s3_assets.Asset ( this , "UserData" , { // å
çšã®ã¹ã¯ãªããã start.bash ãšããååã§ä¿åããŠããã path: path.join ( __dirname , "start.bash" ), } ); asset.grantRead ( role ); const userData = cdk.aws_ec2.UserData.forLinux ( { shebang: "#!/usr/bin/env bash" , } ); const filePath = this .userData.addS3DownloadCommand ( { bucket: asset.bucket , bucketKey: asset.s3ObjectKey , } ); userData.addExecuteFileCommand ( { filePath , arguments : [ props.runnerDownloader.functionArn , props.registrationTokenGenerator.funcitonArn , organization , ] .join ( " " ), } ); this .launchTemplate new cdk.aws_ec2.LaunchTemplate ( this , "LaunchTemplate" , { role , instanceType: cdk.aws_ec2.InstanceType. of( cdk.aws_ec2.InstanceClass.T3 , cdk.aws_ec2.InstanceSize.SMALL , ), // ããã«ã¯ GitHub Actions ã§å©çšããã€ã¡ãŒãžãæå®ããã // ãã ããå
çšã®ã¹ã¯ãªããå
ã§ awscli v2 ãš jq ãå©çšããŠããã®ã§ã // ããããå«ããäžã§ã GitHub Actions ã§ãµããŒããããŠãã // ã¿ã€ãã® OS ãæå®ããå¿
èŠãããã // // åè: https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#supported-architectures-and-operating-systems-for-self-hosted-runners machineImage: GithubActionsMachineImage , userDate , ebsOptimized: true , // ã·ã£ããããŠã³æã«ã¯ã€ã³ã¹ã¿ã³ã¹ã terminate ãããã instanceInitiatedShutdownBehavior: cdk.aws_ec2.InstanceInitiatedShutdownBehavor.TERMINATE , blockDevices: [ { deviceName: "/dev/sda1" , volume: cdk.aws_ec2.BlockDeviceVolume.ebs ( 100 , { volumeType: cdk.aws_ec2.EvsDeviceVolumeType.GP3 , deleteOnTermination: true , encrypted: true , } ) } ] } ); } } 次ã«ãã® Launch template ããã€ã³ã¹ã¿ã³ã¹ãèµ·åãã Lambda 颿°ãæžããŸãã ãŸãã¯é¢æ°ã®ã³ãŒããçšæããŸã(ãã®é¢æ°ãå
çšãŸã§ã®é¢æ°ãšåãã§ Lambda ã³ã³ããã€ã¡ãŒãžãšããŠæäŸããŸã)ã package main import ( "context" "fmt" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" ) // ããšã§ãã®é¢æ°ã SQS ã«æ¥ç¶ãã SQS ããã€ãã³ãããããšã£ããèµ·åããããã«ããããã // ããã§ã¯ events.SQSEvent ãåãåãããã«ããŠããŸãã func HandleRequest(ctx context.Context, req events.SQSEvent) error { ltId, has := os.LookupEnv( "LAUNCHTEMPLATE_ID" ); if !has { return fmt.Errorf( "No such environment: LAUNCHTEMPLATE_ID" ); } ltVer, has := os.LookupEnv( "LAUNCHTEMPLATE_VERSION" ); if !has { return fmt.Errorf( "No such environment: LAUNCHTEMPLATE_VERSION" ) } snId, has := os.LookupEnv( "SUBNET_ID" ); if !has { return fmt.Errorf( "No such environment: SUBNET_ID" ); } cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return err } client := ec2.NewFromConfig(cfg) for range req.Records { param := &ec2.RunInstancesInput{ LaunchTemplate: &types.LaunchTemplateSpecification{ LaunchTemplateId: aws.String(ltId), Version: aws.String(ltVer), }, SubnetId: aws.String(snId), MaxCount: aws.Int32( 1 ), MinCount: aws.Int32( 1 ), } resp, err := client.RunInstances(ctx, param) if err != nil { return err } } return nil } func main() { lambda.Start(HandleRequest) } ãã®é¢æ°ã® CDK ã³ãŒããçšæããŸãã // å
çšã® LaunchTemplate ã¯ã©ã¹ãã€ã³ããŒãããã import { LaunchTemplate } from "./launch-template" ; export type InstanceLauncherProps = { launchTemplate: LaunchTemplate ; subnet: cdk.aws_ec2.ISubnet ; } ; // self-hosted runners ãç»é²ããããã®ããŒã¯ã³ãçæãã Lambda 颿° export class InstanceLauncher extends cdk.aws_lambda.DockerImageFunction { constructor( scope: Construct , id: string , props: InstanceLauncherProps ) { super( scope , id , { code: cdk.aws_lambda.DockerImageCode.fromImageAsset ( // Lambda 颿°ã®ã³ãŒãã®èšçœ®å
ã¯ãã®ãœãŒã¹ãã¡ã€ã«ãšåãéå±€ã«ãã // `handler/registration-token-generator` ã« path.join ( __dirname , "handler" , "instance-launcher" ) ), timeout: cdk.Duration.minutes ( 5 ), environment: { LAUNCHTEMPLATE_ID: props.launchTemplate.launchTemplateId , LAUNCHTEMPLATE_VERSION: props.launchTemplate.latestVersionNumber , SUBNET_ID: props.subnet.subnetId , } , memorySize: 1024 , } ); this .addToRolePolicy ( new cdk.aws_iam.PolicyStatement ( { resources: [ "*" ] , // EC2 ã€ã³ã¹ã¿ã³ã¹ãç«ã¡äžããã«ã¯ `ec2:RunInstances` ãå¿
èŠã // ãŸãã ç«ã¡äžãã EC2 ã€ã³ã¹ã¿ã³ã¹ã«ããŒã«ãä»äžããããã« `iam:PassRole` ãå¿
èŠã§ã // ããã«ã¿ã°ã Launch Template ãéããŠã€ãããããããã«ã¯ CreateTags ãå¿
èŠãšãªãã actions: [ "ec2:RunInstances" , "iam:PassRole" , "ec2:CreateTags" ] } ) ); } } æåŸã«ãããããŸãšããã³ã³ããŒãã³ããäœæããŸãã import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; import { InstanceLauncher } from "./instance-launcher" ; import { RunnerDownloader } from "./runner-downloader" ; import { RegistrationTokenGenerator } from "./registration-token-generator" ; import { LaunchTemplate } from "./LaunchTemplate" ; export type SelfHostedRunnerProps = { githubAppsAppId: string ; githubAppsPrivateKey: string ; organization: string ; subnet: cdk.aws_ec2.ISubnet ; } ; export class SelfHostedRunner extends cdk.Resource { public readonly instanceLauncher: InstanceLauncher ; constructor( scope: Construct , id: string , props: SelfHostedRunnerProps ) { super( scope , id ); const runnerDownloader = new RunnerDownloader ( this , "RunnerDownloader" ); const registrationTokenGenerator = new RegistrationTokenGenerator ( this , "RegistrationTokenGenerator" , { githubAppsAppId: props.githubAppsAppId , githubAppsPrivateKey: props.githubAppsPrivateKey , } ); const launchTemplate = new LaunchTemplate ( this , "LaunchTemplate" , { runnerDownloader , registrationTokenGenerator , organization: props.organization , } ); this .instanceLauncher = new InstanceLauncher ( this , "InstanceLauncher" , { launchTemplate , subnet: props.subnet , } ); } } Webhook self-hosted runners ããŒããèµ·åããéšåã®äœæãã§ããã®ã§ãä»åºŠã¯ Webhook ãåä¿¡ããéšåãäœæããŠããŸãã æåã«ã Webhook ã€ãã³ããéä¿¡ãã SQS ãšãå
çšçšæãã self-hosted runners ããŒããèµ·åãã Lambda 颿°ããã® SQS ã®ã¡ãã»ãŒãžåä¿¡ãããèµ·åããããã«ããèšå®ããŸãã import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; // instance ãç«ã¡äžãã Lambda 颿°ãã€ã³ããŒã import { InstanceLauncher } from "./instance-launcher" ; export type InstanceLaunchQueueProps = { // ããããã£ã§ self-hosted runners ããŒããèµ·åãã Lambda 颿°ãæž¡ããŠããã instanceLauncher: InstanceLauncher ; } export class InstanchLaunchQueue extends cdk.aws_sqs.Queue { constructor( scope: Construct , id: string , props: InstanceLaunchQueueProps ) { super( scope , id , { visibilityTimeout: cdk.Duration.minutes ( 5 ) } ); // SQS ã®ã€ãã³ããåä¿¡ã㊠EC2 ã€ã³ã¹ã¿ã³ã¹ãç«ã¡äžãã Lambda 颿°ãåŒã¶ props.intanceLauncher.addEventSource ( new cdk.aws_lambda_event_sourecs.SqsEventSource ( this ) ); } } 次㫠Webhook ã®åä¿¡éšåãšãªã API Gateway ã«ãã Web API ã®äœæãšããã®è£ã§åãã€ãã³ãã®ãã£ã«ã¿ãªã³ã°ãè¡ã£ãããšã« SQS ãž Webhook ã€ãã³ããéä¿¡ãã Lambda 颿°ãå®è£
ããŸãã ã€ãã³ãã®ãã£ã«ã¿ãªã³ã°æ¹æ³ã§ããã å
è¿°ãããšããã workflow_job ã€ãã³ããå©çšããŸãã ãã® workflow_job ã€ãã³ãã®ãã€ããŒãã«ã¯ workflow ãšãããªããžã§ã¯ããå«ãŸããŠããããã®äžã« status ãšããããããã£ããããŸãã ãã® status ããããã£ã¯ CI/CD ãžã§ãã®ã¹ããŒã¿ã¹ã衚ããŠããŸãã ã¹ããŒã¿ã¹ãšããŠã¯æ¬¡ã®3ã€ãçšæãããŠããŸãã queued : ãžã§ããéå§ãããã®ã®ããŸã runner ãå²ãåœãŠãããŠããªãç¶æ
in_progress : ãžã§ãã« runner ãå²ãåœãŠãããŠçŸåšãžã§ããèµ°ã£ãŠããç¶æ
completed : ãžã§ããå®äºããããšã衚ãç¶æ
ãã®ãã¡ã queued ãä»åå¿
èŠã«ãªãã€ãã³ãã§ããããããã以å€ã®ãã®ããŸãã¯ã¯ããããšãå¿
èŠã§ãã ããã«ã workflow_job ã€ãã³ãã®ãã€ããŒãäžã«ãã workflow ãªããžã§ã¯ãã«ã¯ labels ãšããæåé
åããããã£ãããã self-hosted runners ã§èµ°ããã CI/CD ãžã§ããéå§ããå Žåã¯ãã®é
åäžã« self-hosted ãšããã©ãã«ãå
¥ã£ãŠããŸãã ãªã®ã§ã self-hosted runners ã§èµ°ããžã§ãã ããæœåºããéã«ã¯ãã® labels ããããã£ã®èŠçŽ ãèŠãŠãããå¿
èŠããããŸãã ããããå
ã«å¿
èŠãšãªã Webhook ã€ãã³ãã®ã¿ãæœåºãã self-hosted runners ããŒããèµ·åãããšãããŸã§ãäœæããŸã 5 ã ãŸããã€ãã³ããã£ã«ã¿ãªã³ã°ãããããšã« SQS ãžã¡ãã»ãŒãžãéä¿¡ãã Lambda 颿°ã®å®è£
ã¯æ¬¡ã®éãã§ãã package main import ( "context" "fmt" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/google/go-github/v45/github" ) // ããšã§ API Gateway ãããã®é¢æ°ãåŒã³åºã func HandleRequest(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error ) { qUrl, has := os.LookupEnv( "QUEUE_URL" ) if !has { resp := events.APIGatewayProxyResponse{ StatusCode: 500 , Body: "No such environment: QUEUE_URL" , } return resp, fmt.Errorf( "No such environment: QUEUE_URL" ) } // ã€ãã³ãã®ãã£ã«ã¿ãªã³ã° var ev github.WorkflowJobEvent if err := json.Unmarshal(req.Body, &ev); err != nil { // workflow_job ã€ãã³ã以å€ã¯å
šéšç¡èŠ resp := events.APIGatewayProxyResponse{ StatusCode: 200 , Body: "ignore event" , } return resp, nil } if stat := ev.GetWorkflowJob().GetStatus(); stat != "queued" { // queued ãªãã®ä»¥å€ã¯å
šéšç¡èŠ resp := events.APIGatewayProxyResponse{ StatusCode: 200 , Body: "ignore event" , } return resp, nil } isShr := false for _, label := range ev.GetWorkflowJob().Labels { if label == "self-hosted" { isShr = true break } } if !isShr { // queued ãªãã®ä»¥å€ã¯å
šéšç¡èŠ resp := events.APIGatewayProxyResponse{ StatusCode: 200 , Body: "ignore event" , } return resp, nil } // Queue ã«ã¡ãã»ãŒãžãéä¿¡ cfg, err := config.LoadDefaultConfig(ctx) if err != nil { resp := events.APIGatewayProxyResponse{ StatusCode: 500 , Body: fmt.Sprintf( "%v" , err), } return resp, err } client := sqs.NewFromConfig(cfg) param := &sqs.SendMessageInput{ QueueUrl: aws.String(qUrl), // Queue ããã€ãã³ããã§ãã°ãã®ãŸãŸ self-hosted runners ãç«ã¡äžããããã // ã¡ãã»ãŒãžã¯äœã§ãããããã®ãããããã§ã¯ `{}` ãã¡ãã»ãŒãžãšããŠéä¿¡ããŠããã MessageBody: aws.String( "{}" ), } if _, err = client.SendMessage(ctx, param); err != nil { resp := events.APIGatewayProxyResponse{ StatusCode: 500 , Body: fmt.Sprintf( "%v" , err), } return resp, err } resp := events.APIGatewayProxyResponse{ StatusCode: 200 , Body: "OK" , } return resp, nil } func main() { lambda.Start(HandleRequest) } ãã®é¢æ°ãããããžã§ãã³ã°ããããã® CDK ã³ãŒãã¯æ¬¡ã®éãã§ãã import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; import * as path from "path" ; import { InstanceLaunchQueue } from "./instance-launch-queue" ; export type EventPublisherProps = { queue: InstanceLaunchQueue ; } ; export class EventPublisher extends cdk.aws_lambda.DockerImageFunction { constructor( scope: Construct , id: string , props: EventPublisherProps ) { super( scope , id , { code: cdk.aws_lambda.DockerImageCode.fromImageAsset ( // Lambda 颿°ã®ã³ãŒãã®èšçœ®å
ã¯ãã®ãœãŒã¹ãã¡ã€ã«ãšåãéå±€ã«ãã // `handler/event-publisher` ã« path.join ( __dirname , "handler" , "event-publisher" ) ), timeout: cdk.Duration.minutes ( 5 ), environment: { QUEUE_URL: props.queue.queueUrl , } , memorySize: 1024 , } ); this .addToRolePolicy ( new cdk.aws_iam.PolicyStatement ( { resources: [ "*" ] , actions: [ "sqs:SendMessage" ] } ) ); } } æåŸã«ã Webhook ã®åãå£ãšãªã API Gateway ãèšçœ®ããçšæãã Lambda 颿°ãåŒã³åºãããã«èšå®ããã°äœæå®äºã§ãã import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; // äœæãããã®ãã€ã³ããŒã import { EventPublisher } from "./event-publisher" ; import { InstanceLaunchQueue } from "./instance-launch-queue" ; import { InstanceLauncher } from "./instance-launcher" ; export type WebhookProps = { instanceLauncher: InstanceLauncher ; } ; export class Webhook extends cdk.Resource { public readonly gateway: cdk.aws_apigateway.LambdaRestApi ; constructor( scope: Construct , id: string , props: InstanceLauncher ) { super( scope , id ); const queue = new InstanceLaunchQueue ( this , "Queue" , { instanceLauncher: props.instanceLauncher , } ); const eventPublisher = new EventPublisher ( this , "EventPublisher" , { queue , } ); this .gateway = new cdk.aws_apigateway.LambdaRestApi ( this , "Webhook" , { handler: eventPublisher } ); } } ããšã¯ GitHub åŽã§ Webhook ãäœæããããã§ãããã€ããã API Gateway ã® URL ã Webhook ãšããŠç»é²ããã°å®äºã§ãã ãŸãšã ãããŸã§ self-hosted runners ã AWS äžã« CDK ãçšããŠæ§ç¯ããæ¹æ³ã«ã€ããŠè§£èª¬ããŸããã å€å°è€éãªæ©æ§ãšãªã£ãŠã¯ããŸãããããããããšãšããŠã¯ Webhook ãã CI/CD ãžã§ãã®éå§ã€ãã³ããããã self-hosted runners ãç«ã¡äžããã ãã§ãã ããã§ã¯è§£èª¬ããŠããŸããããå€å°æ¡åŒµããŠããã° arm64 ã¢ãŒããã¯ãã£ãª self-hosted runners ç°å¢ãäœæãããã Windows ç°å¢ãçšæããŠå®è¡ãããã§ããŸãã ãã®ç°å¢ãããŒã¹ãšããŠçããã§ããè¯ãéçºç°å¢ãäœæããŠããã ããã°ãšæããŸãã https://docs.github.com/ja/actions/using-github-hosted-runners/about-github-hosted-runners ↩ self-hosted runners ã®ç«ã¡äžã㯠Webhook ã€ãã³ãã®åä¿¡ãšã¯å¥ã®æäœã«ãªããŸãããææªãã®åŸã®åŠçã§ EC2 ã€ã³ã¹ã¿ã³ã¹ãç«ã¡äžãããªããŠã GitHub Actions ã®ãžã§ãã¯72æéã§å€±æãšããŠãšããã€ããããŸããããã«ãžã§ãã¯å床æµããã®ã§ã self-hosted runners ç°å¢ãå床ç«ã¡äžããŠãžã§ããèµ°ãããããšãã§ããã°ãããããåãé¢ãããšã«ããåé¡ã¯ãããŸã§å€§ããã¯ãªããšæããŸãã ↩ https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-configure-lambda-function-trigger.html ↩ Auto Scaling Group ã䜿ãã°åãããã«ã€ã³ã¹ã¿ã³ã¹ã®èµ·åãããæ¥œã«è¡ããŸãããèµ·åããã€ã³ã¹ã¿ã³ã¹ã®ç®¡çãç
©éã«ãªããŸããå°ã詊ããŠã¿ãã°ããããŸãããã€ã³ã¹ã¿ã³ã¹æ°ãå¢ããã®ã¯ãšãŠãç°¡åãªäžæ¹ãã€ã³ã¹ã¿ã³ã¹æ°ãæžããã®ã¯ãšãŠãé£ããã§ããããšãã°ã倧æµã® CI/CD ãžã§ãã¯ããã«çµäºããããšãå€ããããç¡é§ãªã³ã¹ãããããªãããã«åæ¥ã«ã¯ self-hosted runners ç°å¢ãæäŸãã EC2 ã€ã³ã¹ã¿ã³ã¹ã®æ°ã0ã«ãããšããŸãããããã ML ãã¬ãŒãã³ã°ããã€ã³ã¹ã¿ã³ã¹çæéã®ããã CI/CD ãžã§ããéæå€ã«æå
¥ãããåæã«ãžã§ããå²ã蟌ãã ãšããäžèšã«ãŒã«ã«åãã€ã³ã¹ã¿ã³ã¹ãçµäºããããããžã§ããäžæãããããŠããŸããŸãããããã¡ãããšã©ã®ã€ã³ã¹ã¿ã³ã¹ãèµ°ã£ãŠããã®ããææ¡ããã®ã¯é£ãããããé©åãªãžã§ã管çãããä»çµã¿ãå°å
¥ããªããã°ãªããŸããããªã®ã§ã Auto Scaling Group ã䜿ããã«ãžã§ãã®èŠæ±ã«å¿ã㊠Launch Template ããèµ·åããããã«ããŠããããžã§ããèµ°ã£ãåŸã¯ãã®ãŸãŸ EC2 ã€ã³ã¹ã¿ã³ã¹ãçµäºããããã«ããã°è€éãªãžã§ãã®ç®¡çã¯å¿
èŠãªããªããŸãã ↩ å®ã¯ GitHub ã«ã¯ Organization ã§å
±æããŠå©çšã§ãã self-hosted runners ã®ä»ã«ããªããžããªãŒæ¯ã«ã self-hosted runners ãç»é²ã§ããŸãã workflow_job ã€ãã³ãã§ã¯ãããã®åºå¥ãã€ããªãããããªããžããªãŒæ¯ã«ç»é²ããŠãã self-hosted runners ãå©çšãããã®ãã Organization ã§å
±æããŠå©çšã§ãã self-hosted runners ãèµ·åãããã®ããåºå¥ããããšã¯é£ããã§ããç°¡åãªè§£æ±ºçãšããŠã¯å self-hosted runners ã« ã©ãã« ãç»é²ã§ããã®ã§ããããå©çšããŠãããããåºå¥ããããã«ããã°è¯ãã§ãããã ããä»å瀺ãäŸã§ã¯ãã®ãããªãªããžããªãŒæ¯ã«ç»é²ãããŠãã self-hosted runners ã¯ãªããã®ãšããŠé²ããŸãã ↩