Skip to content

Bidirectional Jira ↔ AWS DevOps Agent Integration

11 minute read
Content level: Advanced
0

AWS DevOps Agent can investigate incidents autonomously, but it doesn't have a native Jira integration. This article shows how to connect the two bidirectionally

Authors: Manish Mishra (Sr TAM) & Ayush Bhambhani (TAM)

Introduction

AWS DevOps Agent can investigate incidents autonomously, but it doesn't have a native Jira integration. This article shows how to connect the two bidirectionally:

  1. Jira → DevOps Agent: A new Jira ticket triggers an investigation via webhook
  2. DevOps Agent → Jira: Investigation results are posted back as a comment on the ticket

The entire AWS-side infrastructure deploys with a single CloudFormation command — three parameters, no intermediate services.

The full bidirectional flow

Jira                                         AWS Account
────                                         ───────────

1. Jira ticket created      ── webhook ──►   DevOps Agent Webhook
   (automation triggers        (API Key)     (generic endpoint)
    "Send web request")                           │
                                                  ▼
                                             DevOps Agent
                                             (investigates autonomously)
                                                  │
                                                  ▼
2. Comment posted           ◄── Jira API ──  Callback Lambda
   with RCA findings                            ▲
                                                │ triggered by
                                              EventBridge
                                              (Investigation Completed /
                                               Linked / Failed / Cancelled)

What you get

A CloudFormation stack that creates:

ResourceWhat it does
Secrets ManagerStores Jira base URL, email, and API token
IAM RoleRead-only access to DevOps Agent API + specific secret
Lambda FunctionFetches investigation summary, handles linked tickets, posts to Jira
EventBridge RuleCatches all 6 investigation lifecycle events
Lambda PermissionAllows EventBridge to invoke the Lambda

Prerequisites

  1. An AWS DevOps Agent Space with an API Key webhook configured
  2. A Jira Cloud API token (create one here)
  3. The Jira bot account needs Add Comments permission on your project(s)
  4. AWS CLI configured with permissions to deploy CloudFormation stacks

⚠️ The DevOps Agent webhook uses API Key authentication (not HMAC).

Deploy in one command

aws cloudformation deploy \
  --template-file cfn-jira-devops-agent-callback.yaml \
  --stack-name devops-agent-jira-callback \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    JiraBaseUrl="https://your-domain.atlassian.net" \
    JiraEmail="devops-bot@yourcompany.com" \
    JiraApiToken="your-jira-api-token" \
  --region us-east-1

Three parameters, one command — that deploys the full callback infrastructure. The API token is stored in Secrets Manager (NoEcho — won't appear in CloudFormation output). No Agent Space ID needed — the Lambda reads it from each event dynamically.

After deployment: Configure Jira

The CloudFormation stack handles the callback side (DevOps Agent → Jira). You still need to configure the trigger side (Jira → DevOps Agent) in your Jira project.

Create the Jira automation rule

  1. Go to SettingsSystemGlobal automationCreate flow/rule
  2. Choose Create from scratch
  3. Trigger: Work item created
  4. Action: Send web request

URL:

https://event-ai.<region>.api.aws/webhook/generic/<your-webhook-id>

Method: POST

Headers:

NameValue
Content-Typeapplication/json
AuthorizationBearer <your-devops-agent-api-key>

Body (Custom data):

{
  "eventType": "incident",
  "incidentId": "{{issue.key}}-{{now.toEpochMillis()}}",
  "action": "created",
  "priority": "{{issue.priority.name.toUpperCase()}}",
  "title": "[Jira {{issue.key}}] {{issue.summary}}",
  "description": "{{issue.description}}",
  "timestamp": "{{now.format(\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\")}}",
  "service": "{{issue.components.first.name}}",
  "data": {
    "metadata": {
      "jira_issue_key": "{{issue.key}}",
      "project": "{{issue.project.key}}",
      "reporter": "{{issue.reporter.displayName}}"
    }
  }
}
  1. Name the rule and enable it.

Priority must be uppercase (HIGH, CRITICAL, not High). Use .toUpperCase().

Epoch in incidentId prevents DevOps Agent from deduplicating repeated triggers on the same ticket.

How the Lambda handles each event

EventWhat gets posted to Jira
CompletedFull RCA summary on the original ticket + all linked tickets
Linked"Linked to original investigation from: [ORIGINAL-KEY]" + reason
Failed"Could not complete - investigate manually" + reason
Timed Out"Timed out" + reason
Cancelled"Cancelled" + reason (e.g., USER_CANCELED)
Skipped"Skipped based on criteria" + reason

Linked ticket handling

When DevOps Agent links a new incident to a previous investigation:

  • The linked ticket's comment includes which original ticket it's linked to
  • When the primary investigation completes, all linked tickets get the findings too

End-to-end

  1. Create a Jira ticket with High priority
  2. Check Jira automation audit log for HTTP 200
  3. Wait for investigation to complete
  4. Verify a comment appears on the ticket with the findings

Cleanup

aws cloudformation delete-stack \
  --stack-name devops-agent-jira-callback \
  --region us-east-1

This removes the Lambda, IAM role, EventBridge rule, secret, and all permissions.

Optional: Kiro automated setup

If you use Kiro, download both files:

  1. cfn-jira-devops-agent-callback.yaml — place in your workspace root
  2. Save the steering file below as .kiro/steering/jira-devops-agent-integration.md

Then tell Kiro: "Install the Jira DevOps Agent integration". It will collect your credentials, deploy the stack, and walk you through the Jira configuration.

Steering file

# Jira ↔ DevOps Agent Bidirectional Integration

## When to use

Use this steering when the user asks to:
- Install Jira DevOps Agent integration
- Set up Jira callback for DevOps Agent
- Connect Jira to DevOps Agent
- Deploy the Jira investigation integration
- Set up bidirectional Jira DevOps Agent

## What this does

Deploys a CloudFormation stack that:
1. Creates a Lambda that posts DevOps Agent investigation results back to Jira tickets
2. Creates an EventBridge rule that catches all investigation lifecycle events
3. Stores Jira credentials in Secrets Manager

Then guides the user through configuring the Jira automation rule (trigger side).

## Prerequisites

Before starting, confirm the user has:
- [ ] Both files in their workspace:
  - `.kiro/steering/jira-devops-agent-integration.md` (this file)
  - `cfn-jira-devops-agent-callback.yaml` (CloudFormation template)
- [ ] A DevOps Agent Space with an **API Key** webhook (not HMAC)
- [ ] Their webhook URL and API key
- [ ] A Jira Cloud API token
- [ ] The Jira bot account email
- [ ] Their Jira base URL (e.g., `https://org.atlassian.net`)

## Steps

### Step 1: Collect credentials

Ask the user for:
1. **Jira base URL** — e.g., `https://yourorg.atlassian.net`
2. **Jira email** — the account that will post comments
3. **Jira API token** — from https://id.atlassian.com/manage-profile/security/api-tokens

### Step 2: Deploy the CloudFormation stack

Run:

`aws cloudformation deploy \
  --template-file cfn-jira-devops-agent-callback.yaml \
  --stack-name devops-agent-jira-callback \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    JiraBaseUrl="<user-provided>" \
    JiraEmail="<user-provided>" \
    JiraApiToken="<user-provided>" \
  --region us-east-1
`
Wait for completion and verify:

aws cloudformation describe-stacks \
  --stack-name devops-agent-jira-callback \
  --query 'Stacks[0].StackStatus' \
  --region us-east-1

Expected: CREATE_COMPLETE

### Step 3: Guide Jira automation setup

Tell the user to create a Jira automation rule:

1. **Settings** → **Automation** → **Global automation** → **Create flow**
2. Choose **Create from scratch**
3. Trigger: **Work item created**
4. Action: **Send web request**
5. Configure:
   - URL: https://event-ai.us-east-1.api.aws/webhook/generic/<their-webhook-id>
   - Method: POST
   - Headers: Content-Type: application/json and Authorization: Bearer <their-api-key>
   - Body: Use the webhook payload from the main article

6. Name: "Trigger DevOps Agent Investigation"
7. Enable the rule

### Step 4: Test

Ask the user to create a test Jira ticket with High priority, then verify a task was created in DevOps Agent and a comment appears on the ticket after investigation completes.

### Step 5: Confirm

- The integration is live
- All investigation events will post comments to Jira
- Linked tickets will show which original ticket they're linked to
- To remove: aws cloudformation delete-stack --stack-name devops-agent-jira-callback --region us-east-1

## Important notes

- Webhook must be **API Key** type. HMAC will not work.
- Priority must be **uppercase** (HIGH, CRITICAL, not High).
- The Lambda is space-independent — no agent space ID needed.

The complete CloudFormation template

Save as cfn-jira-devops-agent-callback.yaml and deploy with the command at the top of this article.

AWSTemplateFormatVersion: '2010-09-09'
Description: >
  DevOps Agent Jira Callback - Automatically posts investigation results
  back to Jira tickets as comments. Handles all investigation lifecycle
  events (Completed, Linked, Failed, Cancelled, Timed Out, Skipped).

Parameters:
  JiraBaseUrl:
    Type: String
    Description: Jira Cloud base URL (e.g., https://yourorg.atlassian.net)
    AllowedPattern: ^https://.+$

  JiraEmail:
    Type: String
    Description: Email for the Jira bot account that posts comments

  JiraApiToken:
    Type: String
    NoEcho: true
    Description: Jira API token (from id.atlassian.com/manage-profile/security/api-tokens)

Resources:

  # --- Secrets Manager ---
  JiraSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: devops-agent-jira-callback
      Description: Jira API credentials for DevOps Agent callback
      SecretString: !Sub |
        {
          "jiraBaseUrl": "${JiraBaseUrl}",
          "email": "${JiraEmail}",
          "apiToken": "${JiraApiToken}"
        }

  # --- IAM Role ---
  CallbackLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: devops-agent-jira-callback-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: devops-agent-jira-access
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - aidevops:ListJournalRecords
                  - aidevops:GetBacklogTask
                  - aidevops:ListBacklogTasks
                Resource: !Sub arn:aws:aidevops:${AWS::Region}:${AWS::AccountId}:agentspace/*
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:devops-agent-jira-callback-*
```

```yaml
  # --- Lambda Function ---
  CallbackLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: devops-agent-jira-callback
      Runtime: python3.12
      Handler: index.lambda_handler
      Timeout: 60
      MemorySize: 128
      Role: !GetAtt CallbackLambdaRole.Arn
      Environment:
        Variables:
          REGION: !Ref AWS::Region
          JIRA_SECRET_ID: devops-agent-jira-callback
      Code:
        ZipFile: |
          import json
          import os
          import re
          import base64
          import boto3
          from urllib import request, error

          secrets_client = boto3.client('secretsmanager')
          devops_client = boto3.client('devops-agent', region_name=os.environ['REGION'])
          _jira_config = None

          def get_jira_config():
              global _jira_config
              if _jira_config is None:
                  resp = secrets_client.get_secret_value(SecretId=os.environ['JIRA_SECRET_ID'])
                  _jira_config = json.loads(resp['SecretString'])
              return _jira_config

          def get_summary(agent_space_id, execution_id):
              for record_type in ['investigation_summary_md', 'investigation_result']:
                  try:
                      resp = devops_client.list_journal_records(
                          agentSpaceId=agent_space_id, executionId=execution_id, recordType=record_type)
                      records = resp.get('records', [])
                      if records:
                          content = records[0].get('content', '')
                          try:
                              parsed = json.loads(content)
                              return parsed.get('text', content)
                          except (json.JSONDecodeError, TypeError):
                              return content
                  except Exception as e:
                      print(f"Error fetching {record_type}: {e}")
              return None

          def extract_jira_key(reference_id):
              if not reference_id:
                  return ''
              m = re.match(r'^([A-Z]+-\d+)', reference_id)
              return m.group(1) if m else reference_id

          def build_comment(event_type, summary, linked_from=None, reason=None):
              reason_line = f"\nReason: {reason}" if reason else ""
              if event_type == 'Investigation Completed':
                  heading = "AWS DevOps Agent - Investigation Completed"
                  body_text = summary or "Investigation completed but no summary available."
              elif event_type == 'Investigation Linked':
                  heading = "AWS DevOps Agent - Investigation Linked"
                  prefix = "This incident is similar to a previously investigated issue."
                  if linked_from:
                      prefix += f"\nLinked to original investigation from: {linked_from}"
                  if reason:
                      prefix += reason_line
                  body_text = f"{prefix}\n\n{summary}" if summary else prefix
              elif event_type == 'Investigation Failed':
                  heading = "AWS DevOps Agent - Investigation Failed"
                  body_text = f"The automated investigation could not complete. Please investigate manually.{reason_line}"
              elif event_type == 'Investigation Timed Out':
                  heading = "AWS DevOps Agent - Investigation Timed Out"
                  body_text = f"The automated investigation timed out. Please investigate manually.{reason_line}"
              elif event_type == 'Investigation Cancelled':
                  heading = "AWS DevOps Agent - Investigation Cancelled"
                  body_text = f"The automated investigation was cancelled.{reason_line}"
              elif event_type == 'Investigation Skipped':
                  heading = "AWS DevOps Agent - Investigation Skipped"
                  body_text = f"The investigation was skipped based on agent space criteria.{reason_line}"
              else:
                  heading = f"AWS DevOps Agent - {event_type}"
                  body_text = summary or f"Event: {event_type}{reason_line}"
              return heading, body_text

          def post_jira_comment(issue_key, heading, body_text):
              config = get_jira_config()
              base_url = config['jiraBaseUrl'].rstrip('/')
              url = f"{base_url}/rest/api/3/issue/{issue_key}/comment"
              body = {"body":{"version":1,"type":"doc","content":[
                  {"type":"heading","attrs":{"level":3},"content":[{"type":"text","text":heading}]},
                  {"type":"codeBlock","attrs":{"language":"markdown"},"content":[{"type":"text","text":body_text}]}
              ]}}
              auth = base64.b64encode(f"{config['email']}:{config['apiToken']}".encode()).decode()
              data = json.dumps(body).encode('utf-8')
              req = request.Request(url, data=data, headers={
                  'Content-Type': 'application/json', 'Authorization': f'Basic {auth}'})
              try:
                  with request.urlopen(req, timeout=15) as resp:
                      print(f"Jira comment posted: {resp.status} on {issue_key}")
                      return resp.status
              except error.URLError as e:
                  print(f"Jira POST failed for {issue_key}: {e}")
                  return None

          def lambda_handler(event, context):
              print(f"Received: {json.dumps(event)}")
              event_type = event.get('detail-type', 'Unknown')
              metadata = event['detail']['metadata']
              task_id = metadata['task_id']
              execution_id = metadata.get('execution_id', '')
              agent_space_id = metadata['agent_space_id']

              primary_key = ''
              status_reason = ''
              try:
                  resp = devops_client.get_backlog_task(agentSpaceId=agent_space_id, taskId=task_id)
                  task = resp.get('task', {})
                  ref = task.get('reference', {})
                  primary_key = extract_jira_key(ref.get('referenceId', ''))
                  status_reason = task.get('statusReason', '')
                  if not status_reason:
                      meta = task.get('metadata', {})
                      status_reason = meta.get('canceledReason', '') or meta.get('failedReason', '') or meta.get('skippedReason', '')
                  if task.get('primaryTaskId'):
                      try:
                          pr = devops_client.get_backlog_task(agentSpaceId=agent_space_id, taskId=task['primaryTaskId'])
                          original_key = extract_jira_key(pr['task']['reference'].get('referenceId', ''))
                          heading, body_text = build_comment(event_type, None, linked_from=original_key, reason=status_reason)
                          result = post_jira_comment(primary_key, heading, body_text)
                          return {'statusCode': 200, 'event_type': event_type, 'this_ticket': primary_key, 'linked_from': original_key}
                      except Exception as e:
                          print(f"Error getting primary task: {e}")
              except Exception as e:
                  print(f"Error getting task: {e}")

              if not primary_key:
                  return {'statusCode': 200, 'message': 'No issue key, skipped'}

              linked_tasks_info = {}
              try:
                  all_tasks = devops_client.list_backlog_tasks(agentSpaceId=agent_space_id)
                  for t in all_tasks.get('tasks', []):
                      if t.get('primaryTaskId') == task_id:
                          key = extract_jira_key(t.get('reference', {}).get('referenceId', ''))
                          if key:
                              linked_tasks_info[key] = t.get('statusReason', '')
              except Exception as e:
                  print(f"Error listing linked tasks: {e}")

              summary = None
              if event_type in ['Investigation Completed', 'Investigation Linked']:
                  summary = get_summary(agent_space_id, execution_id)

              results = {}
              heading, body_text = build_comment(event_type, summary, reason=status_reason)
              results[primary_key] = post_jira_comment(primary_key, heading, body_text)
              for linked_key, linked_reason in linked_tasks_info.items():
                  heading, body_text = build_comment(event_type, summary, linked_from=primary_key, reason=linked_reason)
                  results[linked_key] = post_jira_comment(linked_key, heading, body_text)
              return {'statusCode': 200, 'event_type': event_type, 'primary_ticket': primary_key, 'linked_tickets': list(linked_tasks_info.keys())}

  # --- EventBridge Rule ---
  InvestigationEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: devops-agent-jira-callback
      Description: Triggers Jira callback on all DevOps Agent investigation lifecycle events
      State: ENABLED
      EventPattern:
        source:
          - aws.aidevops
        detail-type:
          - Investigation Completed
          - Investigation Linked
          - Investigation Failed
          - Investigation Timed Out
          - Investigation Cancelled
          - Investigation Skipped
      Targets:
        - Id: JiraCallbackLambda
          Arn: !GetAtt CallbackLambda.Arn

  # --- Lambda Permission for EventBridge ---
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref CallbackLambda
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt InvestigationEventRule.Arn

Outputs:
  LambdaFunctionArn:
    Description: Callback Lambda ARN
    Value: !GetAtt CallbackLambda.Arn

  EventBridgeRuleArn:
    Description: EventBridge Rule ARN
    Value: !GetAtt InvestigationEventRule.Arn

  SecretArn:
    Description: Jira credentials secret ARN
    Value: !Ref JiraSecret

Conclusion

This integration connects Jira and DevOps Agent in both directions. A ticket triggers an investigation, and the results come back as a comment — covering completions, linked incidents, failures, and cancellations.

The approach uses API Key auth so Jira can call DevOps Agent directly without a signing proxy. The Lambda is space-independent, handles linked ticket fan-out, and includes the reason for every non-completion event.

The same pattern works with other ticketing systems like Opsgenie with the appropriate API call.

AWS
EXPERT

published a month ago217 views